├── 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 |
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 |
--------------------------------------------------------------------------------