You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, imprintColorStr = "sienna", backgroundColorStr = "dimgray", depth = 1.5, lightAngle = 135) {
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
// Helper to parse color strings (CSS color names, hex, rgb()) to {r, g, b} objects
function parseColor(colorStr) {
// Create a temporary element to resolve the color string
const tempDiv = document.createElement('div');
tempDiv.style.color = colorStr;
// Append to body (and remove) to ensure computed style is available for color names
// This is necessary for getComputedStyle to work correctly with color names.
document.body.appendChild(tempDiv);
const computedColor = window.getComputedStyle(tempDiv).color;
document.body.removeChild(tempDiv);
const match = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (match) {
return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
}
// Fallback for safety, though getComputedStyle usually returns rgb()
// If colorStr is invalid, getComputedStyle might return black or transparent black.
console.warn(`Could not parse color: ${colorStr}. Defaulting to black.`);
return { r: 0, g: 0, b: 0 };
}
const imprintColor = parseColor(imprintColorStr);
const backgroundColor = parseColor(backgroundColorStr);
// 1. Create a temporary canvas for image processing steps
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0, width, height);
// 2. Get pixel data, convert to grayscale, and store original alpha
const originalImageData = tempCtx.getImageData(0, 0, width, height);
const originalData = originalImageData.data;
const grayscaleValues = new Uint8ClampedArray(width * height);
const originalAlphas = new Uint8ClampedArray(width * height);
for (let i = 0; i < originalData.length; i += 4) {
const r = originalData[i];
const g = originalData[i + 1];
const b = originalData[i + 2];
const alpha = originalData[i + 3];
// Standard grayscale conversion formula
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
const flatIndex = i / 4;
grayscaleValues[flatIndex] = gray;
originalAlphas[flatIndex] = alpha;
}
// 3. Apply emboss effect to grayscale data
const embossedGrayscale = new Uint8ClampedArray(width * height);
// Convert lightAngle (degrees: 0=right, 90=up, 135=top-left, etc.) to a vector
// In typical math coordinates, positive Y is up. Canvas Y is down.
// A light source from angle A means comparing current pixel with a neighbor offset by (-cos(A), -sin(A)) related to canvas axes.
const angleRad = lightAngle * Math.PI / 180.0;
const lightVecX = Math.cos(angleRad); // Component of light vector along X-axis
const lightVecY = Math.sin(angleRad); // Component of light vector along math Y-axis (up)
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const currentIndex = y * width + x;
const currentGray = grayscaleValues[currentIndex];
// Determine neighbor coordinates for the emboss difference calculation.
// The neighbor displacement is opposite to the light direction components scaled by depth.
// For canvas (Y down), if lightVecY is positive (light is from "up" in math coords),
// then the y-offset for the neighbor pixel should be negative to look "up" in canvas.
const neighborX = Math.round(x - lightVecX * depth);
const neighborY = Math.round(y - lightVecY * depth); // Correct for canvas Y-down
const clampedNeighborX = Math.max(0, Math.min(width - 1, neighborX));
const clampedNeighborY = Math.max(0, Math.min(height - 1, neighborY));
const neighborIndex = clampedNeighborY * width + clampedNeighborX;
const neighborGray = grayscaleValues[neighborIndex];
// Emboss formula: current - neighbor + bias (128)
// This creates highlights if current > neighbor (surface facing light)
// and shadows if current < neighbor.
let embossedValue = currentGray - neighborGray + 128;
embossedValue = Math.max(0, Math.min(255, embossedValue)); // Clamp to 0-255
embossedGrayscale[currentIndex] = embossedValue;
}
}
// 4. Prepare final output canvas
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outCtx = outputCanvas.getContext('2d', { willReadFrequently: true });
// Fill with background color first
outCtx.fillStyle = backgroundColorStr; // Use string directly for fillStyle
outCtx.fillRect(0, 0, width, height);
const finalImageData = outCtx.getImageData(0, 0, width, height);
const finalData = finalImageData.data;
// 5. Combine: colorize embossed data with imprintColor and blend onto background using original alpha
for (let i = 0; i < finalData.length; i += 4) {
const flatIndex = i / 4;
const originalPixelAlpha = originalAlphas[flatIndex];
// Define a threshold for what's considered part of the fossil based on original alpha
const alphaThreshold = 20;
if (originalPixelAlpha > alphaThreshold) {
const shadingFactor = embossedGrayscale[flatIndex] / 255.0; // Normalized 0-1
// Modulate the imprintColor by the shadingFactor
const fossilR = imprintColor.r * shadingFactor;
const fossilG = imprintColor.g * shadingFactor;
const fossilB = imprintColor.b * shadingFactor;
// Alpha blending: C_out = C_fossil * alpha_orig + C_bg * (1 - alpha_orig)
const blendAlpha = originalPixelAlpha / 255.0;
finalData[i] = fossilR * blendAlpha + backgroundColor.r * (1 - blendAlpha);
finalData[i + 1] = fossilG * blendAlpha + backgroundColor.g * (1 - blendAlpha);
finalData[i + 2] = fossilB * blendAlpha + backgroundColor.b * (1 - blendAlpha);
finalData[i + 3] = 255; // Final output is fully opaque on its background
} else {
// Pixel is not part of the fossil (transparent in original or below threshold),
// so it remains the background color.
finalData[i] = backgroundColor.r;
finalData[i + 1] = backgroundColor.g;
finalData[i + 2] = backgroundColor.b;
finalData[i + 3] = 255;
}
}
outCtx.putImageData(finalImageData, 0, 0);
return outputCanvas;
}
Apply Changes