Spaces:
Running
Running
| <html><head><base href="https://www.example.com/advanced-image-preprocessing-ascii-converter/"> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Image Preprocessing and ASCII Art Converter</title> | |
| <style> | |
| :root { | |
| --fluid-5-20: clamp(0.3125rem, 0.2731rem + 1.2605vw, 1.25rem); | |
| --fluid-8-20: clamp(0.5rem, 0.4685rem + 1.0084vw, 1.25rem); | |
| --fluid-5-9: clamp(0.3125rem, 0.302rem + 0.3361vw, 0.5625rem); | |
| --fluid-2-5: clamp(0.125rem, 0.1168rem + 0.2609vw, 0.3125rem); | |
| } | |
| @font-face { | |
| font-family: 'jgs9'; | |
| src: url('https://websim.ai/fonts/jgs9.woff2') format('woff2'); | |
| font-weight: normal; | |
| font-style: normal; | |
| } | |
| body { | |
| font-family: 'Roboto', Arial, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #040404; | |
| color: #fdfdfd; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background-color: #111; | |
| padding: 30px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px rgba(255,255,255,0.1); | |
| } | |
| h1, h2 { | |
| text-align: center; | |
| color: #fdfdfd; | |
| font-family: "jgs9", sans-serif; | |
| } | |
| .upload-section { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| #imageUpload { | |
| display: none; | |
| } | |
| .upload-btn { | |
| background-color: #3498db; | |
| color: white; | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background-color 0.3s ease; | |
| } | |
| .upload-btn:hover { | |
| background-color: #2980b9; | |
| } | |
| .image-container { | |
| display: flex; | |
| justify-content: space-between; | |
| flex-wrap: wrap; | |
| margin-bottom: 30px; | |
| } | |
| .image-preview { | |
| flex-basis: calc(33% - 20px); | |
| margin-bottom: 20px; | |
| background-color: #222; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 4px rgba(255,255,255,0.1); | |
| } | |
| .image-preview h3 { | |
| background-color: #34495e; | |
| color: #fff; | |
| margin: 0; | |
| padding: 10px; | |
| font-size: 18px; | |
| } | |
| canvas { | |
| max-width: 100%; | |
| height: auto; | |
| display: block; | |
| } | |
| .controls { | |
| background-color: #222; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| } | |
| .slider-container { | |
| margin-bottom: 15px; | |
| } | |
| .slider-container label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-weight: bold; | |
| color: #fdfdfd; | |
| } | |
| .slider { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 10px; | |
| border-radius: 5px; | |
| background: #555; | |
| outline: none; | |
| opacity: 0.7; | |
| transition: opacity .2s; | |
| } | |
| .slider:hover { | |
| opacity: 1; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3498db; | |
| cursor: pointer; | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3498db; | |
| cursor: pointer; | |
| } | |
| .number-input { | |
| width: 50px; | |
| margin-left: 10px; | |
| background-color: #333; | |
| color: #fdfdfd; | |
| border: 1px solid #555; | |
| border-radius: 3px; | |
| padding: 3px; | |
| } | |
| #processBtn { | |
| display: block; | |
| width: 100%; | |
| padding: 12px; | |
| background-color: #2ecc71; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: background-color 0.3s ease; | |
| } | |
| #processBtn:hover { | |
| background-color: #27ae60; | |
| } | |
| .ascii-art { | |
| font-family: "jgs9", monospace; | |
| white-space: pre; | |
| background-color: #222; | |
| padding: 20px; | |
| border-radius: 5px; | |
| overflow: auto; | |
| max-height: 600px; | |
| color: #fdfdfd; | |
| font-size: 2px; | |
| line-height: 1; | |
| text-align: center; | |
| box-shadow: 0 0 10px rgba(0,0,0,0.5); | |
| margin-top: 20px; | |
| } | |
| .download-buttons { | |
| display: flex; | |
| justify-content: center; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .download-btn { | |
| background-color: #4CAF50; | |
| border: none; | |
| color: white; | |
| padding: 10px 20px; | |
| text-align: center; | |
| text-decoration: none; | |
| display: inline-block; | |
| font-size: 16px; | |
| margin: 4px 2px; | |
| cursor: pointer; | |
| border-radius: 5px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loadingMessage" style="text-align: center; padding: 20px;"> | |
| Loading OpenCV.js, please wait... | |
| </div> | |
| <div class="container"> | |
| <h1>Advanced Image Preprocessing and ASCII Art Converter</h1> | |
| <div class="upload-section"> | |
| <input type="file" id="imageUpload" accept="image/*"> | |
| <label for="imageUpload" class="upload-btn">Upload Image</label> | |
| </div> | |
| <div class="image-container"> | |
| <div class="image-preview"> | |
| <h3>Original Image</h3> | |
| <canvas id="originalCanvas"></canvas> | |
| </div> | |
| <div class="image-preview"> | |
| <h3>Preprocessed Image</h3> | |
| <canvas id="preprocessedCanvas"></canvas> | |
| </div> | |
| <div class="image-preview"> | |
| <h3>Structure Map</h3> | |
| <canvas id="structureMapCanvas"></canvas> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <div class="slider-container"> | |
| <label for="edgeThresholdSlider">Edge Detection Threshold:</label> | |
| <input type="range" id="edgeThresholdSlider" class="slider" min="0" max="255" value="100"> | |
| <span id="edgeThresholdValue">100</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="gaussianBlurSlider">Gaussian Blur:</label> | |
| <input type="range" id="gaussianBlurSlider" class="slider" min="0" max="10" value="1" step="0.1"> | |
| <span id="gaussianBlurValue">1</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="thinningIterationsSlider">Thinning Iterations:</label> | |
| <input type="range" id="thinningIterationsSlider" class="slider" min="1" max="10" value="3"> | |
| <span id="thinningIterationsValue">3</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="scaleSlider">Scale (%):</label> | |
| <input type="range" id="scaleSlider" class="slider" min="10" max="200" value="100"> | |
| <span id="scaleValue">100</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="dogThresholdSlider">DoG Threshold:</label> | |
| <input type="range" id="dogThresholdSlider" class="slider" min="0" max="255" value="100"> | |
| <span id="dogThresholdValue">100</span> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="outputHeightSlider">Output Height (rows):</label> | |
| <input type="range" id="outputHeightSlider" class="slider" min="10" max="200" value="50"> | |
| <input type="number" id="outputHeightValue" class="number-input" min="10" max="200" value="50"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="scaleCountSlider">Number of Scales:</label> | |
| <input type="range" id="scaleCountSlider" class="slider" min="1" max="8" value="4"> | |
| <input type="number" id="scaleCountValue" class="number-input" min="1" max="8" value="4"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="ncrfRadiusSlider">Non-CRF Radius:</label> | |
| <input type="range" id="ncrfRadiusSlider" class="slider" min="1" max="20" value="5"> | |
| <input type="number" id="ncrfRadiusValue" class="number-input" min="1" max="20" value="5"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="modulationStrengthSlider">Modulation Strength:</label> | |
| <input type="range" id="modulationStrengthSlider" class="slider" min="0" max="1" step="0.1" value="0.5"> | |
| <input type="number" id="modulationStrengthValue" class="number-input" min="0" max="1" step="0.1" value="0.5"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="detailThresholdSlider">Detail Threshold:</label> | |
| <input type="range" id="detailThresholdSlider" class="slider" min="0" max="1" step="0.05" value="0.1"> | |
| <input type="number" id="detailThresholdValue" class="number-input" min="0" max="1" step="0.05" value="0.1"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="charSetSelect">Character Set:</label> | |
| <select id="charSetSelect"> | |
| <option value="standard">Standard</option> | |
| <option value="structure">Structure</option> | |
| <option value="outline">Outline</option> | |
| <option value="blocks">Blocks</option> | |
| <option value="geometric">Geometric</option> | |
| <option value="dots">Dots</option> | |
| </select> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="invertColorsCheckbox"> | |
| <input type="checkbox" id="invertColorsCheckbox"> Invert Colors | |
| </label> | |
| </div> | |
| <button id="processBtn">Process Image</button> | |
| </div> | |
| <div class="ascii-output"> | |
| <h2>ASCII Output</h2> | |
| <div id="asciiOutput" class="ascii-art"></div> | |
| </div> | |
| <div class="download-buttons"> | |
| <button id="downloadPreprocessed" class="download-btn">Download Preprocessed Image</button> | |
| <button id="downloadStructureMap" class="download-btn">Download Structure Map</button> | |
| <button id="downloadAscii" class="download-btn">Download ASCII Art</button> | |
| </div> | |
| </div> | |
| <script> | |
| let originalMat, preprocessedMat, structureMap; | |
| let originalCanvas, preprocessedCanvas, structureMapCanvas; | |
| function handleImageUpload(event) { | |
| const file = event.target.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| try { | |
| originalMat = cv.imread(img); | |
| cv.imshow('originalCanvas', originalMat); | |
| processImage(); | |
| } catch (err) { | |
| console.error('Error processing image:', err); | |
| alert('Error processing image. Please try again.'); | |
| } | |
| } | |
| img.src = e.target.result; | |
| } | |
| reader.readAsDataURL(file); | |
| } | |
| function openCvReady() { | |
| document.getElementById('loadingMessage').style.display = 'none'; | |
| document.getElementById('imageUpload').disabled = false; | |
| document.getElementById('processBtn').disabled = false; | |
| originalCanvas = document.getElementById('originalCanvas'); | |
| preprocessedCanvas = document.getElementById('preprocessedCanvas'); | |
| structureMapCanvas = document.getElementById('structureMapCanvas'); | |
| document.getElementById('imageUpload').addEventListener('change', handleImageUpload); | |
| document.getElementById('processBtn').addEventListener('click', processImage); | |
| document.getElementById('downloadPreprocessed').addEventListener('click', downloadPreprocessedImage); | |
| document.getElementById('downloadStructureMap').addEventListener('click', downloadStructureMap); | |
| document.getElementById('downloadAscii').addEventListener('click', downloadAsciiArt); | |
| } | |
| </script> | |
| <script async src="https://docs.opencv.org/4.5.1/opencv.js" onload="openCvReady();" type="text/javascript"></script> | |
| <script> | |
| function structureLineExtraction(src) { | |
| let dst = new cv.Mat(); | |
| cv.Canny(src, dst, 50, 150, 3, false); | |
| return dst; | |
| } | |
| function edgeDetection(src) { | |
| let dst = new cv.Mat(); | |
| let ksize = new cv.Size(3, 3); | |
| let sobel_x = new cv.Mat(); | |
| let sobel_y = new cv.Mat(); | |
| cv.Sobel(src, sobel_x, cv.CV_64F, 1, 0, ksize); | |
| cv.Sobel(src, sobel_y, cv.CV_64F, 0, 1, ksize); | |
| cv.magnitude(sobel_x, sobel_y, dst); | |
| cv.normalize(dst, dst, 0, 255, cv.NORM_MINMAX, cv.CV_8U); | |
| return dst; | |
| } | |
| function preThinning(src) { | |
| let dst = src.clone(); | |
| let rows = src.rows; | |
| let cols = src.cols; | |
| for (let i = 1; i < rows - 1; i++) { | |
| for (let j = 1; j < cols - 1; j++) { | |
| let p = src.ucharPtr(i, j)[0]; | |
| if (p === 0) continue; | |
| let p1 = src.ucharPtr(i-1, j)[0]; | |
| let p3 = src.ucharPtr(i, j+1)[0]; | |
| let p5 = src.ucharPtr(i+1, j)[0]; | |
| let p7 = src.ucharPtr(i, j-1)[0]; | |
| let B_odd = p1 + p3 + p5 + p7; | |
| if (B_odd < 2) { | |
| dst.ucharPtr(i, j)[0] = 0; | |
| } else if (B_odd > 2) { | |
| dst.ucharPtr(i, j)[0] = 255; | |
| } | |
| } | |
| } | |
| return dst; | |
| } | |
| function improvedThinning(src, iterations) { | |
| let dst = src.clone(); | |
| let changed; | |
| for (let i = 0; i < iterations; i++) { | |
| changed = false; | |
| dst = preThinning(dst); | |
| changed |= zhangSuenSubIteration(dst, 0); | |
| changed |= zhangSuenSubIteration(dst, 1); | |
| if (!changed) break; | |
| } | |
| return dst; | |
| } | |
| function processImage() { | |
| if (!originalMat) { | |
| console.error('No image loaded'); | |
| return; | |
| } | |
| try { | |
| const edgeThreshold = parseInt(document.getElementById('edgeThresholdSlider').value); | |
| const blurSize = parseFloat(document.getElementById('gaussianBlurSlider').value); | |
| const iterations = parseInt(document.getElementById('thinningIterationsSlider').value); | |
| const outputHeight = parseInt(document.getElementById('outputHeightSlider').value); | |
| const scaleCount = parseInt(document.getElementById('scaleCountSlider').value); | |
| const ncrfRadius = parseInt(document.getElementById('ncrfRadiusSlider').value); | |
| const modulationStrength = parseFloat(document.getElementById('modulationStrengthSlider').value); | |
| const detailThreshold = parseFloat(document.getElementById('detailThresholdSlider').value); | |
| let blurred = new cv.Mat(); | |
| let gray = new cv.Mat(); | |
| let edges = new cv.Mat(); | |
| cv.GaussianBlur(originalMat, blurred, new cv.Size(0, 0), blurSize, blurSize, cv.BORDER_DEFAULT); | |
| cv.cvtColor(blurred, gray, cv.COLOR_RGBA2GRAY); | |
| cv.Canny(gray, edges, edgeThreshold, edgeThreshold * 2, 3, false); | |
| preprocessedMat = improvedThinning(edges, iterations); | |
| cv.imshow('preprocessedCanvas', preprocessedMat); | |
| structureMap = extractStructure(gray, scaleCount, ncrfRadius, modulationStrength); | |
| displayStructureMap(structureMap); | |
| convertToASCII(structureMap, detailThreshold); | |
| blurred.delete(); | |
| gray.delete(); | |
| edges.delete(); | |
| } catch (err) { | |
| console.error('Error processing image:', err); | |
| alert('Error processing image. Please try again.'); | |
| } | |
| } | |
| function zhangSuenThinning(src, iterations) { | |
| let dst = src.clone(); | |
| let changed; | |
| for (let i = 0; i < iterations; i++) { | |
| changed = false; | |
| changed |= zhangSuenSubIteration(dst, 0); | |
| changed |= zhangSuenSubIteration(dst, 1); | |
| if (!changed) break; | |
| } | |
| return dst; | |
| } | |
| function zhangSuenSubIteration(img, step) { | |
| let changed = false; | |
| let rows = img.rows; | |
| let cols = img.cols; | |
| let markers = new cv.Mat(rows, cols, cv.CV_8U, new cv.Scalar(0)); | |
| for (let i = 1; i < rows - 1; i++) { | |
| for (let j = 1; j < cols - 1; j++) { | |
| if (img.ucharPtr(i, j)[0] === 0) continue; | |
| let p2 = img.ucharPtr(i-1, j)[0]; | |
| let p3 = img.ucharPtr(i-1, j+1)[0]; | |
| let p4 = img.ucharPtr(i, j+1)[0]; | |
| let p5 = img.ucharPtr(i+1, j+1)[0]; | |
| let p6 = img.ucharPtr(i+1, j)[0]; | |
| let p7 = img.ucharPtr(i+1, j-1)[0]; | |
| let p8 = img.ucharPtr(i, j-1)[0]; | |
| let p9 = img.ucharPtr(i-1, j-1)[0]; | |
| let A = ((p2 === 0 && p3 === 255) + (p3 === 0 && p4 === 255) + | |
| (p4 === 0 && p5 === 255) + (p5 === 0 && p6 === 255) + | |
| (p6 === 0 && p7 === 255) + (p7 === 0 && p8 === 255) + | |
| (p8 === 0 && p9 === 255) + (p9 === 0 && p2 === 255)); | |
| let B = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9; | |
| let m1 = step === 0 ? (p2 * p4 * p6) : (p2 * p4 * p8); | |
| let m2 = step === 0 ? (p4 * p6 * p8) : (p2 * p6 * p8); | |
| if (A === 1 && (B >= 2 * 255 && B <= 6 * 255) && m1 === 0 && m2 === 0) { | |
| markers.ucharPtr(i, j)[0] = 255; | |
| changed = true; | |
| } | |
| } | |
| } | |
| for (let i = 0; i < rows; i++) { | |
| for (let j = 0; j < cols; j++) { | |
| if (markers.ucharPtr(i, j)[0] === 255) { | |
| img.ucharPtr(i, j)[0] = 0; | |
| } | |
| } | |
| } | |
| markers.delete(); | |
| return changed; | |
| } | |
| function extractStructure(grayMat, scaleCount, ncrfRadius, modulationStrength) { | |
| const width = grayMat.cols; | |
| const height = grayMat.rows; | |
| const data = grayMat.data; | |
| const scales = []; | |
| for (let i = 0; i < scaleCount; i++) { | |
| scales.push(Math.pow(2, i)); | |
| } | |
| let structureMap = new Array(width * height).fill(0); | |
| for (let scale of scales) { | |
| const response = simulateCRFResponse(data, width, height, scale); | |
| for (let i = 0; i < structureMap.length; i++) { | |
| structureMap[i] = Math.max(structureMap[i], response[i]); | |
| } | |
| } | |
| structureMap = applyNonCRFModulation(structureMap, width, height, ncrfRadius, modulationStrength); | |
| return structureMap; | |
| } | |
| function simulateCRFResponse(data, width, height, scale) { | |
| const response = new Array(width * height).fill(0); | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const i = y * width + x; | |
| const grayValue = data[i]; | |
| if (x > scale && x < width - scale && y > scale && y < height - scale) { | |
| const diff = Math.abs(grayValue - data[y * width + x - scale]) + | |
| Math.abs(grayValue - data[y * width + x + scale]) + | |
| Math.abs(grayValue - data[(y - scale) * width + x]) + | |
| Math.abs(grayValue - data[(y + scale) * width + x]); | |
| response[i] = diff / (4 * 255); | |
| } | |
| } | |
| } | |
| return response; | |
| } | |
| function applyNonCRFModulation(structureMap, width, height, radius, strength) { | |
| const modulated = new Array(width * height).fill(0); | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const i = y * width + x; | |
| let sum = 0; | |
| let count = 0; | |
| for (let dy = -radius; dy <= radius; dy++) { | |
| for (let dx = -radius; dx <= radius; dx++) { | |
| const nx = x + dx; | |
| const ny = y + dy; | |
| if (nx >= 0 && nx < width && ny >= 0 && ny < height) { | |
| sum += structureMap[ny * width + nx]; | |
| count++; | |
| } | |
| } | |
| } | |
| const avgResponse = sum / count; | |
| modulated[i] = structureMap[i] * (1 - strength * avgResponse); | |
| } | |
| } | |
| return modulated; | |
| } | |
| function displayStructureMap(structureMap) { | |
| const width = preprocessedCanvas.width; | |
| const height = preprocessedCanvas.height; | |
| const ctx = structureMapCanvas.getContext('2d'); | |
| const imageData = ctx.createImageData(width, height); | |
| for (let i = 0; i < structureMap.length; i++) { | |
| const value = Math.floor(structureMap[i] * 255); | |
| imageData.data[i * 4] = value; | |
| imageData.data[i * 4 + 1] = value; | |
| imageData.data[i * 4 + 2] = value; | |
| imageData.data[i * 4 + 3] = 255; | |
| } | |
| structureMapCanvas.width = width; | |
| structureMapCanvas.height = height; | |
| ctx.putImageData(imageData, 0, 0); | |
| } | |
| function convertToASCII(structureMap, detailThreshold) { | |
| const width = preprocessedCanvas.width; | |
| const height = preprocessedCanvas.height; | |
| const outputHeight = parseInt(document.getElementById('outputHeightSlider').value); | |
| const outputWidth = Math.floor(outputHeight * width / height); | |
| const charSetType = document.getElementById('charSetSelect').value; | |
| const invert = document.getElementById('invertColorsCheckbox').checked; | |
| const charSets = { | |
| standard: '@%#*+=-:. ', | |
| structure: '.,/\\_|~\'*+^][}{ ', | |
| outline: 'βββ ', | |
| blocks: 'ββββ ', | |
| geometric: 'β β‘β’β£β€β₯β¦β§β¨β© ', | |
| dots: 'β’Β·ββββββ ' | |
| }; | |
| const characters = charSets[charSetType]; | |
| let asciiArt = ''; | |
| for (let y = 0; y < outputHeight; y++) { | |
| for (let x = 0; x < outputWidth; x++) { | |
| const srcX = Math.floor(x * width / outputWidth); | |
| const srcY = Math.floor(y * height / outputHeight); | |
| const value = structureMap[srcY * width + srcX]; | |
| let charIndex = Math.floor(value * (characters.length - 1)); | |
| if (invert) { | |
| charIndex = characters.length - 1 - charIndex; | |
| } | |
| asciiArt += value > detailThreshold ? characters[charIndex] : ' '; | |
| } | |
| asciiArt += '\n'; | |
| } | |
| document.getElementById('asciiOutput').textContent = asciiArt; | |
| } | |
| function downloadPreprocessedImage() { | |
| if (!preprocessedCanvas.toDataURL) { | |
| alert('No preprocessed image available.'); | |
| return; | |
| } | |
| const link = document.createElement('a'); | |
| link.download = 'preprocessed_image.png'; | |
| link.href = preprocessedCanvas.toDataURL(); | |
| link.click(); | |
| } | |
| function downloadStructureMap() { | |
| if (!structureMapCanvas.toDataURL) { | |
| alert('No structure map available.'); | |
| return; | |
| } | |
| const link = document.createElement('a'); | |
| link.download = 'structure_map.png'; | |
| link.href = structureMapCanvas.toDataURL(); | |
| link.click(); | |
| } | |
| function downloadAsciiArt() { | |
| const asciiArt = document.getElementById('asciiOutput').textContent; | |
| if (!asciiArt.trim()) { | |
| alert('No ASCII art available.'); | |
| return; | |
| } | |
| const blob = new Blob([asciiArt], {type: 'text/plain'}); | |
| const link = document.createElement('a'); | |
| link.download = 'ascii_art.txt'; | |
| link.href = URL.createObjectURL(blob); | |
| link.click(); | |
| } | |
| document.getElementById('edgeThresholdSlider').addEventListener('input', function(e) { | |
| document.getElementById('edgeThresholdValue').textContent = e.target.value; | |
| }); | |
| document.getElementById('gaussianBlurSlider').addEventListener('input', function(e) { | |
| document.getElementById('gaussianBlurValue').textContent = e.target.value; | |
| }); | |
| document.getElementById('thinningIterationsSlider').addEventListener('input', function(e) { | |
| document.getElementById('thinningIterationsValue').textContent = e.target.value; | |
| }); | |
| document.getElementById('scaleSlider').addEventListener('input', function(e) { | |
| document.getElementById('scaleValue').textContent = e.target.value; | |
| }); | |
| document.getElementById('dogThresholdSlider').addEventListener('input', function(e) { | |
| document.getElementById('dogThresholdValue').textContent = e.target.value; | |
| }); | |
| function syncInputs(slider, value) { | |
| slider.addEventListener('input', () => value.value = slider.value); | |
| value.addEventListener('input', () => slider.value = value.value); | |
| } | |
| syncInputs(document.getElementById('outputHeightSlider'), document.getElementById('outputHeightValue')); | |
| syncInputs(document.getElementById('scaleCountSlider'), document.getElementById('scaleCountValue')); | |
| syncInputs(document.getElementById('ncrfRadiusSlider'), document.getElementById('ncrfRadiusValue')); | |
| syncInputs(document.getElementById('modulationStrengthSlider'), document.getElementById('modulationStrengthValue')); | |
| syncInputs(document.getElementById('detailThresholdSlider'), document.getElementById('detailThresholdValue')); | |
| const sliders = document.querySelectorAll('.slider'); | |
| sliders.forEach(slider => { | |
| slider.addEventListener('input', processImage); | |
| }); | |
| document.getElementById('imageUpload').disabled = true; | |
| document.getElementById('processBtn').disabled = true; | |
| </script> | |
| </body> | |
| </html> |