├── README.md ├── style.css ├── index.html └── src └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # Ascii Art Converter 2 | 3 | Support project for tutorial: [Converting an Image into Ascii Art](https://www.jonathan-petitcolas.com/2017/12/28/converting-image-to-ascii-art.html). 4 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | } 4 | 5 | body { 6 | padding: 2rem 3rem; 7 | font-family: 'VT323', monospace; 8 | line-height: 1.5rem; 9 | font-size: 18px; 10 | } 11 | 12 | header { 13 | display: flex; 14 | align-items: baseline; 15 | font-size: 18px; 16 | } 17 | 18 | header h1 { 19 | margin-right: 1.5rem; 20 | } 21 | 22 | input { 23 | margin-top: 2rem; 24 | font-size: 18px; 25 | } 26 | 27 | pre { 28 | font-family: 'Courier New', 'monospace'; 29 | margin: 1rem auto; 30 | font-size: 8px; 31 | line-height: 1; 32 | } 33 | 34 | footer { 35 | position: absolute; 36 | bottom: 1rem; 37 | } 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ascii Art Converter 8 | 9 | 10 | 11 | 12 |
13 |

Ascii Art Converter

14 |

Upload a picture and turn it into pure ASCII masterpiece!

15 |
16 | 17 | 18 | 19 |

20 | 
21 |     
24 |     
25 | 
26 | 
27 | 


--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
  1 | const canvas = document.getElementById('preview');
  2 | const fileInput = document.querySelector('input[type="file"');
  3 | const asciiImage = document.getElementById('ascii');
  4 | 
  5 | const context = canvas.getContext('2d');
  6 | 
  7 | const toGrayScale = (r, g, b) => 0.21 * r + 0.72 * g + 0.07 * b;
  8 | 
  9 | const getFontRatio = () => {
 10 |     const pre = document.createElement('pre');
 11 |     pre.style.display = 'inline';
 12 |     pre.textContent = ' ';
 13 | 
 14 |     document.body.appendChild(pre);
 15 |     const { width, height } = pre.getBoundingClientRect();
 16 |     document.body.removeChild(pre);
 17 | 
 18 |     return height / width;
 19 | };
 20 | 
 21 | const fontRatio = getFontRatio();
 22 | 
 23 | const convertToGrayScales = (context, width, height) => {
 24 |     const imageData = context.getImageData(0, 0, width, height);
 25 | 
 26 |     const grayScales = [];
 27 | 
 28 |     for (let i = 0 ; i < imageData.data.length ; i += 4) {
 29 |         const r = imageData.data[i];
 30 |         const g = imageData.data[i + 1];
 31 |         const b = imageData.data[i + 2];
 32 | 
 33 |         const grayScale = toGrayScale(r, g, b);
 34 |         imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = grayScale;
 35 | 
 36 |         grayScales.push(grayScale);
 37 |     }
 38 | 
 39 |     context.putImageData(imageData, 0, 0);
 40 | 
 41 |     return grayScales;
 42 | };
 43 | 
 44 | const MAXIMUM_WIDTH = 80;
 45 | const MAXIMUM_HEIGHT = 80;
 46 | 
 47 | const clampDimensions = (width, height) => {
 48 |     const rectifiedWidth = Math.floor(getFontRatio() * width);
 49 | 
 50 |     if (height > MAXIMUM_HEIGHT) {
 51 |         const reducedWidth = Math.floor(rectifiedWidth * MAXIMUM_HEIGHT / height);
 52 |         return [reducedWidth, MAXIMUM_HEIGHT];
 53 |     }
 54 | 
 55 |     if (width > MAXIMUM_WIDTH) {
 56 |         const reducedHeight = Math.floor(height * MAXIMUM_WIDTH / rectifiedWidth);
 57 |         return [MAXIMUM_WIDTH, reducedHeight];
 58 |     }
 59 | 
 60 |     return [rectifiedWidth, height];
 61 | };
 62 | 
 63 | fileInput.onchange = (e) => {
 64 |     const file = e.target.files[0];
 65 | 
 66 |     const reader = new FileReader();
 67 |     reader.onload = (event) => {
 68 |         const image = new Image();
 69 |         image.onload = () => {
 70 |             const [width, height] = clampDimensions(image.width, image.height);
 71 | 
 72 |             canvas.width = width;
 73 |             canvas.height = height;
 74 | 
 75 |             context.drawImage(image, 0, 0, width, height);
 76 |             const grayScales = convertToGrayScales(context, width, height);
 77 | 
 78 |             fileInput.style.display = 'none';
 79 |             drawAscii(grayScales, width);
 80 |         }
 81 | 
 82 |         image.src = event.target.result;
 83 |     };
 84 | 
 85 |     reader.readAsDataURL(file);
 86 | };
 87 | 
 88 | const grayRamp = '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^`\'. ';
 89 | const rampLength = grayRamp.length;
 90 | 
 91 | const getCharacterForGrayScale = grayScale => grayRamp[Math.ceil((rampLength - 1) * grayScale / 255)];
 92 | 
 93 | const drawAscii = (grayScales, width) => {
 94 |     const ascii = grayScales.reduce((asciiImage, grayScale, index) => {
 95 |         let nextChars = getCharacterForGrayScale(grayScale);
 96 |         if ((index + 1) % width === 0) {
 97 |             nextChars += '\n';
 98 |         }
 99 | 
100 |         return asciiImage + nextChars;
101 |     }, '');
102 | 
103 |     asciiImage.textContent = ascii;
104 | };
105 | 


--------------------------------------------------------------------------------