You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, caseNumber = '1897-B', agentName = 'В. Васнецов', caseDate = '24.08.1897') {
/**
* Dynamically loads necessary Google Fonts that support Cyrillic.
* This ensures the "case file" aesthetic can be achieved.
* The function is async to wait for fonts to be ready before drawing text.
*/
const loadFonts = async () => {
const fonts = {
'Playfair Display SC': '700',
'Courier Prime': '400'
};
const fontFamilies = Object.keys(fonts);
const fontQuery = fontFamilies.map(family => `family=${family.replace(/ /g, '+')}:wght@${fonts[family]}`).join('&');
const fontApiUrl = `https://fonts.googleapis.com/css2?${fontQuery}&display=swap`;
const fontLinkId = 'google-fonts-gamayun-investigation';
if (!document.getElementById(fontLinkId)) {
const link = document.createElement('link');
link.id = fontLinkId;
link.rel = 'stylesheet';
link.href = fontApiUrl;
document.head.appendChild(link);
// Wait for the stylesheet to load before trying to use the fonts
await new Promise(resolve => { link.onload = resolve; });
}
// Wait for the browser to confirm the fonts are ready for rendering
await Promise.all(
fontFamilies.map(family => document.fonts.load(`12px "${family}"`))
);
};
await loadFonts();
// 1. CANVAS SETUP
const padding = { top: 100, right: 100, bottom: 150, left: 100 };
const canvas = document.createElement('canvas');
canvas.width = originalImg.width + padding.left + padding.right;
canvas.height = originalImg.height + padding.top + padding.bottom;
const ctx = canvas.getContext('2d');
// 2. CREATE BACKGROUND (OLD PARCHMENT PAPER)
// Base color
ctx.fillStyle = '#f4eeda';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add subtle noise/stains for texture
ctx.globalAlpha = 0.05;
for (let i = 0; i < 30000; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const radius = Math.random() * 2;
ctx.fillStyle = '#40382D'; // Dark brown
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1.0;
// Vignette effect to age the edges
const gradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height) / 4,
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height) / 1.5
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.25)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 3. PROCESS THE IMAGE (DUOTONE EFFECT INSPIRED BY THE PAINTING)
// Draw the image to a temporary canvas to get its pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// Duotone colors from Vasnetsov's "Gamayun, the Prophetic Bird"
const darkColor = { r: 25, g: 20, b: 50 }; // Dark stormy blue/purple
const lightColor = { r: 255, g: 180, b: 90 }; // Sunset orange/yellow
for (let i = 0; i < data.length; i += 4) {
// Get grayscale value using the luminosity method
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
const normalizedGray = gray / 255;
// Interpolate between the dark and light colors based on the grayscale value
data[i] = darkColor.r + (lightColor.r - darkColor.r) * normalizedGray;
data[i + 1] = darkColor.g + (lightColor.g - darkColor.g) * normalizedGray;
data[i + 2] = darkColor.b + (lightColor.b - darkColor.b) * normalizedGray;
}
// Put the modified image data back
tempCtx.putImageData(imageData, 0, 0);
// Draw a dark border behind the image
ctx.fillStyle = '#333';
ctx.fillRect(padding.left - 4, padding.top - 4, originalImg.width + 8, originalImg.height + 8);
// Draw the processed image onto the main canvas
ctx.drawImage(tempCanvas, padding.left, padding.top);
// 4. ADD "CASE FILE" TEXT & OVERLAYS
// Header text
ctx.fillStyle = '#3a3228';
ctx.font = '700 32px "Playfair Display SC"';
ctx.textAlign = 'left';
ctx.fillText(`ДЕЛО № ${caseNumber}`, padding.left, 65);
ctx.textAlign = 'right';
ctx.font = '700 20px "Playfair Display SC"';
ctx.fillText('СОВЕРШЕННО СЕКРЕТНО', canvas.width - padding.right, 65);
// Footer text (typewriter style)
ctx.font = '16px "Courier Prime"';
ctx.textAlign = 'left';
const textYStart = canvas.height - padding.bottom + 40;
ctx.fillText(`Объект: Вещая птица "Гамаюн"`, padding.left, textYStart);
ctx.fillText(`Дата: ${caseDate}`, padding.left, textYStart + 25);
ctx.fillText(`Агент: ${agentName}`, padding.left, textYStart + 50);
// 5. ADD STAMPS
// Red "APPROVED" stamp
ctx.save();
ctx.translate(canvas.width - padding.right - 100, canvas.height - padding.bottom + 60);
ctx.rotate(-0.15); // a slight, more natural rotation
ctx.fillStyle = 'rgba(200, 0, 0, 0.7)';
ctx.strokeStyle = 'rgba(150, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(-100, -25, 200, 50);
ctx.fillRect(-100, -25, 200, 50);
ctx.font = '700 30px "Playfair Display SC"';
ctx.fillStyle = '#f4eeda';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('ОДОБРЕНО', 0, 0); // "APPROVED"
ctx.restore();
// Circular "Ministry" stamp
ctx.save();
ctx.translate(padding.left + 90, canvas.height - padding.bottom + 80);
ctx.strokeStyle = 'rgba(0, 50, 150, 0.7)';
ctx.lineWidth = 3;
// Double circle for the stamp border
ctx.beginPath();
ctx.arc(0, 0, 45, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 50, 0, Math.PI * 2);
ctx.stroke();
// Text inside the stamp
ctx.fillStyle = 'rgba(0, 50, 150, 0.8)';
ctx.font = '700 12px "Playfair Display SC"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('О.К.С.А.', 0, -10); // Abbreviation for a fictional department
ctx.fillText(new Date().getFullYear().toString(), 0, 15);
ctx.restore();
return canvas;
}
Apply Changes