Last active
June 5, 2023 17:22
-
-
Save rsalgado/4a09e888cdaddd97d029a1dc0631ffc9 to your computer and use it in GitHub Desktop.
ASCII Art Image Converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="contents"> | |
<h1>ASCII Art Image Converter</h1> | |
<main id="main"> | |
<div id="left-section" class="section"> | |
<h2>Input</h2> | |
<div> | |
<h3>Input File (Select an Image):</h3> | |
<p>If the input is not a square image, a square section of it will be selected. More precisely, the image will be cropped to be square by using its smallest side.</p> | |
<input type="file" id="file-input"> | |
</div> | |
<div> | |
<h3>Preview:</h3> | |
<canvas id="preview-canvas"></canvas> | |
<p>This is the preview of (the part of) the image that will be rendered.</p> | |
</div> | |
<div> | |
<h3>Reduced Grayscale Image:</h3> | |
<canvas id="reduced-canvas"></canvas> | |
<p> | |
This is the grayscale reduced version of the image. It will be used to render | |
the ASCII output. Each pixel from this image will be mapped to a character from the | |
ramp, based on its value (luminosity). | |
</p> | |
</div> | |
<div> | |
<h3>Character Ramp:</h3> | |
<input type="text" id="ramp-input" value="8@S#0I?J=!;:-,. " /> | |
<button id="generate-ascii-button">Generate ASCII Art</button> | |
<p> | |
This is the characters ("colors") ramp for the ASCII output. | |
It can contain any number of characters. The order should be from "darker" | |
characters to "lighter" ones, usually with a whitespace as the last character. | |
</p> | |
</div> | |
</div> | |
<div id="right-section" class="section"> | |
<h2>Output</h2> | |
<div id="output-container"> | |
<pre id="ascii-output"></pre> | |
</div> | |
</div> | |
</main> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Constants | |
const MAX_PREVIEW_SIZE = 400; | |
const OUTPUT_SIZE = 128; | |
// Canvas contexts | |
const previewContext = document.querySelector("#preview-canvas").getContext("2d"); | |
const reducedContext = document.querySelector("#reduced-canvas").getContext("2d"); | |
// Functions | |
const getLuminosity = (r, g, b) => 0.21*r + 0.72*g + 0.07*b; | |
const toGrayScaleInPlace = (context, size) => { | |
let imageData = context.getImageData(0, 0, size, size); | |
for (let i = 0; i < imageData.data.length; i += 4) { | |
const r = imageData.data[i]; | |
const g = imageData.data[i + 1]; | |
const b = imageData.data[i + 2]; | |
const luminosity = getLuminosity(r, g, b); | |
imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = luminosity; | |
}; | |
context.putImageData(imageData, 0, 0); | |
}; | |
const getGrayScaleValues = (context) => { | |
const size = context.canvas.clientWidth; | |
const imageData = context.getImageData(0, 0, size, size); | |
const grayScaleValues = []; | |
let value = null; | |
for (let i = 0; i < imageData.data.length; i+= 4) { | |
// Pick the first channel's (red) color. Any other works are they are equal. | |
value = imageData.data[i]; | |
grayScaleValues.push(value); | |
} | |
return grayScaleValues; | |
} | |
const getCharacter = (value, ramp) => { | |
const L = ramp.length; | |
const quantizedValue = Math.trunc((value / 256) * L); | |
return ramp[quantizedValue]; | |
} | |
const drawASCII = (grayScaleValues, width, ramp, outputElement) => { | |
let textOutput = ""; | |
for (let i = 0; i < grayScaleValues.length; i++) { | |
let character = getCharacter(grayScaleValues[i], ramp); | |
textOutput += character; | |
if ((i + 1) % width === 0) | |
textOutput += "\n"; | |
} | |
outputElement.textContent = textOutput; | |
}; | |
const initialize = () => { | |
const reducedCanvas = document.querySelector("#reduced-canvas"); | |
const ramp = document.querySelector("#ramp-input").value; | |
const asciiOutput = document.querySelector("#ascii-output"); | |
reducedCanvas.width = OUTPUT_SIZE; | |
reducedCanvas.height = OUTPUT_SIZE; | |
reducedContext.rect(0, 0, OUTPUT_SIZE, OUTPUT_SIZE); | |
reducedContext.fill(); | |
const grayScaleValues = getGrayScaleValues(reducedContext); | |
drawASCII(grayScaleValues, OUTPUT_SIZE, ramp, asciiOutput); | |
}; | |
// DOM Manipulation and Event Handlers. | |
document.querySelector("#file-input").addEventListener("change", (e) => { | |
const imageFile = e.target.files[0]; | |
const fileReader = new FileReader(); | |
fileReader.addEventListener("load", (loadEvent) => { | |
const image = new Image(); | |
image.addEventListener("load", () => { | |
const previewCanvas = document.querySelector("#preview-canvas"); | |
const side = Math.min(image.width, image.height); | |
const previewSide = Math.min(side, MAX_PREVIEW_SIZE); | |
previewCanvas.width = previewSide; | |
previewCanvas.height = previewSide; | |
previewContext.drawImage(image, 0, 0, side, side, 0, 0, previewSide, previewSide); | |
reducedContext.drawImage(image, 0, 0, side, side, 0, 0, OUTPUT_SIZE, OUTPUT_SIZE); | |
toGrayScaleInPlace(reducedContext, OUTPUT_SIZE); | |
const ramp = document.querySelector("#ramp-input").value; | |
const asciiOutput = document.querySelector("#ascii-output"); | |
const grayScaleValues = getGrayScaleValues(reducedContext); | |
drawASCII(grayScaleValues, OUTPUT_SIZE, ramp, asciiOutput); | |
}); | |
image.src = loadEvent.target.result; | |
}); | |
fileReader.readAsDataURL(imageFile); | |
}); | |
document.querySelector("#generate-ascii-button").addEventListener("click", () => { | |
const ramp = document.querySelector("#ramp-input").value; | |
const asciiOutput = document.querySelector("#ascii-output"); | |
const grayScaleValues = getGrayScaleValues(reducedContext); | |
drawASCII(grayScaleValues, OUTPUT_SIZE, ramp, asciiOutput); | |
}); | |
// Initialize the preview and output, just to have something visible. | |
initialize(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* NOTE: | |
I chose some of the values manually, after getting tired of | |
fiddling with CSS and trying to find an optimal responsive desing. I think this will do for now. | |
*/ | |
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap'); | |
* { | |
box-sizing: border-box; | |
} | |
#contents { | |
padding: 1rem; | |
background: #eee; | |
min-height: 100vh; | |
} | |
#main { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 1rem; | |
} | |
.section { | |
background: #fff; | |
box-shadow: 0px 0px 4px 2px #ddd; | |
} | |
#left-section { | |
width: 480px; | |
padding: 1rem; | |
} | |
#left-section canvas { | |
border: 1px solid gray; | |
} | |
#right-section { | |
flex-grow: 1; | |
flex-shrink: 0; | |
padding: 0 1rem; | |
/*min-width: 800px; */ | |
} | |
#output-container { | |
display: flex; | |
justify-content: center; | |
} | |
#ascii-output { | |
margin: 1rem 0; | |
/* border: 1px solid gray; */ | |
display: inline-block; | |
font-size: 6px; | |
font-family: "Space Mono", monospace; | |
line-height: 100%; | |
font-weight: bold; | |
letter-spacing: 2.75px; | |
} | |
#ramp-input { | |
font-family: "Space Mono", monospace; | |
font-weight: bold; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment