You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
overlayText = "PROJECT LOG: CLASSIFIED\nSUBJECT: SPECIMEN-Z\nSTATUS: UNSTABLE\nOBSERVATIONS: ERRATIC BEHAVIOR, INCREASED AGGRESSION, UNEXPECTED MUTATIONS NOTED...",
textColor = "rgba(40, 30, 20, 0.9)", // Dark, slightly desaturated brown
textFontFamily = "Permanent Marker", // Desired font
vintageIntensity = 0.65,
grungeIntensity = 0.55
) {
// Dynamically load the font if FontFace API is available
const FONT_UNIQUE_NAME = textFontFamily + "_MadScientistLoaded"; // Unique name for FontFace instance
const FONT_URL = "https://fonts.gstatic.com/s/permanentmarker/v17/Fh4uPib9Iyv2ucM6pGQMWimMp004La2Cfw.woff2"; // URL for "Permanent Marker"
let actualFontToUse = textFontFamily; // This will be updated upon successful load or fallback
if (typeof FontFace !== 'undefined') {
try {
// Check if already loaded to avoid re-adding
let fontLoaded = false;
for (const font of document.fonts) {
if (font.family === FONT_UNIQUE_NAME) {
fontLoaded = true;
break;
}
}
if (!fontLoaded) {
const font = new FontFace(FONT_UNIQUE_NAME, `url(${FONT_URL})`);
await font.load();
document.fonts.add(font);
}
actualFontToUse = FONT_UNIQUE_NAME;
} catch (e) {
console.warn(`Font ${textFontFamily} from ${FONT_URL} failed to load:`, e);
actualFontToUse = "cursive, sans-serif"; // Fallback
}
} else {
console.warn("FontFace API not supported. Using fallback font.");
actualFontToUse = "cursive, sans-serif"; // Fallback for very old browsers
}
const w = originalImg.naturalWidth;
const h = originalImg.naturalHeight;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
// 1. Fill with paper color background
ctx.fillStyle = '#f3efde'; // Aged paper, slightly yellowish-beige
ctx.fillRect(0, 0, w, h);
// 2. Create a temporary canvas for processing the image with filters
const imageProcessingCanvas = document.createElement('canvas');
imageProcessingCanvas.width = w;
imageProcessingCanvas.height = h;
const imgProcCtx = imageProcessingCanvas.getContext('2d');
imgProcCtx.drawImage(originalImg, 0, 0, w, h); // Draw original image
// 3. Apply vintage filter effect to the image on the temporary canvas
if (vintageIntensity > 0) {
const sepiaVal = vintageIntensity;
const brightnessVal = 1 - (vintageIntensity * 0.12); // e.g., 0.65 -> 0.922
const contrastVal = 1 + (vintageIntensity * 0.18); // e.g., 0.65 -> 1.117
let filterOperations = [];
if (sepiaVal > 0.01) filterOperations.push(`sepia(${sepiaVal})`);
if (brightnessVal < 0.995) filterOperations.push(`brightness(${brightnessVal})`);
if (contrastVal > 1.005) filterOperations.push(`contrast(${contrastVal})`);
if (filterOperations.length > 0) {
imgProcCtx.filter = filterOperations.join(' ');
// To "bake" the filter: draw the canvas content onto itself while filter is active
// This requires copying to another temporary element or drawing image from original source
const tempFilteredImage = document.createElement('canvas');
tempFilteredImage.width = w;
tempFilteredImage.height = h;
const tempFilteredCtx = tempFilteredImage.getContext('2d');
tempFilteredCtx.drawImage(imageProcessingCanvas, 0, 0); // Copy current (unfiltered) state
imgProcCtx.clearRect(0,0,w,h); // Clear the processing canvas
imgProcCtx.filter = filterOperations.join(' '); // Re-apply filter
imgProcCtx.drawImage(tempFilteredImage, 0, 0); // Draw with filter
imgProcCtx.filter = 'none'; // Reset filter on the processing context
}
}
// 4. Draw the (potentially filtered) image onto the main canvas
ctx.drawImage(imageProcessingCanvas, 0, 0, w, h);
// 5. Add grunge/stains
if (grungeIntensity > 0) {
const numStainsTarget = 5 + grungeIntensity * 25; // Base: 5 to 30 stains
// Scale number of stains roughly with image area, sqrt to moderate scaling
const numStains = Math.max(1, Math.floor(numStainsTarget * Math.sqrt(w * h / (600*600)) ));
ctx.globalCompositeOperation = 'multiply'; // Stains blend well with multiply
for (let i = 0; i < numStains; i++) {
const stainX = Math.random() * w;
const stainY = Math.random() * h;
const diagonal = Math.sqrt(w*w + h*h); // Use diagonal for relative sizing
const baseRadius = (Math.random() * 0.025 + 0.005) * diagonal; // 0.5% to 3% of diagonal
const stainOpacity = (Math.random() * 0.3 + 0.05) * grungeIntensity; // Max ~0.35 * intensity
const r_val = 50 + Math.random() * 60; // Dark Brown/Greyish: 50-110
const g_val = Math.max(20, r_val - (25 + Math.random() * 35)); // Ensure G is lower for brownish
const b_val = Math.max(10, g_val - (20 + Math.random() * 25)); // Ensure B is lower
const stainColor = `rgba(${Math.floor(r_val)}, ${Math.floor(g_val)}, ${Math.floor(b_val)}, ${stainOpacity})`;
const splotchCount = 1 + Math.floor(Math.random() * 3); // 1 to 3 splotches form a single "stain"
for (let j = 0; j < splotchCount; j++) {
const splotchRadius = baseRadius * (0.5 + Math.random()); // Vary individual splotch size
const offsetX = (Math.random() - 0.5) * baseRadius * 1.2; // Allow splotches to spread
const offsetY = (Math.random() - 0.5) * baseRadius * 1.2;
ctx.beginPath();
ctx.arc(stainX + offsetX, stainY + offsetY, splotchRadius, 0, Math.PI * 2);
ctx.fillStyle = stainColor;
ctx.fill();
}
}
ctx.globalCompositeOperation = 'source-over'; // Reset composite mode for text
}
// 6. Overlay handwritten-style text
if (overlayText.trim() !== "") {
const lines = overlayText.split('\n');
// Font size responsive to image dimensions, with min/max caps
const baseFontSize = Math.max(14, Math.min(h * 0.035, w * 0.05, 36));
ctx.fillStyle = textColor;
ctx.textBaseline = 'middle'; // Easier for vertical alignment when rotating
let currentY = h * 0.07; // Starting Y position for text block
const marginX = w * 0.05; // Side margins
const lineSpacingFactor = 1.6; // Relative line height
for (const line of lines) {
if (currentY > h - (marginX + baseFontSize)) break; // Stop if text overflows vertically
const fontSizeVariation = 0.9 + Math.random() * 0.2; // 90% to 110% of base
const effectiveFontSize = baseFontSize * fontSizeVariation;
ctx.font = `${effectiveFontSize}px "${actualFontToUse}"`;
const textMetrics = ctx.measureText(line);
const textWidth = textMetrics.width;
let lineX;
// Basic text alignment: try to center short lines, left-align longer ones
if (textWidth < w - 2 * marginX) { // If line fits within margins
lineX = marginX + (w - 2 * marginX - textWidth) / 2; // Centered within margins
lineX += (Math.random() - 0.5) * effectiveFontSize * 0.2; // Add jitter to centered position
} else {
lineX = marginX + (Math.random() - 0.5) * effectiveFontSize * 0.1; // Left-aligned with jitter
}
lineX = Math.max(marginX * 0.8, Math.min(lineX, w - marginX - textWidth)); // Keep text on pagebounds
const yJitter = (Math.random() - 0.5) * effectiveFontSize * 0.1; // Baseline jitter
const angle = (Math.random() - 0.5) * 0.035; // Rotation: +/- ~2 degrees
// Calculate center of the text line for rotation
const rotateCenterX = lineX + textWidth / 2;
const rotateCenterY = currentY + yJitter + effectiveFontSize / 2;
ctx.save();
ctx.translate(rotateCenterX, rotateCenterY);
ctx.rotate(angle);
ctx.fillText(line, -textWidth / 2, 0); // Draw text centered around new (0,0)
ctx.restore();
// Advance Y for next line with some randomness in spacing
currentY += effectiveFontSize * lineSpacingFactor * (0.9 + Math.random() * 0.2);
}
}
return canvas;
}
Apply Changes