You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg,
padding = 50,
sepiaIntensity = 0.7,
pageColor = "rgb(245, 230, 200)", // Parchment like
edgeRoughness = 15,
edgeDarkeningColor = "rgba(101, 67, 33, 0.5)", // Sienna like
numStains = 5,
stainBaseColor = "rgb(160, 110, 70)", // Brownish
textureIntensity = 0.5 // Range 0-1 for texture dot density
) {
const paperBaseWidth = originalImg.width + 2 * padding;
const paperBaseHeight = originalImg.height + 2 * padding;
const canvasMargin = Math.max(20, edgeRoughness * 1.5); // Ensure enough margin for effects
const canvas = document.createElement('canvas');
canvas.width = paperBaseWidth + 2 * canvasMargin;
canvas.height = paperBaseHeight + 2 * canvasMargin;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const paperX = canvasMargin;
const paperY = canvasMargin;
// Helper: Parse RGB/A or Hex color string
function parseColor(colorStr) {
if (!colorStr) return { r: 0, g: 0, b: 0, a: 0 }; // transparent black if no color
let r, g, b, a = 1;
const input = String(colorStr).trim().toLowerCase();
if (input.startsWith('#')) {
if (input.length === 7) { // #RRGGBB
r = parseInt(input.substring(1, 3), 16);
g = parseInt(input.substring(3, 5), 16);
b = parseInt(input.substring(5, 7), 16);
} else if (input.length === 4) { // #RGB
r = parseInt(input.substring(1, 2) + input.substring(1, 2), 16);
g = parseInt(input.substring(2, 3) + input.substring(2, 3), 16);
b = parseInt(input.substring(3, 4) + input.substring(3, 4), 16);
} else return { r: 0, g: 0, b: 0, a: 0 };
} else if (input.startsWith('rgb')) {
const partsMatch = input.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (!partsMatch) return { r: 0, g: 0, b: 0, a: 0 };
r = parseInt(partsMatch[1]);
g = parseInt(partsMatch[2]);
b = parseInt(partsMatch[3]);
if (partsMatch[4] !== undefined) {
a = parseFloat(partsMatch[4]);
}
} else {
// Simple named colors (extend if needed)
const namedColors = { "sienna": {r:160,g:82,b:45}, "brown": {r:165,g:42,b:42} };
if (namedColors[input]) return {...namedColors[input], a:1};
console.warn("Unsupported color format:", colorStr, "Defaulting to transparent black.");
return { r: 0, g: 0, b: 0, a: 0 };
}
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) return { r: 0, g: 0, b: 0, a: 0 };
return { r, g, b, a };
}
// Helper: Generate a rough path for the paper
function createRoughPath(x, y, width, height, roughness, segmentsPerEdge = 20) {
const path = new Path2D();
const points = [];
const r = Math.max(1, roughness); // Ensure roughness is at least 1 to avoid flat lines if 0
// Top-left corner
points.push({x: x + (Math.random()-0.5) * r, y: y + (Math.random()-0.5) * r});
// Top edge
for (let i = 1; i <= segmentsPerEdge; i++) {
const t = i / segmentsPerEdge;
points.push({
x: x + t * width + (Math.random() - 0.5) * r * 0.8,
y: y + (Math.random() - 0.5) * r
});
}
// Top-right corner
points.push({x: x + width + (Math.random()-0.5) * r, y: y + (Math.random()-0.5) * r});
// Right edge
for (let i = 1; i <= segmentsPerEdge; i++) {
const t = i / segmentsPerEdge;
points.push({
x: x + width + (Math.random() - 0.5) * r,
y: y + t * height + (Math.random() - 0.5) * r * 0.8
});
}
// Bottom-right corner
points.push({x: x + width + (Math.random()-0.5) * r, y: y + height + (Math.random()-0.5) * r});
// Bottom edge
for (let i = 1; i <= segmentsPerEdge; i++) {
const t = i / segmentsPerEdge;
points.push({
x: x + width - (t * width) + (Math.random() - 0.5) * r * 0.8,
y: y + height + (Math.random() - 0.5) * r
});
}
// Bottom-left corner
points.push({x: x + (Math.random()-0.5) * r, y: y + height + (Math.random()-0.5) * r});
// Left edge
for (let i = 1; i <= segmentsPerEdge; i++) {
const t = i / segmentsPerEdge;
points.push({
x: x + (Math.random() - 0.5) * r,
y: y + height - (t * height) + (Math.random() - 0.5) * r * 0.8
});
}
path.moveTo(points[0].x, points[0].y);
for(let i = 1; i < points.length; i++) {
path.lineTo(points[i].x, points[i].y);
}
path.closePath();
return path;
}
const paperPath = createRoughPath(paperX, paperY, paperBaseWidth, paperBaseHeight, edgeRoughness);
// 1. Optional: Shadow for the paper
if (edgeRoughness > 0) {
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.35)';
ctx.shadowBlur = edgeRoughness * 1.2;
ctx.shadowOffsetX = edgeRoughness * 0.25;
ctx.shadowOffsetY = edgeRoughness * 0.25;
const parsedPageColorForShadow = parseColor(pageColor);
ctx.fillStyle = `rgba(${parsedPageColorForShadow.r},${parsedPageColorForShadow.g},${parsedPageColorForShadow.b},1)`;
ctx.fill(paperPath);
ctx.restore();
}
// 2. Fill paper with base color
const parsedPageColor = parseColor(pageColor);
ctx.fillStyle = `rgb(${parsedPageColor.r},${parsedPageColor.g},${parsedPageColor.b})`;
ctx.fill(paperPath);
// 3. Clip to paper path for internal content
ctx.save();
ctx.clip(paperPath);
// 3a. Paper Texture (Noise)
if (parsedPageColor && textureIntensity > 0) {
const numDots = Math.floor(paperBaseWidth * paperBaseHeight * 0.001 * textureIntensity);
const baseDotAlpha = 0.08;
for (let i = 0; i < numDots ; i++) {
const x = paperX + Math.random() * paperBaseWidth;
const y = paperY + Math.random() * paperBaseHeight;
const r_off = (Math.random() - 0.5) * 35;
const g_off = (Math.random() - 0.5) * 35;
const b_off = (Math.random() - 0.5) * 35;
const r = Math.max(0, Math.min(255, parsedPageColor.r + r_off));
const g = Math.max(0, Math.min(255, parsedPageColor.g + g_off));
const b = Math.max(0, Math.min(255, parsedPageColor.b + b_off));
const dotAlphaVariation = (Math.random() * 0.5 + 0.5); // 0.5 to 1.0
ctx.fillStyle = `rgba(${r},${g},${b},${baseDotAlpha * dotAlphaVariation})`;
ctx.beginPath();
ctx.arc(x, y, Math.random() * 1.5 + 0.5, 0, Math.PI * 2);
ctx.fill();
}
}
// 3b. Stains
const rgbStainBase = parseColor(stainBaseColor);
if (rgbStainBase && numStains > 0) {
for (let k = 0; k < numStains; k++) {
const stainCX = paperX + padding * 0.3 + Math.random() * (paperBaseWidth - padding * 0.6);
const stainCY = paperY + padding * 0.3 + Math.random() * (paperBaseHeight - padding * 0.6);
const stainMaxR = (Math.random() * 0.12 + 0.05) * Math.min(paperBaseWidth, paperBaseHeight);
const numSegments = Math.floor(Math.random() * 4) + 3;
for (let i = 0; i < numSegments; i++) {
const segRadius = Math.random() * stainMaxR * 0.8 + stainMaxR * 0.2;
const segX = stainCX + (Math.random() - 0.5) * stainMaxR * 0.8;
const segY = stainCY + (Math.random() - 0.5) * stainMaxR * 0.8;
const grad = ctx.createRadialGradient(segX, segY, segRadius * 0.05, segX, segY, segRadius);
const alphaCenter = Math.random() * 0.15 + 0.05;
grad.addColorStop(0, `rgba(${rgbStainBase.r}, ${rgbStainBase.g}, ${rgbStainBase.b}, ${alphaCenter})`);
grad.addColorStop(0.6, `rgba(${rgbStainBase.r}, ${rgbStainBase.g}, ${rgbStainBase.b}, ${alphaCenter * 0.35})`);
grad.addColorStop(1, `rgba(${rgbStainBase.r}, ${rgbStainBase.g}, ${rgbStainBase.b}, 0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(segX, segY,
segRadius * (0.7 + Math.random() * 0.6),
segRadius * (0.7 + Math.random() * 0.6),
Math.random() * Math.PI * 2, 0, Math.PI * 2);
ctx.fill();
}
}
}
// 3c. Draw the Original Image
ctx.save();
if (sepiaIntensity > 0) {
ctx.filter = `sepia(${Math.min(1, Math.max(0, sepiaIntensity))})`; // Clamp between 0 and 1
}
const imgDrawX = paperX + padding;
const imgDrawY = paperY + padding;
const imgDrawWidth = originalImg.width;
const imgDrawHeight = originalImg.height;
ctx.drawImage(originalImg, imgDrawX, imgDrawY, imgDrawWidth, imgDrawHeight);
ctx.restore();
ctx.restore(); // Remove clipping
// 4. Darken/Rough up Edges of the Paper
if (edgeRoughness > 0) {
const parsedEdgeColor = parseColor(edgeDarkeningColor);
if (parsedEdgeColor && parsedEdgeColor.a > 0) { // Only draw if color is somewhat visible
ctx.strokeStyle = `rgba(${parsedEdgeColor.r},${parsedEdgeColor.g},${parsedEdgeColor.b},${parsedEdgeColor.a})`;
ctx.lineWidth = Math.max(0.5, edgeRoughness * 0.1 + Math.random() * edgeRoughness * 0.15);
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke(paperPath);
// Add a second, slightly offset and more transparent stroke for depth
ctx.lineWidth = Math.max(0.5, edgeRoughness * 0.2 + Math.random() * edgeRoughness * 0.2);
ctx.strokeStyle = `rgba(${parsedEdgeColor.r},${parsedEdgeColor.g},${parsedEdgeColor.b},${parsedEdgeColor.a * 0.6})`;
ctx.save();
ctx.translate((Math.random()-0.5) * edgeRoughness * 0.05, (Math.random()-0.5) * edgeRoughness * 0.05);
ctx.stroke(paperPath);
ctx.restore();
}
}
return canvas;
}
Apply Changes