You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, gridColor = 'rgba(70, 170, 220, 0.6)', backgroundColor = '#0A101A', gridSpacing = 25, gridLineWidth = 0.5, sketchLineColorStr = 'rgba(220, 240, 255, 0.9)', blurRadius = 2, sketchThreshold = 100) {
// Helper function to parse color string to an RGBA object
function parseColorToRGBA(colorStr) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
if (!ctx) { // Fallback if context creation fails
// Try to guess some common colors if canvas fails, though unlikely
if (colorStr === 'transparent') return { r:0, g:0, b:0, a:0 };
return { r: 0, g: 0, b: 0, a: 1 }; // Default to black
}
ctx.fillStyle = colorStr;
ctx.fillRect(0, 0, 1, 1);
const data = ctx.getImageData(0, 0, 1, 1).data;
return { r: data[0], g: data[1], b: data[2], a: data[3] / 255.0 };
}
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
if (w === 0 || h === 0) {
// Handle cases where the image has no dimensions (e.g., not loaded yet)
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1; // Return a minimal canvas
emptyCanvas.height = 1;
return emptyCanvas;
}
const parsedSketchLineColor = parseColorToRGBA(sketchLineColorStr);
// Create temporary canvases
// Use { willReadFrequently: true } for potential performance hint if supported
const canvas1 = document.createElement('canvas');
canvas1.width = w;
canvas1.height = h;
const ctx1 = canvas1.getContext('2d', { willReadFrequently: true });
const canvas2 = document.createElement('canvas');
canvas2.width = w;
canvas2.height = h;
const ctx2 = canvas2.getContext('2d', { willReadFrequently: true });
// 1. Draw original image and convert to grayscale on canvas1
ctx1.drawImage(originalImg, 0, 0, w, h);
const imgData = ctx1.getImageData(0, 0, w, h);
const data = imgData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Standard luminance calculation
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = data[i + 1] = data[i + 2] = gray;
// Alpha (data[i+3]) is preserved
}
ctx1.putImageData(imgData, 0, 0); // canvas1 now has grayscale image
// 2. Create blurred-inverted-grayscale version on canvas2
// canvas1 (grayscale) is drawn to canvas2, applying filter in the process.
ctx2.filter = `invert(100%) blur(${blurRadius}px)`;
ctx2.drawImage(canvas1, 0, 0, w, h); // canvas2 now has blurred-inverted-grayscale image
// 3. Color Dodge blend
// Get data from grayscale (canvas1) and blurred-inverted (canvas2)
const grayscalePixelData = ctx1.getImageData(0, 0, w, h).data;
const blurredInvertedPixelData = ctx2.getImageData(0, 0, w, h).data;
const sketchImageData = ctx1.createImageData(w, h);
const sketchData = sketchImageData.data;
for (let i = 0; i < sketchData.length; i += 4) {
const grayVal = grayscalePixelData[i]; // R channel is fine, it's grayscale
const blurInvVal = blurredInvertedPixelData[i]; // R channel
const originalAlpha = grayscalePixelData[i+3]; // Alpha from original grayscale image
let dodgeVal;
if (blurInvVal === 255) { // If the "blend" (blurred-inverted) is white
dodgeVal = blurInvVal; // Result is white
} else if (grayVal === 0) { // if the "base" (grayscale) is black
dodgeVal = 0; // Result is black
}
else {
// Color Dodge formula: Result = BaseColor / (1 - BlendColor/255)
// Which is equivalent to: (BaseColor * 255) / (255 - BlendColor)
dodgeVal = (grayVal * 255.0) / (255.0 - blurInvVal);
dodgeVal = Math.min(255, dodgeVal); // Clamp to 255
}
sketchData[i] = sketchData[i+1] = sketchData[i+2] = dodgeVal;
sketchData[i+3] = originalAlpha; // Preserve alpha from the grayscale image
}
ctx1.putImageData(sketchImageData, 0, 0); // canvas1 now has "sketch" (dark lines, light bg)
// 4. Process sketch: make lines colored according to sketchLineColor, rest transparent
const finalLinesImageData = ctx1.getImageData(0, 0, w, h);
const flData = finalLinesImageData.data;
for (let i = 0; i < flData.length; i += 4) {
const intensity = flData[i]; // R channel of sketch (it's grayscale)
const alpha = flData[i+3]; // Current alpha of sketch pixel
if (alpha === 0) continue; // Skip if already fully transparent
// If pixel intensity is below threshold, it's a line
if (intensity < sketchThreshold) {
flData[i] = parsedSketchLineColor.r;
flData[i+1] = parsedSketchLineColor.g;
flData[i+2] = parsedSketchLineColor.b;
flData[i+3] = parsedSketchLineColor.a * 255; // Apply sketchLineColor alpha
} else { // Lighter background parts become transparent
flData[i+3] = 0;
}
}
ctx1.putImageData(finalLinesImageData, 0, 0); // canvas1 now has colored lines on transparent bg
// 5. Final Composition
const finalCanvas = document.createElement('canvas');
finalCanvas.width = w;
finalCanvas.height = h;
const finalCtx = finalCanvas.getContext('2d');
// Draw background color
finalCtx.fillStyle = backgroundColor;
finalCtx.fillRect(0, 0, w, h);
// Draw grid
if (gridSpacing > 0 && gridLineWidth > 0) {
finalCtx.strokeStyle = gridColor;
finalCtx.lineWidth = gridLineWidth;
finalCtx.beginPath();
for (let x = gridSpacing; x < w; x += gridSpacing) {
finalCtx.moveTo(x + gridLineWidth / 2, 0); // Offset by half line width for sharper lines
finalCtx.lineTo(x + gridLineWidth / 2, h);
}
for (let y = gridSpacing; y < h; y += gridSpacing) {
finalCtx.moveTo(0, y + gridLineWidth / 2);
finalCtx.lineTo(w, y + gridLineWidth / 2);
}
finalCtx.stroke();
}
// Draw the processed lines (from canvas1) onto the final canvas
finalCtx.drawImage(canvas1, 0, 0, w, h);
return finalCanvas;
}
Apply Changes