You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
charCols = "150",
colorized = "1",
invertBrightness = "0",
bgColor = "#ffffff",
fontColor = "#000000",
charString = " ⠄·・゜*゚▽ヮω◇д☆※❀✿♡★●♥■▓█"
) {
const cols = parseInt(charCols);
const useColor = parseInt(colorized) !== 0;
const invert = parseInt(invertBrightness) !== 0;
const symbols = Array.from(charString);
const fontSize = 14;
const fontFamily = "monospace";
const font = `bold ${fontSize}px ${fontFamily}`;
// Measure character dimensions dynamically to maintain aspect ratio and grid alignment
// We measure both a standard ASCII char and a double-width Japanese/decorative symbol
// to ensure the grid cell can encompass the maximum width perfectly.
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCtx.font = font;
const cw1 = tempCtx.measureText("M").width;
const cw2 = tempCtx.measureText("✿").width;
const cellW = Math.max(cw1, cw2);
const cellH = fontSize * 1.25; // standard line-height approximation
const fontAspectCorrection = cellW / cellH;
const rows = Math.max(1, Math.floor((originalImg.height / originalImg.width) * cols * fontAspectCorrection));
// Downsample the image to determine character mapping limits
const imgCanvas = document.createElement('canvas');
imgCanvas.width = cols;
imgCanvas.height = rows;
const imgCtx = imgCanvas.getContext('2d');
// Draw image onto smaller canvas to get pixel luminance and color data
imgCtx.drawImage(originalImg, 0, 0, cols, rows);
const imgData = imgCtx.getImageData(0, 0, cols, rows).data;
// Prepare offscreen canvas holding the actual cached text art
const offscreen = document.createElement('canvas');
offscreen.width = cols * cellW;
offscreen.height = rows * cellH;
const offCtx = offscreen.getContext('2d');
offCtx.fillStyle = bgColor;
offCtx.fillRect(0, 0, offscreen.width, offscreen.height);
offCtx.font = font;
offCtx.textAlign = "center";
offCtx.textBaseline = "middle";
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const idx = (y * cols + x) * 4;
const r = imgData[idx];
const g = imgData[idx + 1];
const b = imgData[idx + 2];
const a = imgData[idx + 3];
// Interpret fully transparent pixels as maximum luminance (acts like background mapping)
let luminance = 0.299 * r + 0.587 * g + 0.114 * b;
if (a < 128) luminance = 255;
let normalized = luminance / 255;
if (invert) {
normalized = 1 - normalized;
}
// Map luminance to character index (0 = lightest/space, length-1 = darkest/densest element)
let charIdx = Math.floor((1 - normalized) * (symbols.length - 1));
charIdx = Math.max(0, Math.min(symbols.length - 1, charIdx));
const char = symbols[charIdx];
if (char === ' ') continue; // Optimization: skip spaces
if (useColor) {
offCtx.fillStyle = `rgb(${r},${g},${b})`;
} else {
offCtx.fillStyle = fontColor;
}
// Render perfectly at the center of the assigned grid cell
offCtx.fillText(char, x * cellW + cellW / 2, y * cellH + cellH / 2);
}
}
// Create interactive viewport canvas returned to the caller
const canvas = document.createElement('canvas');
canvas.width = 1200; // Default logical dimensions (responsive to CSS)
canvas.height = 800;
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.minHeight = "400px";
canvas.style.backgroundColor = bgColor;
canvas.style.cursor = "grab";
canvas.style.display = "block";
canvas.style.touchAction = "none"; // Prevents entire page from scrolling when zooming and panning
canvas.style.boxShadow = "inset 0px 0px 10px rgba(0,0,0,0.1)";
const ctx = canvas.getContext('2d');
let scale = 1;
// Fit completely in view initially
if (offscreen.width > canvas.width || offscreen.height > canvas.height) {
scale = Math.min(canvas.width / offscreen.width, canvas.height / offscreen.height) * 0.95;
}
let offsetX = (canvas.width - offscreen.width * scale) / 2;
let offsetY = (canvas.height - offscreen.height * scale) / 2;
const redraw = () => {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offscreen, offsetX, offsetY, offscreen.width * scale, offscreen.height * scale);
};
redraw();
// Helper to translate responsive CSS space into true internal canvas bounds
function getCanvasPos(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: e.clientX * scaleX,
y: e.clientY * scaleY
};
}
// Implements the Scalable Wheel Scroll zooming
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const pos = getCanvasPos(e);
const zoomAmount = 0.15;
const zoomFactor = e.deltaY < 0 ? (1 + zoomAmount) : (1 - zoomAmount);
const newScale = scale * zoomFactor;
// Boundaries to prevent zooming into the pixel atom or out excessively
if (newScale < 0.05 || newScale > 200) return;
// Math to zoom exactly towards the mouse position
offsetX = pos.x - (pos.x - offsetX) * zoomFactor;
offsetY = pos.y - (pos.y - offsetY) * zoomFactor;
scale = newScale;
redraw();
}, { passive: false });
// Implements Drag to Pan Interaction
let isDragging = false;
let startMouseX = 0;
let startMouseY = 0;
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
canvas.style.cursor = "grabbing";
const pos = getCanvasPos(e);
startMouseX = pos.x - offsetX;
startMouseY = pos.y - offsetY;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
const pos = getCanvasPos(e);
offsetX = pos.x - startMouseX;
offsetY = pos.y - startMouseY;
redraw();
});
const stopDrag = () => {
isDragging = false;
canvas.style.cursor = "grab";
};
canvas.addEventListener('mouseup', stopDrag);
canvas.addEventListener('mouseleave', stopDrag);
return canvas;
}
Apply Changes