You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, sepiaIntensity = 1.0, textureAmount = 0.5, stainColor = 'rgba(100, 80, 50, 0.1)', annotationDensity = 3) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Determine canvas size - add padding for notebook effect
// Adjusted padding for better aesthetics and min value
const padding = Math.max(25, Math.min(originalImg.width, originalImg.height) * 0.12);
canvas.width = originalImg.width + 2 * padding;
canvas.height = originalImg.height + 2 * padding;
// --- 1. Parchment Background ---
const basePaperColor = { r: 240, g: 230, b: 210 }; // A light linen/beige color
ctx.fillStyle = `rgb(${basePaperColor.r}, ${basePaperColor.g}, ${basePaperColor.b})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add noise for texture
if (textureAmount > 0) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
// Max color variation; higher textureAmount gives more pronounced noise
const noiseStrength = 40 * Math.max(0, Math.min(1, textureAmount));
for (let i = 0; i < pixels.length; i += 4) {
const randFactor = (Math.random() - 0.5) * noiseStrength;
pixels[i] = Math.max(0, Math.min(255, pixels[i] + randFactor)); // Red
pixels[i + 1] = Math.max(0, Math.min(255, pixels[i + 1] + randFactor)); // Green
pixels[i + 2] = Math.max(0, Math.min(255, pixels[i + 2] + randFactor)); // Blue
}
ctx.putImageData(imageData, 0, 0);
}
// Add "stains" to the paper
if (stainColor && stainColor.toLowerCase() !== 'none' && stainColor !== '' && textureAmount > 0.05) {
const numStains = Math.floor(Math.random() * 4 + 2); // 2 to 5 stains
for (let i = 0; i < numStains; i++) {
ctx.fillStyle = stainColor; // Uses the provided semi-transparent stain color
const stainX = Math.random() * canvas.width;
const stainY = Math.random() * canvas.height;
// Elliptical stains with random sizes and rotation
const stainRadiusX = (Math.random() * canvas.width / 7) + (canvas.width / 12);
const stainRadiusY = (Math.random() * canvas.height / 9) + (canvas.height / 15);
ctx.beginPath();
ctx.ellipse(stainX, stainY, stainRadiusX, stainRadiusY, Math.random() * Math.PI * 2, 0, Math.PI * 2);
ctx.fill();
}
}
// --- 2. Process and Draw the Image ---
// Use a temporary canvas for image manipulation (e.g., sepia)
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0, originalImg.width, originalImg.height);
// Apply sepia filter if intensity is greater than 0
if (sepiaIntensity > 0) {
const imgData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imgData.data;
const si = Math.max(0, Math.min(1, sepiaIntensity)); // Clamp intensity [0, 1]
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Standard sepia calculation
const sr = (r * 0.393) + (g * 0.769) + (b * 0.189);
const sg = (r * 0.349) + (g * 0.686) + (b * 0.168);
const sb = (r * 0.272) + (g * 0.534) + (b * 0.131);
// Mix original with sepia based on intensity
data[i] = Math.min(255, (sr * si) + (r * (1 - si)));
data[i + 1] = Math.min(255, (sg * si) + (g * (1 - si)));
data[i + 2] = Math.min(255, (sb * si) + (b * (1 - si)));
}
tempCtx.putImageData(imgData, 0, 0);
}
// Draw the processed image onto the main canvas with slight transparency to blend
ctx.globalAlpha = 0.92;
ctx.drawImage(tempCanvas, padding, padding, originalImg.width, originalImg.height);
ctx.globalAlpha = 1.0; // Reset global alpha
// Add a very subtle border around the "pasted" image
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 1;
ctx.strokeRect(padding - 0.5, padding - 0.5, originalImg.width + 1, originalImg.height + 1);
// --- 3. Annotations (Mirrored Text and Diagram Lines) ---
if (annotationDensity > 0) {
const inkColorBase = { r: 70, g: 50, b: 30 }; // Darker, more sepia-like ink
const inkOpacity = 0.45 + Math.min(0.35, sepiaIntensity * 0.4);
const inkColor = `rgba(${inkColorBase.r}, ${inkColorBase.g}, ${inkColorBase.b}, ${inkOpacity})`;
const fontSize = Math.max(10, Math.floor(padding / 4.8)); // Slightly smaller, more delicate font
const lineHeight = fontSize * 1.2;
// Helper function to add mirrored text blocks
const addMirroredTextLines = (xAnchor, yAnchor, baseNumLines, lineLengthMaxPercentage, mirrored = true) => {
const numLines = Math.max(1, Math.min(7, Math.floor(baseNumLines * (annotationDensity / 3))));
if (numLines === 0 || padding < 20) return; // Min padding for text
ctx.save();
ctx.fillStyle = inkColor;
ctx.font = `${fontSize}px 'Georgia', serif`; // Georgia for a classic feel
const charSet = "abcdefghijklmnopqrstuvwxyz"; // Da Vinci's notes used lowercase
const avgCharWidth = ctx.measureText("m").width; // Estimate average char width
const lineLengthMax = canvas.width * lineLengthMaxPercentage;
if (mirrored) {
ctx.translate(xAnchor, yAnchor); // Move origin to the text's logical right anchor
ctx.scale(-1, 1); // Mirror context horizontally
ctx.textAlign = 'right'; // Text will be right-aligned relative to new (0,0)
} else {
ctx.translate(xAnchor, yAnchor); // Move origin to the text's logical left anchor
ctx.textAlign = 'left';
}
for (let i = 0; i < numLines; i++) {
const currentLineYRelative = i * lineHeight;
let text = "";
// Vary line length for realism
const numCharsInLine = Math.floor(((Math.random() * 0.4) + 0.6) * (lineLengthMax / avgCharWidth));
for(let k=0; k < numCharsInLine; k++) {
text += charSet.charAt(Math.floor(Math.random() * charSet.length));
// Add spaces randomly
if (k > 0 && k < numCharsInLine -1 && Math.random() < 0.18 && text.slice(-1) !== ' ') {
text += " ";
}
}
if (text.endsWith(" ")) text = text.slice(0, -1); // Trim trailing space
ctx.fillText(text, 0, currentLineYRelative); // Draw relative to transformed origin
}
ctx.restore();
};
// Define text annotation locations
if (padding >= 20) {
// Top-left quadrant related annotation
addMirroredTextLines(
padding + originalImg.width * 0.03 + (canvas.width * 0.18), // xAnchor (right edge of text)
padding * 0.75, // yAnchor (top of text block)
2, // baseNumLines: scaled by annotationDensity
0.18, // lineLengthMaxPercentage of canvas.width
true
);
// Bottom-right quadrant related annotation
const brNumLines = Math.max(1, Math.min(7, Math.floor(3 * (annotationDensity / 3))));
const brTextBlockHeight = brNumLines * lineHeight;
addMirroredTextLines(
canvas.width - padding * 0.65, // xAnchor (right edge of text)
canvas.height - padding * 0.75 - brTextBlockHeight, // yAnchor (top of text block)
3,
0.22, // lineLengthMaxPercentage
true
);
}
// Add "diagrammatic" lines
const numDiagramLines = Math.floor(Math.random() * annotationDensity * 1.8 + annotationDensity * 0.6);
ctx.strokeStyle = inkColor;
ctx.lineWidth = Math.max(0.7, fontSize / 15); // Thin lines for diagrams
for (let i = 0; i < numDiagramLines; i++) {
ctx.beginPath();
let startX, startY;
// Start lines from various places: image edges or margins
if (Math.random() < 0.65) { // Start near/on image
startX = padding + Math.random() * originalImg.width;
startY = padding + Math.random() * originalImg.height;
} else { // Start from margins
startX = (Math.random() < 0.5 ? Math.random() * padding * 0.7 : canvas.width - Math.random() * padding * 0.7);
startY = (Math.random() < 0.5 ? Math.random() * padding * 0.7 : canvas.height - Math.random() * padding * 0.7);
}
// End points with some randomness to create varied lines
const endX = startX + (Math.random() - 0.5) * (originalImg.width / 2.2);
const endY = startY + (Math.random() - 0.5) * (originalImg.height / 2.2);
ctx.moveTo(startX, startY);
// Use bezier curves for more organic-looking lines
ctx.bezierCurveTo(
startX + (Math.random() - 0.5) * 60, startY + (Math.random() - 0.5) * 60,
endX + (Math.random() - 0.5) * 60, endY + (Math.random() - 0.5) * 60,
endX, endY
);
// Add a small, simple arrowhead to some lines
if (Math.random() < 0.35) {
// Approximate angle at the end of the curve for arrowhead direction
const lastControlX = endX + (Math.random() - 0.5) * 60; // (This is not perfect for Bezier end angle)
const lastControlY = endY + (Math.random() - 0.5) * 60; // One of the control points for the curve
// A simplified angle calculation
const angle = Math.atan2(endY - lastControlY, endX - lastControlX);
const arrowSize = Math.max(3.5, fontSize / 3);
ctx.moveTo(endX, endY); // Ensure path is at endX, endY before drawing arrow parts
ctx.lineTo(endX - arrowSize * Math.cos(angle - Math.PI / 8), endY - arrowSize * Math.sin(angle - Math.PI / 8));
ctx.moveTo(endX, endY);
ctx.lineTo(endX - arrowSize * Math.cos(angle + Math.PI / 8), endY - arrowSize * Math.sin(angle + Math.PI / 8));
}
ctx.stroke();
}
}
return canvas;
}
Apply Changes