smoothretro1982

Ascii font paster (w.i.p) 0.2

Apr 15th, 2026 (edited)
58
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 28.91 KB | None | 0 0
  1. (() => {
  2. if (window.__asciiHUD) return;
  3. window.__asciiHUD = true;
  4.  
  5. let asciiChars = "█▉▊▋▌▍▎▏ ";
  6. let inverted = false, flipH = false, flipV = false, rotation = 0;
  7. let originalImage = null, asciiWidth = 80, asciiHeight = 80, contrast = 1;
  8. let latestAsciiText = "";
  9. let selectedFont = "monospace";
  10. let rawText = "";
  11. let colorMode = "none"; // "none" | "html" | "ansi" | "blocks"
  12.  
  13. const hud = document.createElement("div");
  14. hud.style.position = "fixed";
  15. hud.style.top = "50px";
  16. hud.style.left = "50px";
  17. hud.style.width = "640px";
  18. hud.style.height = "720px";
  19. hud.style.maxHeight = "900px";
  20. hud.style.background = "rgba(0,0,0,0.88)";
  21. hud.style.border = "2px solid #888";
  22. hud.style.borderRadius = "8px";
  23. hud.style.overflow = "hidden";
  24. hud.style.zIndex = 99999;
  25. hud.style.fontFamily = "sans-serif";
  26.  
  27. const header = document.createElement("div");
  28. header.innerText = "ASCII HUD (Color Modes ✓)";
  29. header.style.cursor = "move";
  30. header.style.padding = "6px 8px";
  31. header.style.background = "rgba(0,0,0,0.95)";
  32. header.style.color = "#ccc";
  33. header.style.fontWeight = "700";
  34. header.style.userSelect = "none";
  35. header.style.display = "flex";
  36. header.style.justifyContent = "space-between";
  37. header.style.alignItems = "center";
  38. hud.appendChild(header);
  39.  
  40. const headerLeft = document.createElement("div");
  41. headerLeft.innerText = "ASCII HUD";
  42. headerLeft.style.color = "#ddd";
  43. headerLeft.style.fontWeight = "700";
  44. headerLeft.style.fontSize = "13px";
  45. header.appendChild(headerLeft);
  46.  
  47. const closeBtn = document.createElement("button");
  48. closeBtn.innerText = "✖";
  49. closeBtn.style.background = "transparent";
  50. closeBtn.style.border = "none";
  51. closeBtn.style.color = "#eee";
  52. closeBtn.style.fontSize = "16px";
  53. closeBtn.style.cursor = "pointer";
  54. closeBtn.style.marginLeft = "8px";
  55. closeBtn.onclick = () => { hud.remove(); window.__asciiHUD = false; };
  56. header.appendChild(closeBtn);
  57.  
  58. const container = document.createElement("div");
  59. container.style.padding = "10px";
  60. container.style.overflow = "auto";
  61. container.style.height = "calc(100% - 46px)";
  62. hud.appendChild(container);
  63.  
  64. // UI markup (including new mode selector and ANSI/PNG export)
  65. container.innerHTML = `
  66. <div style="display:flex;gap:8px;align-items:center;">
  67. <input type="file" id="asciiImageInput" title="Upload image">
  68. <button id="fromClipboardBtn" title="Paste image from clipboard">From Clipboard</button>
  69. <button id="webcamBtn" title="Toggle webcam">Webcam</button>
  70. <button id="exportPngBtn" title="Export colored ASCII as PNG">Export PNG</button>
  71. </div>
  72.  
  73. <div style="margin-top:8px;display:flex;gap:8px;align-items:center;">
  74. <label style="color:white;">Upload Font:</label>
  75. <input type="file" id="fontUpload" multiple>
  76. <label style="color:white;">Select Font:</label>
  77. <select id="fontSelect" style="flex:1;background:#222;color:#eee;border:none;padding:4px;">
  78. <option value="monospace">monospace</option>
  79. <option value="Arial">Arial</option>
  80. <option value="Courier New">Courier New</option>
  81. <option value="Times New Roman">Times New Roman</option>
  82. <option value="Verdana">Verdana</option>
  83. </select>
  84. </div>
  85.  
  86. <div style="margin-top:8px;">
  87. <label for="textInput" style="color:white;">Text Input (for text->ASCII):</label><br>
  88. <textarea id="textInput" style="width:100%;height:70px;background:#222;color:#eee;border:none;resize:none;padding:6px;"></textarea>
  89. <button id="useTextBtn" style="margin-top:4px;">Use Text</button>
  90. </div>
  91.  
  92. <div style="margin-top:8px;display:flex;gap:8px;align-items:center;">
  93. <label style="color:white;">Chars:</label>
  94. <select id="charSetSelect" style="background:#222;color:#eee;border:none;padding:4px;">
  95. <option value="█▉▊▋▌▍▎▏ ">Blocks</option>
  96. <option value=" @%#*+=-:.">Standard</option>
  97. <option value=" ░▒▓█">Shades</option>
  98. <option value=" .:-=+*#%@">Detailed</option>
  99. <option value=" 01">Binary</option>
  100. <option value="custom">Custom...</option>
  101. </select>
  102. <input type="text" id="charSetInput" value="${asciiChars}" style="width:80px;background:#222;color:#eee;border:none;padding:4px;display:none;" title="Enter custom characters">
  103. <label style="color:white;">Mode:</label>
  104. <select id="colorModeSelect" style="background:#222;color:#eee;border:none;padding:4px;">
  105. <option value="none">Plain ASCII</option>
  106. <option value="html">HTML-colored</option>
  107. <option value="ansi">ANSI (export)</option>
  108. <option value="blocks">Block mode</option>
  109. </select>
  110. <button id="copyAnsiBtn" title="Copy ANSI to clipboard">Copy ANSI</button>
  111. </div>
  112.  
  113. <div style="margin-top:8px;">
  114. <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
  115. <button id="selectAllBtn">Select All</button>
  116. <button id="copyBtn">Copy Text</button>
  117. <button id="downloadBtn">Download TXT</button>
  118. <button id="invertBtn">Invert</button>
  119. <button id="flipHBtn">Flip H</button>
  120. <button id="flipVBtn">Flip V</button>
  121. </div>
  122. </div>
  123.  
  124. <div style="margin-top:8px;display:flex;gap:8px;align-items:center;">
  125. <label style="color:white;">Rotation (°):</label>
  126. <input type="range" id="rotationSlider" min="0" max="360" value="${rotation}">
  127. <span id="rotationValue">${rotation}</span>
  128. </div>
  129.  
  130. <div style="margin-top:6px;display:flex;gap:8px;align-items:center;">
  131. <label style="color:white;">Width:</label>
  132. <input type="range" id="widthSlider" min="-200" max="200" value="${asciiWidth}">
  133. <span id="widthValue">${asciiWidth}</span>
  134.  
  135. <label style="color:white;">Height:</label>
  136. <input type="range" id="heightSlider" min="-200" max="200" value="${asciiHeight}">
  137. <span id="heightValue">${asciiHeight}</span>
  138. </div>
  139.  
  140. <div style="margin-top:6px;display:flex;gap:8px;align-items:center;">
  141. <label style="color:white;">Size:</label>
  142. <input type="range" id="sizeSlider" min="-100" max="100" value="80">
  143. <span id="sizeValue">80</span>
  144.  
  145. <label style="color:white;">Zoom:</label>
  146. <input type="range" id="zoomSlider" min="0.1" max="5" step="0.1" value="1">
  147. <span id="zoomValue">1x</span>
  148. </div>
  149.  
  150. <div style="margin-top:6px;display:flex;gap:8px;align-items:center;">
  151. <label style="color:white;">Contrast:</label>
  152. <input type="range" id="contrastSlider" min="0.1" max="3" step="0.1" value="${contrast}">
  153. <span id="contrastValue">${contrast}</span>
  154. </div>
  155.  
  156. <div id="webcamPreviewContainer" style="margin-top:8px;">
  157. <video id="webcamPreview" autoplay playsinline style="width:100%; display:none; border:1px solid #555;"></video>
  158. </div>
  159.  
  160. <div style="margin-top:8px;">
  161. <label style="color:white;">Plain ASCII Output (copyable):</label>
  162. <textarea id="asciiOutput" readonly style="width:100%; height:260px; background:transparent; color:#eee; border:none; resize:none; font-family:monospace; font-size:8px; line-height:6px; padding:6px;"></textarea>
  163. <pre id="asciiHtmlOutput" style="display:none; width:100%; height:260px; overflow:auto; background:transparent; color:#eee; border:none; padding:6px; margin:0; box-sizing:border-box; font-family:monospace;"></pre>
  164. </div>
  165. `;
  166.  
  167. document.body.appendChild(hud);
  168.  
  169. // DOM refs
  170. const charSetSelect = container.querySelector("#charSetSelect");
  171. const fontUpload = container.querySelector("#fontUpload");
  172. const fontSelect = container.querySelector("#fontSelect");
  173. const imageInput = container.querySelector("#asciiImageInput");
  174. const asciiOutput = container.querySelector("#asciiOutput");
  175. const asciiHtmlOutput = container.querySelector("#asciiHtmlOutput");
  176. const textInput = container.querySelector("#textInput");
  177. const useTextBtn = container.querySelector("#useTextBtn");
  178. const fromClipboardBtn = container.querySelector("#fromClipboardBtn");
  179. const downloadBtn = container.querySelector("#downloadBtn");
  180. const charSetInput = container.querySelector("#charSetInput");
  181. const webcamBtn = container.querySelector("#webcamBtn");
  182. const webcamPreview = container.querySelector("#webcamPreview");
  183. const colorModeSelect = container.querySelector("#colorModeSelect");
  184. const copyAnsiBtn = container.querySelector("#copyAnsiBtn");
  185. const exportPngBtn = container.querySelector("#exportPngBtn");
  186.  
  187. const selectAllBtn = container.querySelector("#selectAllBtn");
  188. const copyBtn = container.querySelector("#copyBtn");
  189. const invertBtn = container.querySelector("#invertBtn");
  190. const flipHBtn = container.querySelector("#flipHBtn");
  191. const flipVBtn = container.querySelector("#flipVBtn");
  192.  
  193. const widthSlider = container.querySelector("#widthSlider");
  194. const widthValue = container.querySelector("#widthValue");
  195. const heightSlider = container.querySelector("#heightSlider");
  196. const heightValue = container.querySelector("#heightValue");
  197. const sizeSlider = container.querySelector("#sizeSlider");
  198. const sizeValue = container.querySelector("#sizeValue");
  199. const zoomSlider = container.querySelector("#zoomSlider");
  200. const zoomValue = container.querySelector("#zoomValue");
  201. const contrastSlider = container.querySelector("#contrastSlider");
  202. const contrastValue = container.querySelector("#contrastValue");
  203. const rotationSlider = container.querySelector("#rotationSlider");
  204. const rotationValue = container.querySelector("#rotationValue");
  205.  
  206. // IndexedDB helpers (unchanged)
  207. function openFontDB() {
  208. return new Promise((resolve, reject) => {
  209. const request = indexedDB.open("ASCIIHUD_Fonts", 1);
  210. request.onupgradeneeded = e => {
  211. const db = e.target.result;
  212. if (!db.objectStoreNames.contains("fonts")) db.createObjectStore("fonts", { keyPath: "name" });
  213. };
  214. request.onsuccess = e => resolve(e.target.result);
  215. request.onerror = e => reject(e);
  216. });
  217. }
  218. async function saveFontToDB(name, dataURL) {
  219. const db = await openFontDB();
  220. const tx = db.transaction("fonts", "readwrite");
  221. const store = tx.objectStore("fonts");
  222. store.put({ name, data: dataURL });
  223. return tx.complete;
  224. }
  225. async function loadFontsFromDB() {
  226. const db = await openFontDB();
  227. return new Promise((resolve, reject) => {
  228. const tx = db.transaction("fonts", "readonly");
  229. const store = tx.objectStore("fonts");
  230. const request = store.getAll();
  231. request.onsuccess = e => resolve(e.target.result);
  232. request.onerror = e => reject(e);
  233. });
  234. }
  235.  
  236. // Load stored fonts
  237. (async () => {
  238. const storedFonts = await loadFontsFromDB();
  239. storedFonts.forEach(f => {
  240. const fontFace = new FontFace(f.name, `url(${f.data})`);
  241. fontFace.load().then(loaded => {
  242. document.fonts.add(loaded);
  243. const opt = document.createElement("option");
  244. opt.value = f.name;
  245. opt.textContent = f.name;
  246. opt.style.fontFamily = f.name;
  247. fontSelect.appendChild(opt);
  248. }).catch(err => console.error("Failed to load stored font:", err));
  249. });
  250. })();
  251.  
  252. // Font upload
  253. fontUpload.onchange = () => {
  254. for (const file of fontUpload.files) {
  255. const reader = new FileReader();
  256. reader.onload = async e => {
  257. const fontName = file.name.replace(/\..+$/, "");
  258. try {
  259. const fontFace = new FontFace(fontName, `url(${e.target.result})`);
  260. await fontFace.load();
  261. document.fonts.add(fontFace);
  262. const opt = document.createElement("option");
  263. opt.value = fontName;
  264. opt.textContent = fontName;
  265. opt.style.fontFamily = fontName;
  266. fontSelect.appendChild(opt);
  267. fontSelect.value = fontName;
  268. selectedFont = fontName;
  269. await saveFontToDB(fontName, e.target.result);
  270. if (rawText) generateTextImage();
  271. } catch (err) { console.error("Font failed to load:", err); }
  272. };
  273. reader.readAsDataURL(file);
  274. }
  275. };
  276. fontSelect.onchange = () => { selectedFont = fontSelect.value; if (rawText) generateTextImage(); };
  277. charSetSelect.addEventListener("change", () => {
  278. if (charSetSelect.value === "custom") {
  279. charSetInput.style.display = "inline-block";
  280. } else {
  281. charSetInput.style.display = "none";
  282. asciiChars = charSetSelect.value;
  283. charSetInput.value = asciiChars; // Keep the hidden input in sync
  284. renderASCII();
  285. }
  286. });
  287.  
  288. charSetInput.addEventListener("input", () => {
  289. if (charSetSelect.value === "custom") {
  290. asciiChars = charSetInput.value || " ";
  291. renderASCII();
  292. }
  293. });
  294.  
  295. // Utility: draw image with rotation/flip to a canvas scaled to desired ascii size
  296. function drawToScaledCanvas(img, targetW, targetH) {
  297. if (!img) return null;
  298. const radians = rotation * Math.PI / 180;
  299. const imgW = img.width;
  300. const imgH = img.height;
  301. const sin = Math.abs(Math.sin(radians));
  302. const cos = Math.abs(Math.cos(radians));
  303. const rotWidth = imgW * cos + imgH * sin;
  304. const rotHeight = imgW * sin + imgH * cos;
  305.  
  306. const canvas = document.createElement("canvas");
  307. canvas.width = Math.max(1, Math.round(targetW));
  308. canvas.height = Math.max(1, Math.round(targetH));
  309. const ctx = canvas.getContext("2d");
  310. ctx.clearRect(0,0,canvas.width,canvas.height);
  311.  
  312. ctx.save();
  313. ctx.translate(canvas.width/2, canvas.height/2);
  314. ctx.rotate(radians);
  315. ctx.scale(canvas.width/rotWidth, canvas.height/rotHeight);
  316. ctx.translate(-imgW/2, -imgH/2);
  317. // flips
  318. ctx.translate(flipH ? imgW : 0, flipV ? imgH : 0);
  319. ctx.scale(flipH ? -1 : 1, flipV ? -1 : 1);
  320. ctx.drawImage(img, 0, 0, imgW, imgH);
  321. ctx.restore();
  322.  
  323. return canvas;
  324. }
  325.  
  326. // Core: plain ascii (brightness -> chars)
  327. function imageToASCIIString(img, maxWidthChars = 80, maxHeightChars = 80) {
  328. const canvas = drawToScaledCanvas(img, maxWidthChars, maxHeightChars);
  329. if (!canvas) return "";
  330. const ctx = canvas.getContext("2d");
  331. const data = ctx.getImageData(0,0,canvas.width,canvas.height).data;
  332. let ascii = "";
  333. for (let y=0;y<canvas.height;y++){
  334. for (let x=0;x<canvas.width;x++){
  335. const i = (y*canvas.width + x)*4;
  336. const r = data[i], g = data[i+1], b = data[i+2];
  337. const lum = (0.299*r + 0.587*g + 0.114*b)/255;
  338. let bright = Math.min(Math.max((lum-0.5)*contrast+0.5,0),1);
  339. let idx = Math.floor(bright*(asciiChars.length-1));
  340. if (inverted) idx = asciiChars.length-1-idx;
  341. ascii += asciiChars[idx];
  342. }
  343. ascii += "\n";
  344. }
  345. return ascii;
  346. }
  347.  
  348. // New: HTML-colored ASCII (span per char)
  349. function imageToHtmlColored(img, maxWidthChars = 80, maxHeightChars = 80, useBlocks=false) {
  350. const canvas = drawToScaledCanvas(img, maxWidthChars, maxHeightChars);
  351. if (!canvas) return "";
  352. const ctx = canvas.getContext("2d");
  353. const data = ctx.getImageData(0,0,canvas.width,canvas.height).data;
  354. // Use a <pre> friendly string with spans
  355. let out = "";
  356. for (let y=0;y<canvas.height;y++){
  357. for (let x=0;x<canvas.width;x++){
  358. const i = (y*canvas.width + x)*4;
  359. const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
  360. if (a === 0) {
  361. out += '<span style="color:transparent"> </span>';
  362. continue;
  363. }
  364. // Choose character by brightness if not using blocks
  365. const lum = (0.299*r + 0.587*g + 0.114*b)/255;
  366. let bright = Math.min(Math.max((lum-0.5)*contrast+0.5,0),1);
  367. let ch = useBlocks ? "█" : asciiChars[Math.floor(bright*(asciiChars.length-1))];
  368. if (inverted && !useBlocks) ch = asciiChars[asciiChars.length-1 - Math.floor(bright*(asciiChars.length-1))];
  369. // Use foreground color (could also use background)
  370. out += `<span style="color: rgb(${r},${g},${b})">${escapeHtml(ch)}</span>`;
  371. }
  372. out += "\n";
  373. }
  374. return out;
  375. }
  376.  
  377. // New: ANSI (returns string with 24-bit ANSI escape sequences)
  378. // Uses \x1b[38;2;R;G;Bm for foreground color and resets per char
  379. function imageToAnsi(img, maxWidthChars = 80, maxHeightChars = 80, useBlocks=false) {
  380. const canvas = drawToScaledCanvas(img, maxWidthChars, maxHeightChars);
  381. if (!canvas) return "";
  382. const ctx = canvas.getContext("2d");
  383. const data = ctx.getImageData(0,0,canvas.width,canvas.height).data;
  384. let out = "";
  385. for (let y=0;y<canvas.height;y++){
  386. for (let x=0;x<canvas.width;x++){
  387. const i = (y*canvas.width + x)*4;
  388. const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
  389. if (a === 0) {
  390. out += " ";
  391. continue;
  392. }
  393. const lum = (0.299*r + 0.587*g + 0.114*b)/255;
  394. let bright = Math.min(Math.max((lum-0.5)*contrast+0.5,0),1);
  395. let ch = useBlocks ? "█" : asciiChars[Math.floor(bright*(asciiChars.length-1))];
  396. if (inverted && !useBlocks) ch = asciiChars[asciiChars.length-1 - Math.floor(bright*(asciiChars.length-1))];
  397. out += `\x1b[38;2;${r};${g};${b}m${ch}\x1b[0m`;
  398. }
  399. out += "\n";
  400. }
  401. return out;
  402. }
  403.  
  404. // Escape HTML for safe spans
  405. function escapeHtml(s) {
  406. return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
  407. }
  408.  
  409. // Export colored HTML view as PNG (rendering asciiHtmlOutput to canvas)
  410. async function exportAsciiHtmlToPng() {
  411. // create a canvas and draw colored ascii using same drawToScaledCanvas but scaled for font
  412. const scale = Math.max(1, (asciiWidth + asciiHeight) / 160);
  413. const zoom = parseFloat(zoomSlider.value);
  414. const charW = 8 * scale * zoom; // approximate char width
  415. const charH = 12 * scale * zoom; // approximate line height
  416. const cols = Math.max(1, asciiWidth);
  417. const rows = Math.max(1, asciiHeight);
  418. const canvas = document.createElement("canvas");
  419. canvas.width = Math.ceil(charW * cols);
  420. canvas.height = Math.ceil(charH * rows);
  421. const ctx = canvas.getContext("2d");
  422. // white background to make png readable
  423. ctx.fillStyle = "#00000000";
  424. ctx.fillRect(0,0,canvas.width,canvas.height);
  425.  
  426. // draw per-cell colored char (use block mode for best visuals)
  427. const imgCanvas = drawToScaledCanvas(originalImage, cols, rows);
  428. if (!imgCanvas) {
  429. alert("No image to export.");
  430. return;
  431. }
  432. const imgData = imgCanvas.getContext("2d").getImageData(0,0,cols,rows).data;
  433.  
  434. ctx.textBaseline = "top";
  435. ctx.font = `${Math.round(charH)}px ${selectedFont}, monospace`;
  436.  
  437. for (let y=0;y<rows;y++){
  438. for (let x=0;x<cols;x++){
  439. const i = (y*cols + x)*4;
  440. const r = imgData[i], g = imgData[i+1], b = imgData[i+2], a = imgData[i+3];
  441. if (a === 0) continue;
  442. ctx.fillStyle = `rgb(${r},${g},${b})`;
  443. const lum = (0.299*r + 0.587*g + 0.114*b)/255;
  444. const bright = Math.min(Math.max((lum-0.5)*contrast+0.5,0),1);
  445. const ch = "█";
  446. ctx.fillText(ch, x*charW, y*charH);
  447. }
  448. }
  449. // download
  450. const a = document.createElement("a");
  451. a.href = canvas.toDataURL();
  452. a.download = "ascii_color.png";
  453. a.click();
  454. }
  455.  
  456. // Plain ascii render
  457. function renderAsciiPlain() {
  458. if (!originalImage) return;
  459. latestAsciiText = imageToASCIIString(originalImage, asciiWidth, asciiHeight);
  460. asciiOutput.value = latestAsciiText;
  461. }
  462.  
  463. // Full render manager (switches output by colorMode)
  464. function renderASCII() {
  465. if (!originalImage) return;
  466. if (colorMode === "none") {
  467. asciiHtmlOutput.style.display = "none";
  468. asciiOutput.style.display = "block";
  469. renderAsciiPlain();
  470. } else if (colorMode === "html") {
  471. asciiOutput.style.display = "none";
  472. asciiHtmlOutput.style.display = "block";
  473. // Use blocks mode? Use character mode
  474. asciiHtmlOutput.innerHTML = imageToHtmlColored(originalImage, asciiWidth, asciiHeight, false);
  475. } else if (colorMode === "blocks") {
  476. asciiOutput.style.display = "none";
  477. asciiHtmlOutput.style.display = "block";
  478. asciiHtmlOutput.innerHTML = imageToHtmlColored(originalImage, asciiWidth, asciiHeight, true);
  479. } else if (colorMode === "ansi") {
  480. // For ansi mode we still show plain ASCII in textarea but allow ANSI export
  481. asciiHtmlOutput.style.display = "none";
  482. asciiOutput.style.display = "block";
  483. latestAsciiText = imageToASCIIString(originalImage, asciiWidth, asciiHeight);
  484. asciiOutput.value = latestAsciiText;
  485. }
  486. }
  487.  
  488. // Auto font scaling function (keeps ascii readable)
  489. function updateAsciiFontScaling() {
  490. const zoom = parseFloat(zoomSlider.value);
  491. const scale = Math.max(0.2, (asciiWidth + asciiHeight) / 160);
  492. asciiOutput.style.fontSize = `${8 * scale * zoom}px`;
  493. asciiOutput.style.lineHeight = `${6 * scale * zoom}px`;
  494. asciiHtmlOutput.style.fontSize = `${8 * scale * zoom}px`;
  495. asciiHtmlOutput.style.lineHeight = `${6 * scale * zoom}px`;
  496. }
  497.  
  498. // BUTTONS
  499. useTextBtn.onclick = () => { rawText = textInput.value; if (rawText) generateTextImage(); };
  500. selectAllBtn.onclick = () => { asciiOutput.select(); };
  501. copyBtn.onclick = async () => {
  502. try {
  503. await navigator.clipboard.writeText(asciiOutput.value);
  504. } catch (e) {
  505. alert("Copy failed.");
  506. }
  507. };
  508. invertBtn.onclick = () => { inverted = !inverted; renderASCII(); };
  509. flipHBtn.onclick = () => { flipH = !flipH; renderASCII(); };
  510. flipVBtn.onclick = () => { flipV = !flipV; renderASCII(); };
  511. downloadBtn.onclick = () => { const blob = new Blob([latestAsciiText], {type:"text/plain"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "ascii_output.txt"; a.click(); URL.revokeObjectURL(a.href); };
  512.  
  513. copyAnsiBtn.onclick = async () => {
  514. if (!originalImage) { alert("No image."); return; }
  515. const ansi = imageToAnsi(originalImage, asciiWidth, asciiHeight, false);
  516. try {
  517. await navigator.clipboard.writeText(ansi);
  518. alert("ANSI copied to clipboard.");
  519. } catch (e) { alert("Copy failed."); }
  520. };
  521.  
  522. exportPngBtn.onclick = () => exportAsciiHtmlToPng();
  523.  
  524. // SLIDERS: width, height, size, zoom, contrast, rotation
  525. widthSlider.addEventListener("input", () => {
  526. const val = parseInt(widthSlider.value);
  527. flipH = val < 0;
  528. asciiWidth = Math.abs(val) || 1;
  529. widthValue.innerText = val;
  530. updateAsciiFontScaling();
  531. renderASCII();
  532. });
  533. heightSlider.addEventListener("input", () => {
  534. const val = parseInt(heightSlider.value);
  535. flipV = val < 0;
  536. asciiHeight = Math.abs(val) || 1;
  537. heightValue.innerText = val;
  538. updateAsciiFontScaling();
  539. renderASCII();
  540. });
  541. sizeSlider.addEventListener("input", () => {
  542. let val = parseInt(sizeSlider.value);
  543. sizeValue.innerText = val;
  544. if (val < 0) { flipH = true; flipV = true; asciiWidth = asciiHeight = Math.abs(val) || 1; }
  545. else { flipH = false; flipV = false; asciiWidth = asciiHeight = val || 1; }
  546. widthSlider.value = asciiWidth; heightSlider.value = asciiHeight;
  547. widthValue.innerText = asciiWidth; heightValue.innerText = asciiHeight;
  548. updateAsciiFontScaling();
  549. renderASCII();
  550. });
  551. zoomSlider.addEventListener("input", () => {
  552. zoomValue.innerText = parseFloat(zoomSlider.value).toFixed(1) + "x";
  553. updateAsciiFontScaling();
  554. renderASCII();
  555. });
  556. contrastSlider.addEventListener("input", () => { contrast = parseFloat(contrastSlider.value); contrastValue.innerText = contrast.toFixed(1); renderASCII(); });
  557. rotationSlider.addEventListener("input", () => { rotation = parseFloat(rotationSlider.value); rotationValue.innerText = rotation.toFixed(1); renderASCII(); });
  558.  
  559. colorModeSelect.addEventListener("change", () => {
  560. colorMode = colorModeSelect.value;
  561. // show/hide ANSI button and export
  562. copyAnsiBtn.style.display = (colorMode === "ansi") ? "inline-block" : "inline-block";
  563. if (colorMode === "none") {
  564. asciiOutput.style.display = "block";
  565. asciiHtmlOutput.style.display = "none";
  566. }
  567. renderASCII();
  568. });
  569.  
  570. // IMAGE INPUT
  571. imageInput.onchange = () => { const file=imageInput.files[0]; if(!file) return; const img=new Image(); img.onload=()=>{ originalImage=img; renderASCII(); }; img.src=URL.createObjectURL(file); };
  572.  
  573. // CLIPBOARD IMAGE
  574. fromClipboardBtn.onclick = async () => {
  575. try {
  576. const items = await navigator.clipboard.read();
  577. for (const item of items) {
  578. if(!item.types.some(t=>t.startsWith('image/'))) continue;
  579. const blob = await item.getType(item.types[0]);
  580. const img = new Image();
  581. img.onload=()=>{ originalImage=img; renderASCII(); };
  582. img.src=URL.createObjectURL(blob);
  583. break;
  584. }
  585. } catch(e) { alert("Clipboard image paste not supported or permission denied."); console.error(e); }
  586. };
  587.  
  588. // TEXT->IMAGE
  589. function generateTextImage() {
  590. if (!rawText) return;
  591. const canvas = document.createElement("canvas");
  592. const ctx = canvas.getContext("2d");
  593. const fontSize = 36;
  594. const lines = rawText.split("\n");
  595. ctx.font = `${fontSize}px "${selectedFont}", monospace`;
  596. const width = Math.max(...lines.map(l => ctx.measureText(l).width)) + 10;
  597. const height = lines.length * fontSize;
  598. canvas.width = width;
  599. canvas.height = height;
  600. ctx.fillStyle = "white";
  601. ctx.fillRect(0,0,width,height);
  602. ctx.fillStyle = "black";
  603. ctx.textBaseline = "top";
  604. ctx.font = `${fontSize}px "${selectedFont}", monospace`;
  605. lines.forEach((line,i)=>ctx.fillText(line,5,i*fontSize));
  606. const img = new Image();
  607. img.onload = () => { originalImage = img; renderASCII(); };
  608. img.src = canvas.toDataURL();
  609. }
  610.  
  611. // WEBCAM
  612. let webcamStream = null, webcamVideo = null, lastFrameTime = 0;
  613. webcamBtn.onclick = async () => {
  614. if (!webcamStream) {
  615. try {
  616. webcamVideo = document.createElement("video");
  617. webcamVideo.autoplay = true;
  618. webcamVideo.playsInline = true;
  619. webcamStream = await navigator.mediaDevices.getUserMedia({video:true});
  620. webcamVideo.srcObject = webcamStream;
  621. webcamPreview.srcObject = webcamStream;
  622. webcamPreview.style.display = "block";
  623. webcamVideo.onloadeddata = () => { requestAnimationFrame(updateWebcamASCII); };
  624. } catch (e) { alert("Cannot access webcam."); console.error(e); }
  625. } else {
  626. webcamStream.getTracks().forEach(t=>t.stop());
  627. webcamStream=null; webcamVideo=null; originalImage=null;
  628. webcamPreview.srcObject=null; webcamPreview.style.display="none";
  629. }
  630. };
  631. function updateWebcamASCII(ts) {
  632. if (!webcamStream || !webcamVideo) return;
  633. // limit to ~12 FPS for performance
  634. if (ts - lastFrameTime < 80) { requestAnimationFrame(updateWebcamASCII); return; }
  635. lastFrameTime = ts;
  636. const tempCanvas = document.createElement("canvas");
  637. tempCanvas.width = webcamVideo.videoWidth || 320;
  638. tempCanvas.height = webcamVideo.videoHeight || 240;
  639. const tempCtx = tempCanvas.getContext("2d");
  640. tempCtx.drawImage(webcamVideo,0,0,tempCanvas.width,tempCanvas.height);
  641. originalImage = tempCanvas;
  642. renderASCII();
  643. requestAnimationFrame(updateWebcamASCII);
  644. }
  645.  
  646. // DRAG & RESIZE
  647. let isDragging=false, offsetX, offsetY;
  648. header.addEventListener("mousedown", e=>{ isDragging=true; offsetX=e.clientX-hud.offsetLeft; offsetY=e.clientY-hud.offsetTop; });
  649. document.addEventListener("mouseup", ()=>{ isDragging=false; isResizing=false; });
  650. document.addEventListener("mousemove", e=>{
  651. if(!isDragging) return;
  652. const maxX=window.innerWidth-hud.offsetWidth;
  653. const maxY=window.innerHeight-hud.offsetHeight;
  654. hud.style.left=Math.min(Math.max(0,e.clientX-offsetX),maxX)+"px";
  655. hud.style.top=Math.min(Math.max(0,e.clientY-offsetY),maxY)+"px";
  656. });
  657.  
  658. const resizeHandle=document.createElement("div");
  659. resizeHandle.style.width="14px";
  660. resizeHandle.style.height="14px";
  661. resizeHandle.style.position="absolute";
  662. resizeHandle.style.right="4px";
  663. resizeHandle.style.bottom="4px";
  664. resizeHandle.style.cursor="nwse-resize";
  665. resizeHandle.style.background="transparent";
  666. hud.appendChild(resizeHandle);
  667.  
  668. let isResizing=false, startX, startY, startWidth, startHeight;
  669. resizeHandle.addEventListener("mousedown", e=>{
  670. e.stopPropagation();
  671. isResizing=true;
  672. startX=e.clientX; startY=e.clientY;
  673. startWidth=hud.offsetWidth; startHeight=hud.offsetHeight;
  674. });
  675. document.addEventListener("mousemove", e=>{
  676. if(!isResizing) return;
  677. const dx=e.clientX-startX;
  678. const dy=e.clientY-startY;
  679. const maxWidth=window.innerWidth-hud.offsetLeft;
  680. const maxHeight=window.innerHeight-hud.offsetTop;
  681. hud.style.width=Math.min(Math.max(360,startWidth+dx),maxWidth)+"px";
  682. hud.style.height=Math.min(Math.max(320,startHeight+dy),maxHeight)+"px";
  683. });
  684.  
  685. // initial scaling
  686. updateAsciiFontScaling();
  687.  
  688. // initial render (if image already set)
  689. if (originalImage) renderASCII();
  690.  
  691. })();
  692.  
Advertisement
Add Comment
Please, Sign In to add comment