Creating Text Art Using JavaScript

Having fun with the JavaScript p5 library and my cell painting pictures

I love finding new and interesting ways to visualise my work, especially as I am often working with people who have a completely different background to me and I need to communicate what I have done. In doing this I spend a lot of time researching visualisation and I came across a video from The Coding Train on creating ASCII Videos. It’s super cool but never going to come up in my work. But when I decided I wanted an eye-catching graphic for the beginning of a presentation I decided to give it a try.

The idea behind the video is that different ASCII characters take up different amount of space, for example and ‘N’ lights up more pixels than a hyphen ‘-’. This can then be used as a primitive form of shading, in that every pixel of a grayscale image can be replaced with a square and at the center of the square a character could be written. If a pixel is bright than a character should be drawn that will light up a larger area. This effect actually works rather well.

The presentation I am doing is on using machine learning to investigate how cells change shape under different mutations. This means I have loads of cool pictures of cells that look really impressive (just google cell painting for some examples) but I also wanted to add a nod to machine learning, the main focus of my work. I decided the character overlay gave the picture a more computational theme.

This work was done mainly using the p5 JavaScript library, which has an absolutely fabulous online editor. I am not going to rehash the tutorial done by The Coding Train as it is fantastic (and you should go watch it now!) so I will just post my code and point out where I made some changes.

const density = 'Ñ@#W$9876543210?!abc;:+=-,._ '

let cellPainting

function preload() {
  cellPainting = loadImage("CellPainting.png");
}

function setup() {
  createCanvas(720, 432);
}

function draw() {
  background(0);
  
  let w = width/cellPainting.width;
  let h = height/cellPainting.height;
  cellPainting.loadPixels()
  
  let maxavg = 0;
  let minavg = 99999;
  
  for (let i = 0; i < cellPainting.width; i++) {
    for (let j = 0; j < cellPainting.height; j++) {
      const pixelIndex = (i + j * cellPainting.width) * 4;
      const r = cellPainting.pixels[pixelIndex + 0];
      const g = cellPainting.pixels[pixelIndex + 1];
      const b = cellPainting.pixels[pixelIndex + 2];
      const avg = (r+g+b)/3;
      
      if (avg > maxavg) {
        maxavg = avg;
      }
      
      if (avg < minavg) {
        minavg = avg;
      }
      
    }
  }
  
  for (let i = 0; i < cellPainting.width; i++) {
    for (let j = 0; j < cellPainting.height; j++) {
      const pixelIndex = (i + j * cellPainting.width) * 4;
      const r = cellPainting.pixels[pixelIndex + 0];
      const g = cellPainting.pixels[pixelIndex + 1];
      const b = cellPainting.pixels[pixelIndex + 2];
      const avg = (r+g+b)/3;
      
      noStroke();
      fill(r,g,b);
      
      const len = density.length;
      const charIndex = floor(map(avg, minavg, maxavg, 0, len));
      
      
      textSize(w);
      textAlign(CENTER, CENTER);
      text(density.charAt(charIndex), i * w + w * 0.5, j * h + h * 0.5);
    }
  }
}

saveCanvas("MYCanvas", "png")

Firstly, as they did in the video, I used photo shop to downsample the number of pixels in my uploaded image so that more characters could be seen. Then I mostly followed the given code. My first change was a basic change to the size of the canvas but my second was to add two new variables maxavg and minavg. These found the maximum and minimum values used to map to our character set to make sure we were using the full set of values (see also changing the second and third argument of the map function to minavg and maxavg). Then I duplicated the for loop in from the tutorial but this time using it to find the minimum and maximum values for the average RGB brightness. I am sure there is a better way to do this but if it ain’t broke don’t fix it!

I then proceeded to use the for loop from the tutorial but I reverted the fill to show the RGB colour values again (fill(r,g,b)) as the colours are incredibly important in my image. This means I get a different effect than the one maybe intended from the tutorial but it’s something I am very happy with. My final image is shown in the thumbnail, but for my presentation I actually overlayed the original image on top but at very high transparency to get more of the defined cell structure as well.

Hugh Warden
Hugh Warden
PhD Student - Human Genetics Unit

I use machine learning and cell painting to look at how cancer affects the morphology of cells.