<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>字符图片转换器</title>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background-color: #000;
color: #eee;
}
@media (max-width: 768px) {
.layout {
flex-direction: column;
}
.sidebar {
width: 100%;
}
}
.layout {
display: flex;
height: 100vh;
}
.sidebar {
width: 380px;
flex-shrink: 0;
background: #111;
color: #eee;
padding: 20px;
box-sizing: border-box;
}
.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;
padding: 20px;
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;
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<section class="control-group global-settings">
<h3>全局设置</h3>
<div class="control">
<label for="theme">主题:</label>
<select id="theme">
<option value="dark" selected>暗色</option>
<option value="light">亮色</option>
</select>
</div>
<div class="control">
<label for="ignoreWhite">忽略纯白色:</label>
<input type="checkbox" id="ignoreWhite" checked />
</div>
</section>
<section class="control-group upload-group">
<h3>1. 上传文件</h3>
<div class="control">
<input type="file" id="upload" accept="image/*" />
</div>
</section>
<section class="control-group image-processing">
<h3>2. 基本调整</h3>
<div class="control">
<label for="asciiWidth">输出宽度 (字符):</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">亮度:</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">对比度:</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">模糊 (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">反转颜色:</label>
<input type="checkbox" id="invert" />
</div>
</section>
<section class="control-group dithering-settings">
<h3>3. 抖动选项</h3>
<div class="control">
<label for="dithering">启用抖动:</label>
<input type="checkbox" id="dithering" checked />
</div>
<div class="control">
<label for="ditherAlgorithm">抖动算法:</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. 字符集</h3>
<div class="control">
<label for="charset">选择字符集:</label>
<select id="charset">
<option value="detailed" selected>详细</option>
<option value="standard">标准</option>
<option value="blocks">块</option>
<option value="binary">二进制</option>
<option value="hex">十六进制</option>
<option value="manual">手动</option>
</select>
</div>
<div class="control" id="manualCharControl" style="display: none">
<label for="manualCharInput">手动字符:</label>
<input type="text" id="manualCharInput" maxlength="1" value="0" />
</div>
</section>
<section class="control-group edge-detection-settings">
<h3>5. 边缘检测</h3>
<p>选择一种边缘检测方法:</p>
<div class="control">
<input
type="radio"
name="edgeMethod"
id="edgeNone"
value="none"
checked
/>
<label for="edgeNone">无边缘检测</label>
</div>
<div class="control">
<input
type="radio"
name="edgeMethod"
id="edgeSobel"
value="sobel"
/>
<label for="edgeSobel">Sobel 边缘检测</label>
</div>
<div class="control">
<input type="radio" name="edgeMethod" id="edgeDoG" value="dog" />
<label for="edgeDoG">DoG (轮廓) 检测</label>
</div>
<div class="control" id="sobelThresholdControl" style="display: none">
<label for="edgeThreshold">Sobel 阈值:</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 阈值:</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. 显示设置</h3>
<div class="control">
<label for="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">重置所有设置</button>
</div>
</section>
</aside>
<main class="main-content">
<pre id="ascii-art"></pre>
<button id="copyBtn">复制 ASCII 艺术</button>
<button id="downloadBtn">下载 PNG</button>
</main>
</div>
<canvas id="canvas" style="display: none"></canvas>
<script>
let currentImage = null;
const baseFontSize = 7;
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
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);
}
for (let y = 0; y < kernelSize; y++) {
for (let x = 0; x < kernelSize; x++) {
kernel[y][x] /= sum;
}
}
return 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;
}
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;
}
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 };
}
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)
) {
neighbor1 = mag[y][x - 1];
neighbor2 = mag[y][x + 1];
} else if (theta >= 22.5 && theta < 67.5) {
neighbor1 = mag[y - 1][x + 1];
neighbor2 = mag[y + 1][x - 1];
} else if (theta >= 67.5 && theta < 112.5) {
neighbor1 = mag[y - 1][x];
neighbor2 = mag[y + 1][x];
} else if (theta >= 112.5 && theta < 157.5) {
neighbor1 = mag[y - 1][x - 1];
neighbor2 = mag[y + 1][x + 1];
}
suppressed[y][x] =
currentMag >= neighbor1 && currentMag >= neighbor2
? currentMag
: 0;
}
}
return suppressed;
}
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") {
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") {
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") {
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") {
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 {
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;
}
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;
}
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;
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;
}
function downloadPNG() {
const preElement = document.getElementById("ascii-art");
const asciiText = preElement.textContent;
if (!asciiText.trim()) {
alert("No ASCII art to download.");
return;
}
const lines = asciiText.split("\n");
const scaleFactor = 2;
const borderMargin = 20 * scaleFactor;
const computedStyle = window.getComputedStyle(preElement);
const baseFontSize = parseInt(computedStyle.fontSize, 10);
const fontSize = baseFontSize * scaleFactor;
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
tempCtx.font = `${fontSize}px Consolas, Monaco, "Liberation Mono", monospace`;
let maxLineWidth = 0;
for (let i = 0; i < lines.length; i++) {
const lineWidth = tempCtx.measureText(lines[i]).width;
if (lineWidth > maxLineWidth) {
maxLineWidth = lineWidth;
}
}
const lineHeight = fontSize;
const textWidth = Math.ceil(maxLineWidth);
const textHeight = Math.ceil(lines.length * lineHeight);
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");
const bgColor = document.body.classList.contains("light-mode")
? "#fff"
: "#000";
offCtx.fillStyle = bgColor;
offCtx.fillRect(0, 0, canvasWidth, canvasHeight);
offCtx.font = `${fontSize}px Consolas, Monaco, "Liberation Mono", monospace`;
offCtx.textBaseline = "top";
offCtx.fillStyle = document.body.classList.contains("light-mode")
? "#000"
: "#eee";
for (let i = 0; i < lines.length; i++) {
offCtx.fillText(
lines[i],
borderMargin,
borderMargin + i * lineHeight
);
}
offCanvas.toBlob(function (blob) {
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "ascii_art.png";
a.click();
});
}
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);
});
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);
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";
updateSettings();
});
});
document
.getElementById("edgeThreshold")
.addEventListener("input", updateSettings);
document
.getElementById("dogEdgeThreshold")
.addEventListener("input", updateSettings);
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);
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;
const theme = document.getElementById("theme").value;
document.body.classList.toggle("light-mode", theme === "light");
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";
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);
};
});
</script>
</body>
</html>