You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, bleedIntensity = 15, saturation = 1.4, addPaperTexture = 1, addVignette = 1) {
// Ensure accurate sizing from the original image element
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Scale effects proportionally to image size
const sizeMax = Math.max(width, height);
const baseFreq = 15 / sizeMax;
const blurLevel = 1.5 * (sizeMax / 1000);
const dispScale = Number(bleedIntensity) * (sizeMax / 1000);
const sat = Number(saturation);
// Generate unique ID for SVG filter to avoid collisions
const filterId = "watercolor_" + Math.random().toString(36).substring(2, 9);
// SVG Filter definition for watercolor spreading, bleeding, and enhanced color properties
const svgStr = `
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position: absolute; width: 0; height: 0;">
<filter id="${filterId}" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<!-- Soften image -->
<feGaussianBlur in="SourceGraphic" stdDeviation="${blurLevel}" result="blur1" />
<!-- Primary displacement for structural wobble (the watercolor wet bleed) -->
<feTurbulence type="fractalNoise" baseFrequency="${baseFreq}" numOctaves="4" seed="5" result="turbulence1" />
<feDisplacementMap in="blur1" in2="turbulence1" scale="${dispScale}" xChannelSelector="R" yChannelSelector="G" result="displaced1" />
<!-- Secondary displacement for localized micro-texture irregularities -->
<feTurbulence type="fractalNoise" baseFrequency="${baseFreq * 1.5}" numOctaves="2" seed="10" result="turbulence2" />
<feDisplacementMap in="displaced1" in2="turbulence2" scale="${dispScale * 0.4}" xChannelSelector="R" yChannelSelector="B" result="displaced2" />
<!-- Boost vibrancy/saturation -->
<feColorMatrix type="saturate" values="${sat}" in="displaced2" result="saturated" />
<!-- Slight brightness enhancement to mimic translucent watercolor pigment -->
<feComponentTransfer in="saturated" result="final">
<feFuncR type="linear" slope="1.05"/>
<feFuncG type="linear" slope="1.05"/>
<feFuncB type="linear" slope="1.05"/>
</feComponentTransfer>
</filter>
</svg>
`;
const container = document.createElement('div');
container.innerHTML = svgStr;
document.body.appendChild(container);
try {
// Yield to browser rendering cycle to register SVG filter in the DOM
await new Promise(resolve => requestAnimationFrame(resolve));
// 1. Establish the watercolor paper base tone (Off-white / Warm)
ctx.fillStyle = "#F5F3EE";
ctx.fillRect(0, 0, width, height);
// 2. Draw image with native canvas filter linked to SVG ID
ctx.filter = `url(#${filterId})`;
ctx.drawImage(originalImg, 0, 0, width, height);
// Clear out filter so overlays display cleanly
ctx.filter = "none";
} finally {
// Safe DOM cleanup
if (container.parentNode) {
document.body.removeChild(container);
}
}
// 3. Add procedural paper grain texture
if (Number(addPaperTexture) === 1 || String(addPaperTexture).toLowerCase() === "true") {
const tileSize = 512;
const tCanvas = document.createElement('canvas');
tCanvas.width = tileSize;
tCanvas.height = tileSize;
const tCtx = tCanvas.getContext('2d');
const imgData = new ImageData(tileSize, tileSize);
const data = imgData.data;
// Generate organic paper monochrome variations mapped to warm subtle tints
for (let i = 0; i < data.length; i += 4) {
const val = 235 + (Math.random() * 40 - 20); // Fluctuates 215-255
data[i] = val; // Red channel
data[i + 1] = val - 1; // Green channel (slight warm bias)
data[i + 2] = val - 4; // Blue channel (lowering blue creates warm cream/yellow)
data[i + 3] = 255; // Full alpha opacity
}
tCtx.putImageData(imgData, 0, 0);
// Map tiling pattern so the rendering handles huge resolutions instantly
const pattern = ctx.createPattern(tCanvas, 'repeat');
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, width, height);
// Restore context operation
ctx.globalCompositeOperation = "source-over";
}
// 4. Apply wash borders / vignette (Uneven fade to edge paper color)
if (Number(addVignette) === 1 || String(addVignette).toLowerCase() === "true") {
const cx = width / 2;
const cy = height / 2;
const radiusOut = sizeMax * 0.65;
const radiusIn = Math.min(width, height) * 0.35;
const grad = ctx.createRadialGradient(cx, cy, radiusIn, cx, cy, radiusOut);
grad.addColorStop(0, "rgba(245, 243, 238, 0)"); // Fully transparent in focal center
grad.addColorStop(0.65, "rgba(245, 243, 238, 0.4)"); // Starts fading out softly
grad.addColorStop(1, "rgba(245, 243, 238, 1)"); // Merges solidly with outer paper
ctx.fillStyle = grad;
ctx.fillRect(0, 0, width, height);
}
return canvas;
}
Apply Changes