Convert An Image Into An ASCII Art Masterpiece With Pure JavaScript

Jonathan Petitcolas
Jonathan PetitcolasFebruary 20, 2018

While browsing Stack Overflow, I often pay attention to the "Hot Network Questions" sidebar. They bring me to several interesting challenges, not necessarily related to development. Recently, I found an interesting post: how do ASCII art image conversion algorithms work?

ASCII art image conversion basically consists in two steps:

  1. Convert a picture into gray colors
  2. Map each pixel to a given character depending on the grayscale value

For instance, @ is darker than +, which is also darker than ..

In this post, I'll implement this algorithm in pure JavaScript, to be run in the browser.

For those in a hurry, you can test the converter directly in final demo, or read its source code directly on its GitHub repository.

Uploading an Image into a Canvas

Homer Simpson

The first step is to allow users to upload a picture. That's the job of the file input element. Moreover, as I'm going to manipulate image pixels, I also need a canvas. So here is the main HTML document I'll use:

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Ascii Art Converter</title>
    <h1>Ascii Art Converter</h1>
        <input type="file" name="picture" />
    <canvas id="preview"></canvas>

At this step, I can send a picture to the input, yet nothing happens. That's because I need to plug the file input to the canvas element, using the FileReader API:

const canvas = document.getElementById("preview");
const fileInput = document.querySelector('input[type="file"');

const context = canvas.getContext("2d");

fileInput.onchange = e => {
  // just handling single file upload
  const file =[0];

  const reader = new FileReader();
  reader.onload = event => {
    const image = new Image();
    image.onload = () => {
      canvas.width = image.width;
      canvas.height = image.height;

      context.drawImage(image, 0, 0);

    image.src =;


When the file input changes, I instantiate a new FileReader object, which reads the file, and loads it into the canvas. Note that I adapt the canvas size to the uploaded image to avoid truncation. The last two arguments of drawImage determine the image placement in the canvas. In my case, I we want to start drawing the image from the top left corner (coordinates [0, 0]).

Once I embed this script on the HTML page, I can upload a Homer Simpson image, and it displays in the canvas element:

Upload Preview in Canvas

Note: If you want to snap a picture from your webcam, please refer to the Taking Picture From Webcam Using Canvas post in this blog.

Turning an Image into Gray Colors

Now that the image has been uploaded, I need to convert it into a grayscale image. The color of each pixel can be broken down into three distinct components: red, green, and blue values, as in hexadecimal (#RRGGBB) colors in CSS.

One simple way to compute corresponding gray scale is to average these three values. However, the human eye is not equally sensitive to these three colors. For instance, our eyes are very sensitive to the green color, while blue is only slightly perceived. Hence, we need to ponderate each color using different weights. After taking a look on the (very) detailed Grayscale Wikipedia Page, I decided to compute the grayscale value using the following formula:

GrayScale = 0.21 R + 0.72 G + 0.07 B

So I need to iterate on each pixel of the picture, to extract its RGB components, and to replace each pixel by its related grayscale value. The canvas API provides a getImageData function to manipulate its pixels one by one:

const toGrayScale = (r, g, b) => 0.21 * r + 0.72 * g + 0.07 * b;

const convertToGrayScales = (context, width, height) => {
  const imageData = context.getImageData(0, 0, width, height);

  const grayScales = [];

  for (let i = 0; i <; i += 4) {
    const r =[i];
    const g =[i + 1];
    const b =[i + 2];

    const grayScale = toGrayScale(r, g, b);[i] =[i + 1] =[
      i + 2
    ] = grayScale;


  context.putImageData(imageData, 0, 0);

  return grayScales;

getImageData produces a uni-dimensional array, where each pixel is splitted into four components: red, green, blue, and alpha (for transparency). So I iterate over this array by increments of 4, retrieve the RGB value from the first three items, compute the scale of gray, and then continue until the end.

In this snippet, I modified the original image data. That means the convertToGrayScales() function is impure. Indeed, I wasn't able to find a way to update image data using a copy of the imageData variable.

After that, it's just a matter of adding a call to convertToGrayScales at the end of the image.onload listener. And now the uploaded picture displays in gray scale:

Grayscale Homer Preview

Mapping Pixels to Characters

To display the image using the ASCII character set, the next step is to choose one character for each pixel of the image. Some characters are darker than others - for instance, @ is darker than ., because it occupies more space on screen. The following character ramp is generally used for the shade-to-character conversion:


Mapping a gray scale value to its equivalent character is just an array lookup:

const grayRamp =
  "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
const rampLength = grayRamp.length;

// the grayScale value is an integer ranging from 0 (black) to 255 (white)
const getCharacterForGrayScale = grayScale =>
  grayRamp[Math.ceil(((rampLength - 1) * grayScale) / 255)];

Let's translate the input image into pure characters:

const asciiImage = document.querySelector("pre#ascii");

const drawAscii = grayScales => {
  const ascii = grayScales.reduce((asciiImage, grayScale) => {
    return asciiImage + getCharacterForGrayScale(grayScale);
  }, "");

  asciiImage.textContent = ascii;

I use a <pre> tag in order to keep the aspect ratio of the picture, as it uses a monospaced font.

Calling the drawAscii method at the end of the image.onload callback, I get the following result:

Drawing ASCII image without break lines

At first glance, it seems it doesn't work. Yet, if you scroll horizontally, you notice some strings wandering through the screen. The picture seems to be on a single line. And indeed: all the pixels are on a single dimensional array. Hence, I need to add a break line every width value:

const drawAscii = (grayScales, width) => {
  const ascii = grayScales.reduce((asciiImage, grayScale, index) => {
    let nextChars = getCharacterForGrayScale(grayScale);

    if ((index + 1) % width === 0) {
      nextChars += "\n";

    return asciiImage + nextChars;
  }, "");

  asciiImage.textContent = ascii;

The result is now far better, except for a little detail...

Drawing far too big ASCII image

The ASCII representation is huge. Indeed, I mapped every single pixel to a character. Drawing a small picture of 10x10 pixels would then take 10 lines of 10 characters. That's too big.

I could keep this huge text picture and reduce the font size as shown in the previous picture. Yet, that's not optimal, especially to allow to share ASCII images by email.

Lowering ASCII Image Definition

When browsing the Web to check how others achieve resolution downgrade, I found numerous references to the average method, also called subsampling:

Computing Average Pixels Value on Image

This technique consists in taking sub-arrays of pixels, and to compute their average grayscale. Then, instead of drawing 9 white pixels for the red section above, I would draw a single one, still completly white.

I first dove into the code, trying to compute this average on the unidimensional array. Yet, after an hour of tying myself in knots, I remembered the next two arguments of drawImage canvas method: the output width and height. Their main goal is to resize the picture before drawing it. Exactly what I need! I wasn't able to find how this is done under the hood, but I guess this is using the same subsampling process.

Let's clamp our image dimension:

const MAXIMUM_WIDTH = 80;
const MAXIMUM_HEIGHT = 50;

const clampDimensions = (width, height) => {
  if (height > MAXIMUM_HEIGHT) {
    const reducedWidth = Math.floor((width * MAXIMUM_HEIGHT) / height);
    return [reducedWidth, MAXIMUM_HEIGHT];

  if (width > MAXIMUM_WIDTH) {
    const reducedHeight = Math.floor((height * MAXIMUM_WIDTH) / width);
    return [MAXIMUM_WIDTH, reducedHeight];

  return [width, height];

I focus on height first. Indeed, to better appreciate the artist behind their work, one needs to contemplate their art without scrolling. Also, note that I preserve the image aspect ratio to prevent some weird distortions.

I now need to update the image.onload handler to use the clamped values:

image.onload = () => {
  const [width, height] = clampDimensions(image.width, image.height);

  canvas.width = width;
  canvas.height = height;

  context.drawImage(image, 0, 0, width, height);
  const grayScales = convertToGrayScales(context, width, height);

  drawAscii(grayScales, width);

Now, once I upload my favorite Simpson character, here is the result:

                                             mr kzB   C'
                                            8  f   @   t
                                            ^  8@m-!l!{o%
                                           w c#1)i!!!!!!!!B
                                        `1)))))?!&]  }&!!)q   p]?
                                         t)))))1|      pU       j
                                         a)))))0        @       f
                                         #))))q         '        ^
                                         i))))@  a8      !    &gt;@ l
                                          t)1)W  li      !      .                           :
                                          8)d1W         "`t@XfC %                         %11x]
                                        ~*@1)@)         @^;ll,|j                         %))[!M)LI
            '&zo!                       ^:fx)X)*       O!!!!!l~^"                       cc/!!J)]~x
            j)!llO                        B*))f)Q{   'B!!!!!!]@;x                      B{{i*W1]!!!q
         "MUB1}!!l{                      ' Z))))&gt;!&lt;(?!!!))){0*&gt;@n                     b1{!!!&gt;f!!c@@
         j!!!Z1*d))@                       q))))-!!&lt;#WwLCm0ft??]!t*.@cw               U)!!!!!ol@1))*
        %1!!!!@+!!!iB                     8)%)))-!@/t/}}11]???????]W-?f              :1}Cl!!l,B)1!!!X
        p))!!f{!!!!!+                    W!i!))){&f]??????????????????Y              Q)&lt;!1!!!:1}!!!l8
       @~jB)&gt;*!!!,f!;k                   xvoh)))@t?????????????????]B?B              %)!lZ","%)!!!@!W
      L!!!!&gt;Q|!!!ll!!q                   k)L))))t)?????????????????t@)*             Y))!!!kBaM~!xCxIx
      B!!!!!&lt;c!!,8!!!"B                  IX11Y)#t??????????????????]f]8            81))!!xl!!!MI_#!u
      B)!!!!!%?!@!!!!!"&lt;b                  ?#))%t????????????????????-0          ~h)))_!!h!!!!!i!i^Y
      W)@|!!l@!!lx!!!!!!"Y(                 8))af???]????????????|B{{@          M)))){!!!!!!!!!!i"@
      'ff/|)xt1!!O!!!!!!!!"w!               @))Wf?????????????????%           -*))))?!!!!!!!!!!,"8
       m11kb1))!!!!!!!!!!!!!"*;             @))8t????????????????%          ;@11)))!!!!!!!!!!!"xf
        o1))))))!!!!!!!!!!!!!I"@            @)))t???????????????@         l@1)))){!!!!!!!!!!!"@
         /m)))))]!!!!!!!!!!!!!!I,@          B)))&/???????????]]q        JM1)))))&lt;!!!!!!!!!!!"%
           bq))))1!!!!!!!!!!!!!!!I,&        W))))W)??????????W:    ` IBY))))))-!!!!!!!!!!!!,B
             @1))))!!!!!!!!!!!!!!!!I;& d.   Z)))))+@}?????}@-&gt;   nJuB1)))))){!!!!!!!!!!!!!"8
              xc)))){!!!!!!!!!!!!!!!!l)h]@  ())11)&lt;ilrh&k/l!^"  a!lll81)))}!!!!!!!!!!!!!!"&
                B)))))-!!!!!!!!!!!!!!!!!(l*#@#X+l     X&gt;!!!!,qQmqlllllC1[!!!!!!!!!!!!!!!"@
                 h()))))!!!!!!!!!!!!!!!]  lLilll'    ..}i!!!:Il [lllll:L!!!!!!!!!!!!!!!"@
                  ,*)))))}!!!!!!!!!!!!l%   lklll     W  q!!!!I?  ~ll"   8!!!!!!!!!!!!!"8
                    &))))))!!!!!!!!!!!!W    l$l,     p   $!!lq   J^      b!!!!!!!!!!:"k
                     &lt;d)))))[!!!!!!!!!+      I}      '    x!@;   o       :l!!!!!!!!"+]
                       @1)))))!!!!!!!!B      `o     |     ]U  B  M        B!!!!!!I"o.
                        Uc)))))&gt;!!!!!i        I|JbooB         ^. o        .&lt;!!!!",a
                         .B)))))}!!!!a         B               @'          @!!:"M`
                           bf)))))!!O.         t               1           >I"1Y
                            ^&)))))i'          .                _           _8
                              @)))1Z                            B           0
                               1Z)@l                            C          `;
                               .;$lll`                          .         Q>
                                @llllll                          ?      {a
                                 zrlllll^                        *   +%x
                                   Zh;llll.                     fB@J
                                     ./MBW8z                      %

The resolution has been decreased, and you can't see as many details as before, but that's a mandatory drawback to get shareable ASCII art.

Handling Image Aspect Ratio

The sharpest eyes have probably noticed that the image aspect ratio is not respected. I handled ASCII characters as if they were square, but in reality, characters are circumscribed in a rectangle. Hence, I need to reflect the distortion on the image pixel cut.

As I chose a monospaced font, the width of every character is the same. Hence, the aspect ratio is be the same for all characters. Yet, how can I compute it? I didn't find any solution in pure CSS, so let's hack some DOM manipulation:

const getFontRatio = () => {
  const pre = document.createElement("pre"); = "inline";
  pre.textContent = " ";

  const { width, height } = pre.getBoundingClientRect();

  return height / width;

const fontRatio = getFontRatio();

The trick lies in adding a <pre> element (to keep exact styling), and to compute the display dimensions using the getBoundingClientRect() function.

Let's update the clampDimensions function to take font ratio into account:

 const clampDimensions = (width, height) => {
+    const rectifiedWidth = Math.floor(getFontRatio() * width);
     if (height > MAXIMUM_HEIGHT) {
-        const reducedWidth = Math.floor(width * MAXIMUM_HEIGHT / height);
+        const reducedWidth = Math.floor(rectifiedWidth * MAXIMUM_HEIGHT / height);
         return [reducedWidth, MAXIMUM_HEIGHT];

     if (width > MAXIMUM_WIDTH) {
-        const reducedHeight = Math.floor(height * MAXIMUM_WIDTH / width);
+        const reducedHeight = Math.floor(height * MAXIMUM_WIDTH / rectifiedWidth);
         return [MAXIMUM_WIDTH, reducedHeight];

-    return [width, height];
+    return [rectifiedWidth, height];

I just compute a cross-shaped product between the font ratio and the maximum dimensions.

And now, my ASCII artwork looks like a masterpiece:

                                                                                                  {W$%p#B@@&!     18%I
                                                                                               `@u    ]@&lt;   cB.      `B&gt;
                                                                                              mp     Mw   :xh@B@@@%Wq1,U0
                                                                                             JZ     WB@ov11&lt;!!!!l!!!!l!j*@%-
                                                                                             @  lB@LB))))_!!!!!!!!!!!!!!!!!^J@|
                                                                                       pU)))))))))))))!ob.           ~$f!(@+           &gt;$u
                                                                                       ;W1)))))))))))hO               .+@)               1#
                                                                                        kj)))))))))1B!                   @.               |w
                                                                                        !81))))))))a)                    ?o           fo^  B'
                                                                                         &n)))))))1B     1@@h             %.          @@J. B.
                                                                                         ;&1)))1))18      l]             ^8.              |d                                                        I+&lt;.
                                                                                          #x)1aOB11oI                    LZ .(*$@q1jB*   &gt;8                                                     "@q{1111O@,
                                                                                      '@B r*)(%1J#1(B'                  /W~"`"",:;:""!$,Zd                                                     &J1)))))[~c@hvY&%,
                            ]&@@@Q.                                                   .B,@I@1Zd)1oU)1Bl                hQ!!!l!!!ll!!!"@OQC                                                   "@)1)){!!!Qb{)))())cM
                          8Y1)-!!!~&k                                                  W&lt; 8Bv8t))(@)))x$[           .*dl!!!!!!!!!!!!ij@!,B`                                                ,%8qmqW@@XI#u))(il!l!!]#
                      ^]c%{)))?!l!!!!hm                                                b/  O@M1)))cf))){l1W$aUjuO&@Z!!!!l&gt;~&gt;ii&lt;_[11j@@b?^B`                                              :B)1)?!!!!!!wB|)1!!l!!!ll#&lt;
                    @0[i!l&lt;*M1))i!li}cq@-                                              o1  '@|))))))))){!!!!l!ll!lIl&lt;?/cvnf/rXwW$8Y}?-i;/8$b&gt;      '&lt;:                                  _W1))-!!!!!!!!!&u!!llp@p|{1d%`
                  jo[!!!!!!llhLt@8x-l!!!l]@&gt;                                                MJ{)))))))))!!!!!_*$&Ort///tt////tttf)???????]?&lt;;_*@v@o&gt;I;am                               ,B())~!!!!!!!!l!l#flMk{)1)())1Wn
                 (#))!!!!!!!!l@Y}!!!!!!!!!,oC                                            U8[lI+**11))))1!!_Batttf|{]??????????????????????????]Cql????lB&lt;                              qU))_!UO!l!!!!!!l+@M1))}!!!!!l!k_
                 Mx1))(_!!!!!@)~!!!!!!!ll!!,W|                                          8+il&gt;l!!i)))))))-@Ytf)???????????????????????????????????????]i@"                              @1))!!!WY!!!!!!!"|&11}!!!!!!lil(d
               f$U+}m@Bv1)&gt;i%/-!!!!!!,!|l!!!^B:                                        ?h)tx##8U))))))1pM/f)?????????????????????????????????????|&{?-ca                              &lt;W1)}!!l!08^,:,^"&lt;@()+!!!!l!#ClitZ
             .B)!!!ll!!lv@vub1!!!!!!;"@~!!!!,_%                                        ^81)/@()1))))))q*tf[?????????????????????????????????????/L@@J+#&lt;                             Z%())]!!!!llU@8b@@r81+!!!!X@LB&lt;!,o&gt;
             qX!l!!!!!!!!l}@b1!!!!!"^8(!!!!!I"|$i                                       +@|)))))11)))v8tt)??????????????????????????????????????(th&/-Z_                          '8p1))))~!!!ll*ml!l!!lC%&gt;!!!!IX8!!:CO
             Bx}!!!!!!!!!!!!BXi!""^QB!l!!!!!!l""`L$-                                      }B$B@$w1))1&ztf????????????????????????????????????????????+k~                        k@j))))))[!!!!l8]il!!!!!!!x&$@h1!l!"Cp
             Jm)(@)!!!!!!!!lzqW$$@ux@!!!!!!!!!!!l"""x$X                                       1k))))(@tf|???????????????????????????????????(|??????Iqw                      fBm1)))))))?!!!!lUX!!!!!!!!!!!!l!!!!"^&u
             `@|)fBM@q-i!!+18u{!!l!!wJ!!!!!!!!!!!!!iI"^&gt;8&,                                   &lt;&1)))|Btt1????????????????????????????????????n%@$$$Bf                     !%M1))))))))]!!!!!!!l!!!!!!!!!!!!!!i;"^wh
              l@()|@m{1)1{p@|))[l!!!rYl!!!!!!!!!!!!!!!!!;^`z$U                                ;@1))))Bft)???????????????????????????????????-bb                       'nB#))))))))))+!!!!!!!!!!!!!!!!!!!!!!:"",Wo
                @O))1(XLX11)))))}!!!!!!!!!!!!!!!!!!!!!!!!!!l""l#B!                            :@1))))wktt??????????????????????????????????Q8.                     lm@w1)))))))))[!!!!!!!!!!!!!!!!!!!!!!l"":a@&lt;
                 I@m1))))))))))))}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!:"^(@m                         I@1))))1#Ot)???????????????????????????????bo                    ;w@M/1))))))))){i!!!!!!!!!!!!!!!!!!!!!!l""-@&gt;
                    aBj1)))))))))))&lt;!!!!!!!!!!!!!!!!!!!!!!!!!!ll!!!:""q@1                     i81))))))kMt[???????????????????????????{BC                  |8@w1)1))))))))))&lt;!!!!!!!!!!!!!!!!!!!!!!!!,";@/
                       d@k|))))))))))+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!l"^?%h'                 |a1)))))))1%M)???????????????????????{&W'               ?o@&n1))))))))))))1i!!!!!!!!!!!!!!!!!!!!!!!!!,""%z
                          :B%))))))))))}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!l!i;"`U$x              JJ))))))))))[C@k}?]??????????????1k$@z         U@B@8q@@L11)))))))))))))}!!!!!!!!!!!!!!!!!!!!!!!!!!!:"^#w
                             }$Z1))))))))){!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!;&gt;8@M&lt;&8@~     B()))))))))){ll!~m@$80r|(tvZM@$M|I"^o+      i@#IlllI{@%(1))))))))))}&gt;!!!!!!!!!!!!!!!!!!!!!!!!!!!!;"^q*
                                Q@r))))))))))1&gt;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!lz@ol&lt;B@; &gt;W{)|nLbM8B@$$$$$$$$$@#t;Illl!l!!!l"^@x:;l1U@1lllllllllIU@/1)))){&gt;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!I"^L8.
                                  `M8()))))))))))~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!l&f^llIc@%0(&gt;;;Illll"            )@&lt;l!!!!!!!",@;;II;;Z8IlllllllllllrB1+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!;"^Z&
                                     ?@d1)))))))))))~l!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!}@    ,llU@+lllllll`          'B"  O8!!!!!!!";B;l!$'  JQllllllllll`  Qhl!!!!!!!!!!!!!!!!!!!!!!!!!!!l!I^^q*
                                        v@U1)))))))))))&lt;!!!!!!!!!!!!!!!!!!!!!!!!!!l!Mc.     ^lIWqlllll"           8}     @r!!!l!^IW;oz    `Bllllll^       .@?!!!!!!!!!!!!!!!!!!!!!!!!!!:"^aa
                                           k@f)))))))))))1&gt;!!!!!!!!!!!!!!!!!!!!!!!l|B         :lfBlllI           )M       m*!!!!"I@B'.     o]l^             wQl!!!!!!!!!!!!!!!!!!!!!l^";BJ
                                             ;B&)1))))))))))}!!!l!!!!!!!!!!!!!!!!l&gt;@"          .li@_I            %I        &lt;B!!l^*j"$'     Lx                jh!!!!!!!!!!!!!!!!l!!:^"[$]
                                                j@w1)))))))))))-!!!!!!!!!!!!!!!!!lW&gt;             IIB;           -h           @]}%.   @^    )0                 -%!!!!!!!!!!!!!!!;"""**`
                                                   b@u1)))))))))))i!!!!!!!!!!!!!!W?               :!@^          W!           .}).    '$^   &lt;Q                  l&l!!!!!!!l!iI""^J@_
                                                     `%%|)))))))))))?!!!!!!!!!!l8I                 .~${-&gt;I,^``''                      ,@B$$$/                   {o!l!!!!!I""^r@t
                                                        -$a1))))))))))1i!!!!!l!@..                   Zt                                |b                        cX!!i;""^u@x
                                                           0@J)))))))))))~l!!(%                      .M                                 d}                        $!^""0@[
                                                              &Bt))))))))))?kU                        p                                 `@'                       {k*%;
                                                                +@#1))))))f$,                                                            nL                       `B
                                                                   UBL1)1WZll`                                                            B                       qz
                                                                   '@:O@%llllll,                                                          zJ                   `&W

Far better, isn't it? Reducing the font size also helps to get a better overview of the converted picture.


As usual, here are the related links:

Note that I only handled a static image in this case, but the same technique applies to video streams. It exists: take a look at the ASCII camera. Useless, therefore indispensable!

Did you like this article? Share it!