字符图片转换器

Published on
/
/趣玩前端
<!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>