You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, layerHeightParam = 20, colorPaletteStrParam = "auto", noiseAmountParam = 0.1, distortionAmountParam = 5) {
// Helper function: clamp a value between a minimum and maximum
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
// Helper function: convert hex color string to an RGB object
function hexToRgb(hex) {
if (hex === null || typeof hex === 'undefined') return { r: 128, g: 128, b: 128 }; // Default to gray for null/undefined
let normalizedHex = String(hex).trim();
if (!normalizedHex) return { r: 128, g: 128, b: 128 }; // Default to gray for empty string
if (!normalizedHex.startsWith('#')) {
normalizedHex = '#' + normalizedHex;
}
// Expand shorthand form (e.g. "#03F") to full form (e.g. "#0033FF")
const shorthandRegex = /^#([a-f\d])([a-f\d])([a-f\d])$/i;
normalizedHex = normalizedHex.replace(shorthandRegex, (m, r, g, b) => '#' + r + r + g + g + b + b);
const result = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(normalizedHex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 128, g: 128, b: 128 }; // Default to gray if parsing fails
}
// Helper function: calculate the average color for a specific region (layer) of an image
function calculateAverageColorForLayerRegion(srcPixelData, imgWidth, imgHeight, layerNominalStartY, currentLayerHeight) {
let rSum = 0, gSum = 0, bSum = 0;
let pixelCount = 0;
// Determine the actual start and end Y coordinates for averaging, clamped to image bounds
const startY = clamp(layerNominalStartY, 0, imgHeight - 1);
const endY = clamp(layerNominalStartY + currentLayerHeight, 0, imgHeight);
// If the calculated layer region is outside image bounds or has no height
if (startY >= endY) {
return { r: 128, g: 128, b: 128}; // Default to gray
}
for (let y = startY; y < endY; y++) {
for (let x = 0; x < imgWidth; x++) {
const idx = (y * imgWidth + x) * 4;
rSum += srcPixelData[idx];
gSum += srcPixelData[idx + 1];
bSum += srcPixelData[idx + 2];
pixelCount++;
}
}
if (pixelCount === 0) return { r: 128, g: 128, b: 128 }; // Default gray if no pixels were processed
return {
r: rSum / pixelCount,
g: gSum / pixelCount,
b: bSum / pixelCount
};
}
// --- Parameter parsing and sanitization ---
const layerHeight = Math.max(1, Number(layerHeightParam) || 20);
// Normalize colorPaletteStrParam for consistent checking
const normalizedColorPaletteStr = String(colorPaletteStrParam).trim().toLowerCase();
const noiseAmount = clamp(Number(noiseAmountParam) || 0.1, 0, 1);
const distortionAmount = Math.max(0, Number(distortionAmountParam) || 5);
const DEFAULT_PALETTE_HEX_ARRAY = ["#A37E58","#8C6D56","#755C54","#5E4B4D","#473A42","#B89A78","#D1B797"];
// --- Canvas setup ---
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can be an optimization hint for repeated getImageData/putImageData calls.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = originalImg.naturalWidth || originalImg.width || 0;
const height = originalImg.naturalHeight || originalImg.height || 0;
if (width === 0 || height === 0) {
console.error("Image has zero dimensions. Returning empty canvas.");
canvas.width = 1; // Minimal canvas dimensions
canvas.height = 1;
return canvas;
}
canvas.width = width;
canvas.height = height;
// --- Original image data extraction (for "auto" color mode) ---
let originalPixelData = null;
let useAutoColorMode = (normalizedColorPaletteStr === "auto");
if (useAutoColorMode) {
const originalCanvas = document.createElement('canvas');
const originalCtx = originalCanvas.getContext('2d', { willReadFrequently: true });
originalCanvas.width = width;
originalCanvas.height = height;
originalCtx.drawImage(originalImg, 0, 0);
try {
originalPixelData = originalCtx.getImageData(0, 0, width, height).data;
} catch (e) {
console.warn("Could not get image data for 'auto' mode, possibly due to cross-origin restrictions. Falling back from 'auto' mode.", e);
useAutoColorMode = false; // Fallback: disable auto mode
}
}
// --- Determine active palette (if not in successful "auto" mode) ---
let activePaletteRgbArray = [];
if (!useAutoColorMode) {
let hexStringsForPalette = DEFAULT_PALETTE_HEX_ARRAY; // Default assumption
if (normalizedColorPaletteStr !== "auto" && normalizedColorPaletteStr !== "") {
// User provided a specific palette string (not "auto" and not empty)
const customHexStrings = String(colorPaletteStrParam).trim().split(',') // Use original param for case-sensitive hex
.map(s => s.trim())
.filter(s => s !== ""); // Remove empty strings that might result from " ,, "
if (customHexStrings.length > 0) {
hexStringsForPalette = customHexStrings; // Use user's palette
}
}
activePaletteRgbArray = hexStringsForPalette.map(hex => hexToRgb(hex));
// Ensure palette is not empty (e.g., if default palette was somehow cleared or user provided invalid list)
if (activePaletteRgbArray.length === 0) {
activePaletteRgbArray = DEFAULT_PALETTE_HEX_ARRAY.map(hex => hexToRgb(hex));
}
}
// --- Main image processing loop ---
const outputImgData = ctx.createImageData(width, height);
const outputData = outputImgData.data;
const layerAvgColorsCache = new Map(); // Cache for "auto" mode layer colors
for (let y = 0; y < height; y++) {
const currentTrueLayerIndex = Math.floor(y / layerHeight); // Layer index based on non-distorted y
for (let x = 0; x < width; x++) {
let distortedY = y;
if (distortionAmount > 0) {
// Create a wavy distortion. Phase depends on x and the true layer index to vary patterns.
// (width / (Math.PI * 6)) means roughly 3 sine waves across the image width.
const distortionPhase = (x / (width / (Math.PI * 6))) + (currentTrueLayerIndex * Math.PI / 2.0);
const distortionVal = Math.sin(distortionPhase) * distortionAmount;
distortedY = clamp(y + distortionVal, 0, height - 1); // Clamp to image bounds
}
const effectiveLayerIndex = Math.floor(distortedY / layerHeight); // Layer index based on (potentially) distorted y
let finalLayerColorRgb;
if (useAutoColorMode && originalPixelData) { // "Auto" color mode
if (!layerAvgColorsCache.has(effectiveLayerIndex)) {
const avgColor = calculateAverageColorForLayerRegion(originalPixelData, width, height, effectiveLayerIndex * layerHeight, layerHeight);
layerAvgColorsCache.set(effectiveLayerIndex, avgColor);
}
finalLayerColorRgb = layerAvgColorsCache.get(effectiveLayerIndex);
} else { // Predefined or default palette mode
finalLayerColorRgb = activePaletteRgbArray[effectiveLayerIndex % activePaletteRgbArray.length];
}
// Fallback if color somehow didn't resolve (should be rare)
if (!finalLayerColorRgb) finalLayerColorRgb = {r:128, g:128, b:128};
let r = finalLayerColorRgb.r;
let g = finalLayerColorRgb.g;
let b = finalLayerColorRgb.b;
// Add noise
if (noiseAmount > 0) {
const noiseVal = (Math.random() - 0.5) * 2 * 255 * noiseAmount; // Monochromatic noise
r = clamp(r + noiseVal, 0, 255);
g = clamp(g + noiseVal, 0, 255);
b = clamp(b + noiseVal, 0, 255);
}
const idx = (y * width + x) * 4; // Pixel index in the ImageData array
outputData[idx] = r;
outputData[idx + 1] = g;
outputData[idx + 2] = b;
outputData[idx + 3] = 255; // Set alpha to fully opaque
}
}
ctx.putImageData(outputImgData, 0, 0); // Draw the processed image data onto the canvas
return canvas;
}
Apply Changes