Skip to content

Instantly share code, notes, and snippets.

@charbelrami
Created September 3, 2025 01:17
Show Gist options
  • Save charbelrami/048cf09250e186e76ab210d3075681db to your computer and use it in GitHub Desktop.
Save charbelrami/048cf09250e186e76ab210d3075681db to your computer and use it in GitHub Desktop.
Monospace (ASCII art generator)
<div class="layout">
<aside class="sidebar">
<section class="control-group global-settings">
<h3>Global Settings</h3>
<div class="control">
<label for="theme">Theme:</label>
<select id="theme">
<option value="dark" selected>Dark</option>
<option value="light">Light</option>
</select>
</div>
<div class="control">
<label for="ignoreWhite">Ignore Pure White:</label>
<input type="checkbox" id="ignoreWhite" checked>
</div>
</section>
<section class="control-group upload-group">
<h3>1. Upload Your File</h3>
<div class="control">
<input type="file" id="upload" accept="image/*">
</div>
</section>
<section class="control-group image-processing">
<h3>2. Basic Adjustments</h3>
<div class="control">
<label for="asciiWidth">Output Width (chars):</label>
<input type="range" id="asciiWidth" min="20" max="300" value="150">
<span class="value-label" id="asciiWidthVal">100</span>
</div>
<div class="control">
<label for="brightness">Brightness:</label>
<input type="range" id="brightness" min="-100" max="100" value="0">
<span class="value-label" id="brightnessVal">0</span>
</div>
<div class="control">
<label for="contrast">Contrast:</label>
<input type="range" id="contrast" min="-100" max="100" value="0">
<span class="value-label" id="contrastVal">0</span>
</div>
<div class="control">
<label for="blur">Blur (px):</label>
<input type="range" id="blur" min="0" max="10" step="0.01" value="0">
<span class="value-label" id="blurVal">0</span>
</div>
<div class="control">
<label for="invert">Invert Colors:</label>
<input type="checkbox" id="invert">
</div>
</section>
<section class="control-group dithering-settings">
<h3>3. Dithering Options</h3>
<div class="control">
<label for="dithering">Enable Dithering:</label>
<input type="checkbox" id="dithering" checked>
</div>
<div class="control">
<label for="ditherAlgorithm">Dither Algorithm:</label>
<select id="ditherAlgorithm">
<option value="floyd" selected>Floyd–Steinberg</option>
<option value="atkinson">Atkinson</option>
<option value="noise">Noise</option>
<option value="ordered">Ordered</option>
</select>
</div>
</section>
<section class="control-group charset-settings">
<h3>4. Character Set</h3>
<div class="control">
<label for="charset">Select Set:</label>
<select id="charset">
<option value="detailed" selected>Detailed</option>
<option value="standard">Standard</option>
<option value="blocks">Blocks</option>
<option value="binary">Binary</option>
<option value="hex">Hex</option>
<option value="manual">Manual</option>
</select>
</div>
<div class="control" id="manualCharControl" style="display: none;">
<label for="manualCharInput">Manual Character:</label>
<input type="text" id="manualCharInput" maxlength="1" value="0">
</div>
</section>
<section class="control-group edge-detection-settings">
<h3>5. Edge Detection</h3>
<p>Select one edge detection method:</p>
<div class="control">
<input type="radio" name="edgeMethod" id="edgeNone" value="none" checked>
<label for="edgeNone">No Edge Detection</label>
</div>
<div class="control">
<input type="radio" name="edgeMethod" id="edgeSobel" value="sobel">
<label for="edgeSobel">Sobel Edge Detection</label>
</div>
<div class="control">
<input type="radio" name="edgeMethod" id="edgeDoG" value="dog">
<label for="edgeDoG">DoG (Contour) Detection</label>
</div>
<div class="control" id="sobelThresholdControl" style="display: none;">
<label for="edgeThreshold">Sobel Threshold:</label>
<input type="range" id="edgeThreshold" min="0" max="255" value="100">
<span class="value-label" id="edgeThresholdVal">100</span>
</div>
<div class="control" id="dogThresholdControl" style="display: none;">
<label for="dogEdgeThreshold">DoG Threshold:</label>
<input type="range" id="dogEdgeThreshold" min="0" max="255" value="100">
<span class="value-label" id="dogEdgeThresholdVal">100</span>
</div>
</section>
<section class="control-group display-settings">
<h3>6. Display Settings</h3>
<div class="control">
<label for="zoom">Zoom (%):</label>
<input type="range" id="zoom" min="20" max="600" value="100">
<span class="value-label" id="zoomVal">100</span>
</div>
</section>
<section class="control-group misc-settings">
<div class="control">
<button id="reset">Reset All Settings</button>
</div>
</section>
</aside>
<main class="main-content">
<pre id="ascii-art"></pre>
<button id="copyBtn">Copy ASCII Art</button>
<button id="downloadBtn">Download PNG</button>
</main>
</div>
<canvas id="canvas" style="display: none;"></canvas>
// ---------------- Global Variables ----------------
let currentImage = null;
const baseFontSize = 7; // Base font size for ASCII art
// ---------------- Helper Functions ----------------
// Clamp a value between min and max.
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
// Generate a normalized 2D Gaussian kernel.
function gaussianKernel2D(sigma, kernelSize) {
const kernel = [];
const half = Math.floor(kernelSize / 2);
let sum = 0;
for (let y = -half; y <= half; y++) {
const row = [];
for (let x = -half; x <= half; x++) {
const value = Math.exp(-(x * x + y * y) / (2 * sigma * sigma));
row.push(value);
sum += value;
}
kernel.push(row);
}
// Normalize the kernel.
for (let y = 0; y < kernelSize; y++) {
for (let x = 0; x < kernelSize; x++) {
kernel[y][x] /= sum;
}
}
return kernel;
}
// Convolve a 2D image (array) with a 2D kernel.
function convolve2D(img, kernel) {
const height = img.length,
width = img[0].length;
const kernelSize = kernel.length,
half = Math.floor(kernelSize / 2);
const output = [];
for (let y = 0; y < height; y++) {
output[y] = [];
for (let x = 0; x < width; x++) {
let sum = 0;
for (let ky = 0; ky < kernelSize; ky++) {
for (let kx = 0; kx < kernelSize; kx++) {
const yy = y + ky - half;
const xx = x + kx - half;
let pixel = (yy >= 0 && yy < height && xx >= 0 && xx < width) ? img[yy][xx] : 0;
sum += pixel * kernel[ky][kx];
}
}
output[y][x] = sum;
}
}
return output;
}
// Compute the Difference of Gaussians on a 2D grayscale image.
function differenceOfGaussians2D(gray, sigma1, sigma2, kernelSize) {
const kernel1 = gaussianKernel2D(sigma1, kernelSize);
const kernel2 = gaussianKernel2D(sigma2, kernelSize);
const blurred1 = convolve2D(gray, kernel1);
const blurred2 = convolve2D(gray, kernel2);
const height = gray.length,
width = gray[0].length;
const dog = [];
for (let y = 0; y < height; y++) {
dog[y] = [];
for (let x = 0; x < width; x++) {
dog[y][x] = blurred1[y][x] - blurred2[y][x];
}
}
return dog;
}
// Apply the Sobel operator to a 2D image, returning gradient magnitude and angle arrays.
function applySobel2D(img, width, height) {
const mag = [],
angle = [];
for (let y = 0; y < height; y++) {
mag[y] = [];
angle[y] = [];
for (let x = 0; x < width; x++) {
mag[y][x] = 0;
angle[y][x] = 0;
}
}
const kernelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const kernelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let Gx = 0, Gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pixel = img[y + ky][x + kx];
Gx += pixel * kernelX[ky + 1][kx + 1];
Gy += pixel * kernelY[ky + 1][kx + 1];
}
}
const g = Math.sqrt(Gx * Gx + Gy * Gy);
mag[y][x] = g;
let theta = Math.atan2(Gy, Gx) * (180 / Math.PI);
if (theta < 0) theta += 180;
angle[y][x] = theta;
}
}
return { mag, angle };
}
// Non-maximum suppression to thin out the edges.
function nonMaxSuppression(mag, angle, width, height) {
const suppressed = [];
for (let y = 0; y < height; y++) {
suppressed[y] = [];
for (let x = 0; x < width; x++) {
suppressed[y][x] = 0;
}
}
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const currentMag = mag[y][x];
let neighbor1 = 0, neighbor2 = 0;
const theta = angle[y][x];
if ((theta >= 0 && theta < 22.5) || (theta >= 157.5 && theta <= 180)) {
// 0° direction: compare left and right.
neighbor1 = mag[y][x - 1];
neighbor2 = mag[y][x + 1];
} else if (theta >= 22.5 && theta < 67.5) {
// 45° direction: compare top-right and bottom-left.
neighbor1 = mag[y - 1][x + 1];
neighbor2 = mag[y + 1][x - 1];
} else if (theta >= 67.5 && theta < 112.5) {
// 90° direction: compare top and bottom.
neighbor1 = mag[y - 1][x];
neighbor2 = mag[y + 1][x];
} else if (theta >= 112.5 && theta < 157.5) {
// 135° direction: compare top-left and bottom-right.
neighbor1 = mag[y - 1][x - 1];
neighbor2 = mag[y + 1][x + 1];
}
suppressed[y][x] = (currentMag >= neighbor1 && currentMag >= neighbor2) ? currentMag : 0;
}
}
return suppressed;
}
// ---------------- ASCII Art Generation Functions ----------------
// Generate standard ASCII art (non-DOG modes).
function generateASCII(img) {
const edgeMethod = document.querySelector('input[name="edgeMethod"]:checked').value;
if (edgeMethod === 'dog') {
generateContourASCII(img);
return;
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const asciiWidth = parseInt(document.getElementById('asciiWidth').value, 10);
const brightness = parseFloat(document.getElementById('brightness').value);
const contrastValue = parseFloat(document.getElementById('contrast').value);
const blurValue = parseFloat(document.getElementById('blur').value);
const ditheringEnabled = document.getElementById('dithering').checked;
const ditherAlgorithm = document.getElementById('ditherAlgorithm').value;
const invertEnabled = document.getElementById('invert').checked;
const ignoreWhite = document.getElementById('ignoreWhite').checked;
const charset = document.getElementById('charset').value;
let gradient;
switch (charset) {
case 'standard': gradient = "@%#*+=-:."; break;
case 'blocks': gradient = "█▓▒░ "; break;
case 'binary': gradient = "01"; break;
case 'manual':
const manualChar = document.getElementById('manualCharInput').value || "0";
gradient = manualChar + " ";
break;
case 'hex': gradient = "0123456789ABCDEF"; break;
case 'detailed':
default: gradient = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'.";
break;
}
const nLevels = gradient.length;
const contrastFactor = (259 * (contrastValue + 255)) / (255 * (259 - contrastValue));
const fontAspectRatio = 0.55;
const asciiHeight = Math.round((img.height / img.width) * asciiWidth * fontAspectRatio);
canvas.width = asciiWidth;
canvas.height = asciiHeight;
ctx.filter = blurValue > 0 ? `blur(${blurValue}px)` : "none";
ctx.drawImage(img, 0, 0, asciiWidth, asciiHeight);
const imageData = ctx.getImageData(0, 0, asciiWidth, asciiHeight);
const data = imageData.data;
let gray = [], grayOriginal = [];
for (let i = 0; i < data.length; i += 4) {
let lum = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
if (invertEnabled) lum = 255 - lum;
let adjusted = clamp(contrastFactor * (lum - 128) + 128 + brightness, 0, 255);
gray.push(adjusted);
grayOriginal.push(adjusted);
}
let ascii = "";
if (document.querySelector('input[name="edgeMethod"]:checked').value === 'sobel') {
const threshold = parseInt(document.getElementById('edgeThreshold').value, 10);
gray = applyEdgeDetection(gray, asciiWidth, asciiHeight, threshold);
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
const idx = y * asciiWidth + x;
if (ignoreWhite && grayOriginal[idx] === 255) {
line += " ";
continue;
}
const computedLevel = Math.round((gray[idx] / 255) * (nLevels - 1));
line += gradient.charAt(computedLevel);
}
ascii += line + "\n";
}
} else if (ditheringEnabled) {
if (ditherAlgorithm === 'floyd') {
// Floyd–Steinberg dithering
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
const idx = y * asciiWidth + x;
if (ignoreWhite && grayOriginal[idx] === 255) { line += " "; continue; }
let computedLevel = Math.round((gray[idx] / 255) * (nLevels - 1));
line += gradient.charAt(computedLevel);
const newPixel = (computedLevel / (nLevels - 1)) * 255;
const error = gray[idx] - newPixel;
if (x + 1 < asciiWidth) {
gray[idx + 1] = clamp(gray[idx + 1] + error * (7 / 16), 0, 255);
}
if (x - 1 >= 0 && y + 1 < asciiHeight) {
gray[idx - 1 + asciiWidth] = clamp(gray[idx - 1 + asciiWidth] + error * (3 / 16), 0, 255);
}
if (y + 1 < asciiHeight) {
gray[idx + asciiWidth] = clamp(gray[idx + asciiWidth] + error * (5 / 16), 0, 255);
}
if (x + 1 < asciiWidth && y + 1 < asciiHeight) {
gray[idx + asciiWidth + 1] = clamp(gray[idx + asciiWidth + 1] + error * (1 / 16), 0, 255);
}
}
ascii += line + "\n";
}
} else if (ditherAlgorithm === 'atkinson') {
// Atkinson dithering
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
const idx = y * asciiWidth + x;
if (ignoreWhite && grayOriginal[idx] === 255) { line += " "; continue; }
let computedLevel = Math.round((gray[idx] / 255) * (nLevels - 1));
line += gradient.charAt(computedLevel);
const newPixel = (computedLevel / (nLevels - 1)) * 255;
const error = gray[idx] - newPixel;
const diffusion = error / 8;
if (x + 1 < asciiWidth) {
gray[idx + 1] = clamp(gray[idx + 1] + diffusion, 0, 255);
}
if (x + 2 < asciiWidth) {
gray[idx + 2] = clamp(gray[idx + 2] + diffusion, 0, 255);
}
if (y + 1 < asciiHeight) {
if (x - 1 >= 0) {
gray[idx - 1 + asciiWidth] = clamp(gray[idx - 1 + asciiWidth] + diffusion, 0, 255);
}
gray[idx + asciiWidth] = clamp(gray[idx + asciiWidth] + diffusion, 0, 255);
if (x + 1 < asciiWidth) {
gray[idx + asciiWidth + 1] = clamp(gray[idx + asciiWidth + 1] + diffusion, 0, 255);
}
}
if (y + 2 < asciiHeight) {
gray[idx + 2 * asciiWidth] = clamp(gray[idx + 2 * asciiWidth] + diffusion, 0, 255);
}
}
ascii += line + "\n";
}
} else if (ditherAlgorithm === 'noise') {
// Noise dithering
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
const idx = y * asciiWidth + x;
if (ignoreWhite && grayOriginal[idx] === 255) { line += " "; continue; }
const noise = (Math.random() - 0.5) * (255 / nLevels);
const noisyValue = clamp(gray[idx] + noise, 0, 255);
let computedLevel = Math.round((noisyValue / 255) * (nLevels - 1));
line += gradient.charAt(computedLevel);
}
ascii += line + "\n";
}
} else if (ditherAlgorithm === 'ordered') {
// Ordered dithering using a 4x4 Bayer matrix.
const bayer = [
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5]
];
const matrixSize = 4;
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
const idx = y * asciiWidth + x;
if (ignoreWhite && grayOriginal[idx] === 255) { line += " "; continue; }
const p = gray[idx] / 255;
const t = (bayer[y % matrixSize][x % matrixSize] + 0.5) / (matrixSize * matrixSize);
let valueWithDither = p + t - 0.5;
valueWithDither = Math.min(Math.max(valueWithDither, 0), 1);
let computedLevel = Math.floor(valueWithDither * nLevels);
if (computedLevel >= nLevels) computedLevel = nLevels - 1;
line += gradient.charAt(computedLevel);
}
ascii += line + "\n";
}
}
} else {
// Simple mapping without dithering.
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
const idx = y * asciiWidth + x;
if (ignoreWhite && grayOriginal[idx] === 255) { line += " "; continue; }
const computedLevel = Math.round((gray[idx] / 255) * (nLevels - 1));
line += gradient.charAt(computedLevel);
}
ascii += line + "\n";
}
}
document.getElementById('ascii-art').textContent = ascii;
}
// Apply simple Sobel edge detection on a 1D grayscale array.
function applyEdgeDetection(gray, width, height, threshold) {
let edges = new Array(width * height).fill(255);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let idx = y * width + x;
let a = gray[(y - 1) * width + (x - 1)];
let b = gray[(y - 1) * width + x];
let c = gray[(y - 1) * width + (x + 1)];
let d = gray[y * width + (x - 1)];
let e = gray[y * width + x];
let f = gray[y * width + (x + 1)];
let g = gray[(y + 1) * width + (x - 1)];
let h = gray[(y + 1) * width + x];
let i = gray[(y + 1) * width + (x + 1)];
let Gx = (-1 * a) + (0 * b) + (1 * c) +
(-2 * d) + (0 * e) + (2 * f) +
(-1 * g) + (0 * h) + (1 * i);
let Gy = (-1 * a) + (-2 * b) + (-1 * c) +
(0 * d) + (0 * e) + (0 * f) +
(1 * g) + (2 * h) + (1 * i);
let magVal = Math.sqrt(Gx * Gx + Gy * Gy);
let normalized = (magVal / 1442) * 255;
edges[idx] = normalized > threshold ? 0 : 255;
}
}
return edges;
}
// Generate contour-based ASCII art using DoG and Sobel with non-maximum suppression.
// Now basic adjustments (brightness, contrast, blur, invert) are also applied.
function generateContourASCII(img) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const asciiWidth = parseInt(document.getElementById('asciiWidth').value, 10);
const brightness = parseFloat(document.getElementById('brightness').value);
const contrastValue = parseFloat(document.getElementById('contrast').value);
const blurValue = parseFloat(document.getElementById('blur').value);
const invertEnabled = document.getElementById('invert').checked;
const fontAspectRatio = 0.55;
const asciiHeight = Math.round((img.height / img.width) * asciiWidth * fontAspectRatio);
canvas.width = asciiWidth;
canvas.height = asciiHeight;
// Use blur filter if applicable.
ctx.filter = blurValue > 0 ? `blur(${blurValue}px)` : "none";
ctx.drawImage(img, 0, 0, asciiWidth, asciiHeight);
const imageData = ctx.getImageData(0, 0, asciiWidth, asciiHeight);
const data = imageData.data;
let gray2d = [];
const contrastFactor = (259 * (contrastValue + 255)) / (255 * (259 - contrastValue));
for (let y = 0; y < asciiHeight; y++) {
gray2d[y] = [];
for (let x = 0; x < asciiWidth; x++) {
const idx = (y * asciiWidth + x) * 4;
let lum = 0.299 * data[idx] + 0.587 * data[idx+1] + 0.114 * data[idx+2];
if (invertEnabled) lum = 255 - lum;
lum = clamp(contrastFactor * (lum - 128) + 128 + brightness, 0, 255);
gray2d[y][x] = lum;
}
}
const sigma1 = 0.5, sigma2 = 1.0, kernelSize = 3;
const dog = differenceOfGaussians2D(gray2d, sigma1, sigma2, kernelSize);
const { mag, angle } = applySobel2D(dog, asciiWidth, asciiHeight);
const suppressedMag = nonMaxSuppression(mag, angle, asciiWidth, asciiHeight);
const threshold = parseInt(document.getElementById('dogEdgeThreshold').value, 10);
let ascii = "";
for (let y = 0; y < asciiHeight; y++) {
let line = "";
for (let x = 0; x < asciiWidth; x++) {
if (suppressedMag[y][x] > threshold) {
let adjustedAngle = (angle[y][x] + 90) % 180;
let edgeChar = (adjustedAngle < 22.5 || adjustedAngle >= 157.5) ? "-" :
(adjustedAngle < 67.5) ? "/" :
(adjustedAngle < 112.5) ? "|" : "\\";
line += edgeChar;
} else {
line += " ";
}
}
ascii += line + "\n";
}
document.getElementById('ascii-art').textContent = ascii;
}
// ---------------- Download Function ----------------
function downloadPNG() {
const preElement = document.getElementById('ascii-art');
const asciiText = preElement.textContent;
if (!asciiText.trim()) {
alert("No ASCII art to download.");
return;
}
// Split the ASCII art into lines.
const lines = asciiText.split("\n");
// Set a scaling factor (2x resolution for better quality).
const scaleFactor = 2;
// Define the border margin (in final pixels, then scaled)
const borderMargin = 20 * scaleFactor;
// Get computed style values from the pre element.
const computedStyle = window.getComputedStyle(preElement);
const baseFontSize = parseInt(computedStyle.fontSize, 10);
const fontSize = baseFontSize * scaleFactor;
// Create a temporary canvas to measure text dimensions.
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCtx.font = `${fontSize}px Consolas, Monaco, "Liberation Mono", monospace`;
// Determine the maximum line width.
let maxLineWidth = 0;
for (let i = 0; i < lines.length; i++) {
const lineWidth = tempCtx.measureText(lines[i]).width;
if (lineWidth > maxLineWidth) {
maxLineWidth = lineWidth;
}
}
// Calculate the required text dimensions.
const lineHeight = fontSize; // Basic line height.
const textWidth = Math.ceil(maxLineWidth);
const textHeight = Math.ceil(lines.length * lineHeight);
// Create an offscreen canvas with extra space for the border margin.
const canvasWidth = textWidth + 2 * borderMargin;
const canvasHeight = textHeight + 2 * borderMargin;
const offCanvas = document.createElement('canvas');
offCanvas.width = canvasWidth;
offCanvas.height = canvasHeight;
const offCtx = offCanvas.getContext('2d');
// Fill the background based on the current theme.
const bgColor = document.body.classList.contains('light-mode') ? "#fff" : "#000";
offCtx.fillStyle = bgColor;
offCtx.fillRect(0, 0, canvasWidth, canvasHeight);
// Set the font and text styles.
offCtx.font = `${fontSize}px Consolas, Monaco, "Liberation Mono", monospace`;
offCtx.textBaseline = 'top';
offCtx.fillStyle = document.body.classList.contains('light-mode') ? "#000" : "#eee";
// Draw each line of the ASCII art onto the canvas with the margin offset.
for (let i = 0; i < lines.length; i++) {
offCtx.fillText(lines[i], borderMargin, borderMargin + i * lineHeight);
}
// Convert the canvas content to a blob and trigger a download.
offCanvas.toBlob(function(blob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'ascii_art.png';
a.click();
});
}
// ---------------- Event Listeners ----------------
// File upload event.
document.getElementById('upload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
currentImage = img;
generateASCII(img);
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
// Update controls when changed.
document.getElementById('asciiWidth').addEventListener('input', updateSettings);
document.getElementById('brightness').addEventListener('input', updateSettings);
document.getElementById('contrast').addEventListener('input', updateSettings);
document.getElementById('blur').addEventListener('input', updateSettings);
document.getElementById('dithering').addEventListener('change', updateSettings);
document.getElementById('ditherAlgorithm').addEventListener('change', updateSettings);
document.getElementById('invert').addEventListener('change', updateSettings);
document.getElementById('ignoreWhite').addEventListener('change', updateSettings);
document.getElementById('theme').addEventListener('change', updateSettings);
document.getElementById('charset').addEventListener('change', function() {
const manualControl = document.getElementById('manualCharControl');
manualControl.style.display = (this.value === 'manual') ? 'flex' : 'none';
updateSettings();
});
document.getElementById('zoom').addEventListener('input', updateSettings);
// Edge detection radio buttons.
document.querySelectorAll('input[name="edgeMethod"]').forEach(function(radio) {
radio.addEventListener('change', function() {
const method = document.querySelector('input[name="edgeMethod"]:checked').value;
document.getElementById('sobelThresholdControl').style.display = (method === 'sobel') ? 'flex' : 'none';
document.getElementById('dogThresholdControl').style.display = (method === 'dog') ? 'flex' : 'none';
// Basic adjustments remain enabled in all modes.
updateSettings();
});
});
document.getElementById('edgeThreshold').addEventListener('input', updateSettings);
document.getElementById('dogEdgeThreshold').addEventListener('input', updateSettings);
// Reset and Copy buttons.
document.getElementById('reset').addEventListener('click', resetSettings);
document.getElementById('copyBtn').addEventListener('click', function() {
const asciiText = document.getElementById('ascii-art').textContent;
navigator.clipboard.writeText(asciiText).then(() => {
alert('ASCII Art copied to clipboard!');
}, () => {
alert('Copy failed!');
});
});
document.getElementById('downloadBtn').addEventListener('click', downloadPNG);
// ---------------- Update and Reset Functions ----------------
function updateSettings() {
document.getElementById('asciiWidthVal').textContent = document.getElementById('asciiWidth').value;
document.getElementById('brightnessVal').textContent = document.getElementById('brightness').value;
document.getElementById('contrastVal').textContent = document.getElementById('contrast').value;
document.getElementById('blurVal').textContent = document.getElementById('blur').value;
document.getElementById('zoomVal').textContent = document.getElementById('zoom').value;
document.getElementById('edgeThresholdVal').textContent = document.getElementById('edgeThreshold').value;
document.getElementById('dogEdgeThresholdVal').textContent = document.getElementById('dogEdgeThreshold').value;
// Update theme.
const theme = document.getElementById('theme').value;
document.body.classList.toggle('light-mode', theme === 'light');
// Adjust ASCII art font size based on zoom.
const zoomPercent = parseInt(document.getElementById('zoom').value, 10);
const newFontSize = (baseFontSize * zoomPercent) / 100;
const asciiArt = document.getElementById('ascii-art');
asciiArt.style.fontSize = newFontSize + "px";
asciiArt.style.lineHeight = newFontSize + "px";
if (currentImage) {
generateASCII(currentImage);
}
}
function resetSettings() {
document.getElementById('asciiWidth').value = 100;
document.getElementById('brightness').value = 0;
document.getElementById('contrast').value = 0;
document.getElementById('blur').value = 0;
document.getElementById('dithering').checked = true;
document.getElementById('ditherAlgorithm').value = 'floyd';
document.getElementById('invert').checked = false;
document.getElementById('ignoreWhite').checked = true;
document.getElementById('charset').value = 'detailed';
document.getElementById('zoom').value = 100;
document.getElementById('edgeNone').checked = true;
document.getElementById('edgeThreshold').value = 100;
document.getElementById('dogEdgeThreshold').value = 100;
document.getElementById('sobelThresholdControl').style.display = 'none';
document.getElementById('dogThresholdControl').style.display = 'none';
// Re-enable basic adjustments.
document.getElementById('brightness').disabled = false;
document.getElementById('contrast').disabled = false;
document.getElementById('blur').disabled = false;
document.getElementById('invert').disabled = false;
updateSettings();
}
window.addEventListener('load', function() {
const defaultImg = new Image();
defaultImg.crossOrigin = "Anonymous";
defaultImg.src = "https://i.ibb.co/chHSSFQk/horse.png";
defaultImg.onload = function() {
currentImage = defaultImg;
generateASCII(defaultImg);
};
});
body {
margin: 0;
font-family: system-ui, sans-serif;
background-color: #000; /* Dark theme by default */
color: #eee;
}
.layout {
display: flex;
height: 100vh;
}
/* Increase sidebar width to fit labels and numeric values */
.sidebar {
width: 380px;
flex-shrink: 0;
background: #111; /* Matches dark theme */
color: #eee;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
position: fixed;
top: 0;
bottom: 0;
left: 0;
}
.sidebar h3 {
margin: 10px 0;
font-size: 1.1rem;
}
.control-group {
margin-bottom: 20px;
border-bottom: 1px solid #666;
padding-bottom: 10px;
}
.control-group:last-of-type {
border-bottom: none;
}
.control {
display: flex;
align-items: center;
margin: 8px 0;
}
.control label {
flex: 0 0 150px;
margin-right: 10px;
font-size: 0.9rem;
white-space: nowrap;
}
.control input[type="range"],
.control select,
.control input[type="text"] {
flex: 1;
}
.value-label {
display: inline-block;
width: 40px;
text-align: center;
margin-left: 8px;
font-size: 0.9rem;
}
.main-content {
flex: 1;
margin-left: 380px;
padding: 20px;
overflow-y: auto;
background: #000;
color: #eee;
box-sizing: border-box;
}
#ascii-art {
background: #000;
padding: 10px;
white-space: pre;
font-family: Consolas, Monaco, "Liberation Mono", monospace;
font-size: 7px;
line-height: 7px;
}
button {
margin-right: 10px;
padding: 5px 10px;
}
body.light-mode {
background-color: #fff;
color: #000;
}
body.light-mode .sidebar {
background: #F4F4F4;
color: #000;
}
body.light-mode .main-content {
background: #fff;
color: #000;
}
body.light-mode #ascii-art {
background: #fff;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment