You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, columns = 100, fontSize = 14, animSpeedMs = 5000) {
// Create and style the main visible canvas output
const canvas = document.createElement('canvas');
canvas.style.maxWidth = '100%';
canvas.style.backgroundColor = 'black';
canvas.style.display = 'block';
// Validate image load state
if (!originalImg || !originalImg.width || !originalImg.height) return canvas;
// Parse and constrain parameters
const cols = Math.max(10, Math.min(300, parseInt(columns) || 100));
const fSize = Math.max(8, Math.min(48, parseInt(fontSize) || 14));
const speed = Math.max(500, Math.min(30000, parseInt(animSpeedMs) || 5000));
// Dynamic font measurement for accurate character aspect ratio mapping
const testCtx = document.createElement('canvas').getContext('2d');
testCtx.font = `bold ${fSize}px monospace`;
const measuredW = testCtx.measureText('M').width;
const charW = measuredW > 0 ? measuredW : (fSize * 0.6);
const charH = fSize;
// Maintain aspect ratio factoring in the font dimensions
const rows = Math.round((originalImg.height / originalImg.width) * cols * (charW / charH));
if (rows < 1) return canvas;
canvas.width = cols * charW;
canvas.height = rows * charH;
// Render image to a small offscreen canvas to sample the pixel data
const offCanvas = document.createElement('canvas');
offCanvas.width = cols;
offCanvas.height = rows;
const offCtx = offCanvas.getContext('2d', { willReadFrequently: true });
offCtx.drawImage(originalImg, 0, 0, cols, rows);
const imgData = offCtx.getImageData(0, 0, cols, rows).data;
// Classic 16-color ANSI DOS Palette mapped to RGB
const ansiPalette = [
[0,0,0], [0,0,170], [0,170,0], [0,170,170], [170,0,0], [170,0,170], [170,85,0], [170,170,170],
[85,85,85], [85,85,255], [85,255,85], [85,255,255], [255,85,85], [255,85,255], [255,255,85], [255,255,255]
];
function getNearestColor(r, g, b) {
let minDist = Infinity;
let pColor = ansiPalette[0];
for (let i = 0; i < ansiPalette.length; i++) {
const p = ansiPalette[i];
const dist = (r - p[0]) ** 2 + (g - p[1]) ** 2 + (b - p[2]) ** 2;
if (dist < minDist) {
minDist = dist;
pColor = p;
}
}
return `rgb(${pColor[0]},${pColor[1]},${pColor[2]})`;
}
// Extended ASCII set mapped from darkest to brightest
const asciiStr = ' .\'`^",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';
// Group characters by exact color to heavily optimize rendering
const charGroups = {};
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const i = (y * cols + x) * 4;
const r = imgData[i];
const g = imgData[i + 1];
const b = imgData[i + 2];
const a = imgData[i + 3];
let char = ' ';
let colorKey = 'rgb(0,0,0)';
// Ignore transparent pixels
if (a >= 128) {
colorKey = getNearestColor(r, g, b);
const luma = 0.299 * r + 0.587 * g + 0.114 * b;
const charIdx = Math.floor((luma / 255) * (asciiStr.length - 1));
char = asciiStr[charIdx];
}
if (!charGroups[colorKey]) charGroups[colorKey] = [];
charGroups[colorKey].push({ x, y, char });
}
}
// Pre-render the fully revealed image (Static Image Buffer for extreme performance)
const finalCanvas = document.createElement('canvas');
finalCanvas.width = canvas.width;
finalCanvas.height = canvas.height;
const fctx = finalCanvas.getContext('2d');
fctx.fillStyle = 'black';
fctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
fctx.font = `bold ${fSize}px monospace`;
fctx.textBaseline = 'top';
for (const color in charGroups) {
fctx.fillStyle = color;
for (const item of charGroups[color]) {
if (item.char !== ' ') {
fctx.fillText(item.char, item.x * charW, item.y * charH);
}
}
}
// Interactive Animation setup (Decryption CRT Scanline Loop)
const ctx = canvas.getContext('2d');
const startTime = Date.now();
function draw() {
// Automatically isolate memory leak / rogue loops if the canvas gets completely discarded from the DOM
if (!canvas.isConnected) {
if (canvas.dataset.attached === 'true') return;
} else {
canvas.dataset.attached = 'true';
}
const now = Date.now();
const phase = ((now - startTime) % speed) / speed;
let lockY = 0;
let showNoise = true;
// Timeline progression
if (phase < 0.15) {
lockY = 0; // Stage 1: Establishing connection (Full Noise)
} else if (phase >= 0.15 && phase < 0.75) {
const sweepProgress = (phase - 0.15) / 0.60;
lockY = canvas.height * sweepProgress; // Stage 2: Decoder scanline down
} else {
lockY = canvas.height; // Stage 3: Hold final revealed ANSI Art
showNoise = false;
}
// Wipe current main frame
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw successfully decoded "locked" portion directly from our pre-rendered buffer
const yCrop = Math.floor(lockY);
if (yCrop > 0) {
ctx.drawImage(finalCanvas, 0, 0, canvas.width, yCrop, 0, 0, canvas.width, yCrop);
}
// Render the bright scanning CRT strip separating memory zones
if (yCrop > 0 && yCrop < canvas.height) {
ctx.fillStyle = 'rgba(0, 255, 0, 0.4)';
ctx.fillRect(0, yCrop - 4, canvas.width, 8);
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.fillRect(0, yCrop - 1, canvas.width, 2);
}
// Render live hacker/terminal matrix noise in uncleared spaces
if (showNoise) {
ctx.font = `bold ${fSize}px monospace`;
ctx.textBaseline = 'top';
const startRow = Math.max(0, Math.floor(yCrop / charH));
for (let y = startRow; y < rows; y++) {
for (let x = 0; x < cols; x++) {
// Introduce sparse random gaps via simple prob distribution limit
if (Math.random() > 0.4) {
ctx.fillStyle = Math.random() > 0.8 ? '#cfc' : '#0c0';
const char = asciiStr[Math.floor(Math.random() * asciiStr.length)];
ctx.fillText(char, x * charW, y * charH);
}
}
}
}
requestAnimationFrame(draw);
}
// Bootstrap loop
requestAnimationFrame(draw);
return canvas;
}
Apply Changes