You can edit the below JavaScript code to customize the image tool.
Apply Changes
// This function creates an interactive "Mystères de Pétersbourg" (Mysteries of Petersburg) viewer.
// It transforms the base image into a moody, foggy, dark environment akin to the atmosphere
// of a 19th-century detective mystery, and gives the user an interactive spotlight (flashlight)
// to search through visually hidden visuals, revealing the original vivid tones beneath.
function processImage(originalImg, spotRadius = 150, darknessColor = "rgba(10, 15, 25, 0.85)", blurAmount = 4) {
const width = originalImg.width;
const height = originalImg.height;
// Parse parameters
const radius = parseFloat(spotRadius) || 150;
const blurNum = parseFloat(blurAmount) || 4;
// Create the main visible canvas
const mainCanvas = document.createElement('canvas');
mainCanvas.width = width;
mainCanvas.height = height;
mainCanvas.style.maxWidth = '100%';
mainCanvas.style.height = 'auto';
mainCanvas.style.display = 'block';
mainCanvas.style.cursor = 'crosshair';
mainCanvas.style.touchAction = 'none'; // Prevents scrolling when interacting via touch
const ctx = mainCanvas.getContext('2d');
// Pre-render the dark, mysterious background (fog/gloom of Petersburg)
// This optimizes performance by not applying heavy filters on every mouse movement.
const darkBgCanvas = document.createElement('canvas');
darkBgCanvas.width = width;
darkBgCanvas.height = height;
const darkCtx = darkBgCanvas.getContext('2d');
// Apply moody, vintage filter to the background (grayscale, contrast, sepia, blur)
darkCtx.filter = `sepia(50%) contrast(120%) brightness(80%) grayscale(80%) blur(${blurNum}px)`;
darkCtx.drawImage(originalImg, 0, 0);
darkCtx.filter = 'none';
// Apply the heavy dark tint
darkCtx.fillStyle = darknessColor;
darkCtx.fillRect(0, 0, width, height);
// Apply a subtle vignette effect to deepen the mystery at the edges
const vignette = darkCtx.createRadialGradient(width/2, height/2, width/4, width/2, height/2, width/1.2);
vignette.addColorStop(0, 'rgba(0,0,0,0)');
vignette.addColorStop(1, 'rgba(0,0,0,0.8)');
darkCtx.fillStyle = vignette;
darkCtx.fillRect(0, 0, width, height);
// Create a temporary overlay canvas for creating the spotlight mask
const overlayCanvas = document.createElement('canvas');
overlayCanvas.width = width;
overlayCanvas.height = height;
const overlayCtx = overlayCanvas.getContext('2d');
// Start with spotlight dead center
let mouseX = width / 2;
let mouseY = height / 2;
let isRendering = false;
// Main render loop for interactivity
function render() {
// Draw the pure, vibrant original image
ctx.clearRect(0, 0, width, height);
ctx.drawImage(originalImg, 0, 0);
// Prep the overlay canvas
overlayCtx.globalCompositeOperation = 'source-over';
overlayCtx.clearRect(0, 0, width, height);
// Draw the gloomy mystery background
overlayCtx.drawImage(darkBgCanvas, 0, 0);
// Cut out the spotlight circle using destination-out
overlayCtx.globalCompositeOperation = 'destination-out';
const gradient = overlayCtx.createRadialGradient(mouseX, mouseY, 0, mouseX, mouseY, radius);
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)');
gradient.addColorStop(0.7, 'rgba(0, 0, 0, 0.8)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
overlayCtx.fillStyle = gradient;
overlayCtx.beginPath();
overlayCtx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
overlayCtx.fill();
// Draw the masked dark background onto main canvas
ctx.drawImage(overlayCanvas, 0, 0);
// Add a subtle screen glow to the rim of the spotlight
ctx.globalCompositeOperation = 'screen';
ctx.beginPath();
ctx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
const rimGrad = ctx.createRadialGradient(mouseX, mouseY, radius * 0.9, mouseX, mouseY, radius);
rimGrad.addColorStop(0, 'rgba(255, 255, 255, 0)');
rimGrad.addColorStop(0.8, 'rgba(255, 255, 255, 0.08)');
rimGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = rimGrad;
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
}
// Initial draw
render();
// Event listener helper
function updatePointer(e) {
const rect = mainCanvas.getBoundingClientRect();
// Calculate canvas scaling based on responsive CSS
const scaleX = mainCanvas.width / rect.width;
const scaleY = mainCanvas.height / rect.height;
if (e.touches && e.touches.length > 0) {
mouseX = (e.touches[0].clientX - rect.left) * scaleX;
mouseY = (e.touches[0].clientY - rect.top) * scaleY;
} else {
mouseX = (e.clientX - rect.left) * scaleX;
mouseY = (e.clientY - rect.top) * scaleY;
}
// Use requestAnimationFrame to keep animation butter smooth
if (!isRendering) {
isRendering = true;
requestAnimationFrame(() => {
render();
isRendering = false;
});
}
}
// Attach interactions
mainCanvas.addEventListener('mousemove', updatePointer);
mainCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
updatePointer(e);
}, { passive: false });
return mainCanvas;
}
Apply Changes