You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, stitchCount = 4, stitchColor = '#3d2b1f', tintColor = 'rgba(50, 100, 50, 0.2)', vignetteIntensity = 0.85, grainAmount = 25) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const w = originalImg.naturalWidth;
const h = originalImg.naturalHeight;
canvas.width = w;
canvas.height = h;
// 1. Draw the initial image
ctx.drawImage(originalImg, 0, 0, w, h);
// 2. Apply filters: Grayscale, Tint, and Grain (Pixel manipulation)
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
// Helper to parse the RGBA tint color string
const parseRgba = (colorStr) => {
const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (!match) return { r: 0, g: 0, b: 0, a: 0 };
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: match[4] !== undefined ? parseFloat(match[4]) : 1
};
};
const tint = parseRgba(tintColor);
for (let i = 0; i < data.length; i += 4) {
// Grayscale using the luminosity method
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
// Apply tint by blending the tint color over the grayscale value
const alpha = tint.a;
let finalR = gray * (1 - alpha) + tint.r * alpha;
let finalG = gray * (1 - alpha) + tint.g * alpha;
let finalB = gray * (1 - alpha) + tint.b * alpha;
// Add film grain
const noise = (Math.random() - 0.5) * grainAmount;
finalR += noise;
finalG += noise;
finalB += noise;
// Clamp values to 0-255 range
data[i] = Math.max(0, Math.min(255, finalR));
data[i + 1] = Math.max(0, Math.min(255, finalG));
data[i + 2] = Math.max(0, Math.min(255, finalB));
}
ctx.putImageData(imageData, 0, 0);
// 3. Draw stitches
ctx.strokeStyle = stitchColor;
ctx.lineWidth = Math.max(2, w * 0.004);
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
for (let i = 0; i < stitchCount; i++) {
// Random start and end points for the main stitch line
const x1 = Math.random() * w * 0.8 + w * 0.1;
const y1 = Math.random() * h * 0.8 + h * 0.1;
const x2 = x1 + (Math.random() - 0.5) * w * 0.4;
const y2 = y1 + (Math.random() - 0.5) * h * 0.4;
ctx.beginPath();
ctx.moveTo(x1, y1);
// Use a quadratic curve to make the line less straight
const midX = (x1 + x2) / 2 + (Math.random() - 0.5) * 40;
const midY = (y1 + y2) / 2 + (Math.random() - 0.5) * 40;
ctx.quadraticCurveTo(midX, midY, x2, y2);
ctx.stroke();
// Draw the cross-stitches
const stitchLength = Math.hypot(x2 - x1, y2 - y1);
const numCrossStitches = Math.floor(stitchLength / (w * 0.04));
const dx = (x2 - x1) / stitchLength;
const dy = (y2 - y1) / stitchLength;
const crossLineWidth = ctx.lineWidth * 0.75;
for (let j = 1; j < numCrossStitches; j++) {
const p = j / numCrossStitches;
// Get point along the curved line (approximation)
const t = p;
const px = Math.pow(1 - t, 2) * x1 + 2 * (1 - t) * t * midX + Math.pow(t, 2) * x2;
const py = Math.pow(1 - t, 2) * y1 + 2 * (1 - t) * t * midY + Math.pow(t, 2) * y2;
const crossSize = Math.max(6, w * 0.015);
ctx.save();
ctx.lineWidth = crossLineWidth;
ctx.beginPath();
// Perpendicular line
ctx.moveTo(px - dy * crossSize, py + dx * crossSize);
ctx.lineTo(px + dy * crossSize, py - dx * crossSize);
ctx.stroke();
ctx.restore();
}
}
// Reset shadow for next drawings
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// 4. Draw vignette
const centerX = w / 2;
const centerY = h / 2;
const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
const gradient = ctx.createRadialGradient(centerX, centerY, outerRadius * (1.1 - vignetteIntensity), centerX, centerY, outerRadius);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(0.5, 'rgba(0,0,0,0.1)');
gradient.addColorStop(1, 'rgba(0,0,0,0.8)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
return canvas;
}
Apply Changes