You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
symbols = ".'`~:;=-+*#%@∑∏∫∮∇ΔΘΩ", // Default symbols: ordered light to heavy visual weight
outputCharWidth = 80,
fontSize = 10,
backgroundColor = "white",
textColor = "black",
invertLuminance = 0, // 0 for dark-on-light mapping (dark image -> heavy symbol), 1 for light-on-dark mapping
contrast = 1.0,
characterSpacingFactor = 0.6,
lineSpacingFactor = 1.0
) {
// --- Parameter Coercion and Validation ---
let pOutputCharWidth = parseInt(outputCharWidth, 10);
if (isNaN(pOutputCharWidth) || pOutputCharWidth <= 0) {
pOutputCharWidth = 80;
}
let pFontSize = parseFloat(fontSize);
if (isNaN(pFontSize) || pFontSize <= 0) {
pFontSize = 10;
}
const pBackgroundColor = String(backgroundColor);
const pTextColor = String(textColor);
// interpret invertLuminance: number 1 or string "1" means true, otherwise false.
const pInvertLuminance = Number(invertLuminance) === 1;
let pContrast = parseFloat(contrast);
if (isNaN(pContrast)) {
pContrast = 1.0;
}
let pCharacterSpacingFactor = parseFloat(characterSpacingFactor);
if (isNaN(pCharacterSpacingFactor) || pCharacterSpacingFactor <= 0) {
pCharacterSpacingFactor = 0.6;
}
let pLineSpacingFactor = parseFloat(lineSpacingFactor);
if (isNaN(pLineSpacingFactor) || pLineSpacingFactor <= 0) {
pLineSpacingFactor = 1.0;
}
let pSymbols = String(symbols);
if (pSymbols.length === 0) {
// A fallback set if user provides an empty string, ordered light to heavy.
pSymbols = ".'`~:;=-+*#%@∑∏∫∮∇ΔΘΩ";
}
const numSymbols = pSymbols.length;
// --- Image Processing ---
// 1. Create a temporary canvas to sample pixels from originalImg
const tempCanvas = document.createElement('canvas');
// Specify willReadFrequently for potential performance optimization if available.
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
if (!(originalImg instanceof HTMLImageElement) || originalImg.width === 0 || originalImg.height === 0) {
console.error("Invalid image object or image not loaded properly.");
const errorCanvas = document.createElement('canvas'); errorCanvas.width=300; errorCanvas.height=50;
const errCtx = errorCanvas.getContext('2d');
errCtx.fillStyle = 'lightgray'; errCtx.fillRect(0,0,300,50);
errCtx.fillStyle = 'red'; errCtx.font = '12px Arial';
errCtx.fillText('Error: Invalid or unloaded image provided.', 10, 20);
return errorCanvas;
}
const aspectRatio = originalImg.height / originalImg.width;
// Additional check for aspectRatio if width/height were somehow non-positive after initial check
if (isNaN(aspectRatio) || !isFinite(aspectRatio) || aspectRatio <= 0) {
console.error("Invalid image dimensions or aspect ratio cannot be determined.");
const errorCanvas = document.createElement('canvas'); errorCanvas.width=1; errorCanvas.height=1; /* Minimal canvas */ return errorCanvas;
}
const sampleWidth = pOutputCharWidth;
const sampleHeight = Math.max(1, Math.floor(sampleWidth * aspectRatio)); // Ensure at least 1px height
tempCanvas.width = sampleWidth;
tempCanvas.height = sampleHeight;
// Draw the original image scaled down to the sampling grid dimensions
// This helps in averaging pixel colors for each cell of the symbol grid
tempCtx.drawImage(originalImg, 0, 0, sampleWidth, sampleHeight);
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, sampleWidth, sampleHeight);
} catch (e) {
console.error("Error getting imageData:", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 400; errorCanvas.height = 60; // Adjusted size for better message display
const errCtx = errorCanvas.getContext('2d');
errCtx.fillStyle = 'lightgray'; errCtx.fillRect(0,0,errorCanvas.width, errorCanvas.height);
errCtx.fillStyle = 'red'; errCtx.font = 'bold 12px Arial';
errCtx.fillText('Error: Could not process image data.', 10, 20);
if (e.name === 'SecurityError') {
errCtx.fillText('This might be due to cross-origin security restrictions.', 10, 40);
errCtx.fillText('Ensure the image is from the same domain or has CORS approval.', 10, 55);
} else {
errCtx.fillText('Details: ' + e.message, 10, 40);
}
return errorCanvas;
}
const pixels = imageData.data;
// 2. Prepare the output canvas
const outputCanvas = document.createElement('canvas');
// Calculate cell dimensions based on font size and spacing factors
const cellWidth = pFontSize * pCharacterSpacingFactor;
const cellHeight = pFontSize * pLineSpacingFactor;
outputCanvas.width = Math.max(1, Math.floor(sampleWidth * cellWidth));
outputCanvas.height = Math.max(1, Math.floor(sampleHeight * cellHeight));
const outputCtx = outputCanvas.getContext('2d');
// Fill background
outputCtx.fillStyle = pBackgroundColor;
outputCtx.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
// Set font properties
outputCtx.font = `${pFontSize}px monospace`; // Monospace font is crucial for grid alignment
outputCtx.fillStyle = pTextColor;
outputCtx.textAlign = "center";
outputCtx.textBaseline = "middle"; // Aligns symbol vertically in the center of the cell
// 3. Iterate through sampled pixels and draw symbols
for (let y = 0; y < sampleHeight; y++) {
for (let x = 0; x < sampleWidth; x++) {
const pixelIndex = (y * sampleWidth + x) * 4; // Each pixel has 4 components (R,G,B,A)
const r = pixels[pixelIndex];
const g = pixels[pixelIndex + 1];
const b = pixels[pixelIndex + 2];
// Alpha (pixels[pixelIndex + 3]) is not used for brightness calculation in this version
// Calculate brightness (luminance) using standard coefficients
let brightness = 0.299 * r + 0.587 * g + 0.114 * b; // Range: 0-255
// Apply contrast
let normalizedBrightness = brightness / 255.0; // Normalize to 0-1 range
// The contrast formula adjusts values relative to the midpoint (0.5)
normalizedBrightness = (normalizedBrightness - 0.5) * pContrast + 0.5;
normalizedBrightness = Math.max(0, Math.min(1, normalizedBrightness)); // Clamp back to 0-1
// Determine effective brightness for symbol mapping based on inversion preference
// pSymbols is ordered from visually lightest to heaviest.
// if pInvertLuminance is false (default):
// - Dark image parts (low normalizedBrightness) -> map to (1 - low_norm) -> high effective brightness -> heavy symbol (end of pSymbols)
// - Light image parts (high normalizedBrightness) -> map to (1 - high_norm) -> low effective brightness -> light symbol (start of pSymbols)
// if pInvertLuminance is true:
// - Dark image parts (low normalizedBrightness) -> map to low_norm -> low effective brightness -> light symbol (start of pSymbols)
// - Light image parts (high normalizedBrightness) -> map to high_norm -> high effective brightness -> heavy symbol (end of pSymbols)
let effectiveNormalizedBrightness = pInvertLuminance ? normalizedBrightness : (1.0 - normalizedBrightness);
// Map effective brightness to a symbol index
// effectiveNormalizedBrightness = 0 should map to index 0 (lightest symbol)
// effectiveNormalizedBrightness = 1 should map to index numSymbols-1 (heaviest symbol)
let symbolIndex = Math.floor(effectiveNormalizedBrightness * numSymbols);
// Clamp index to be within the bounds of the pSymbols string
symbolIndex = Math.min(numSymbols - 1, Math.max(0, symbolIndex));
const charToDraw = pSymbols[symbolIndex];
// Calculate drawing position for the center of the current cell
const drawX = x * cellWidth + cellWidth / 2;
const drawY = y * cellHeight + cellHeight / 2;
outputCtx.fillText(charToDraw, drawX, drawY);
}
}
return outputCanvas;
}
Apply Changes