You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, titleText = "ОБЗОР СТУДИИ: КИНОКОМПАНИЯ НОВОГО ГОДА", zoomAmount = "1.2", snowDensity = "60") {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// Set standard cinematic 16:9 aspect ratio for the viewer based on original image width
const viewerWidth = Math.max(originalImg.width, 1280);
const viewerHeight = viewerWidth * (9 / 16);
canvas.width = viewerWidth;
canvas.height = viewerHeight;
canvas.style.maxWidth = "100%";
canvas.style.height = "auto";
canvas.style.display = "block";
canvas.style.boxShadow = "0 8px 32px rgba(0,0,0,0.5)";
canvas.style.cursor = "grab";
const zoom = Math.max(1, parseFloat(zoomAmount) || 1.2);
const particlesCount = parseInt(snowDensity) || 60;
let panX = (viewerWidth - originalImg.width * zoom) / 2;
let panY = (viewerHeight - originalImg.height * zoom) / 2;
let isDragging = false;
let startX = 0;
let startY = 0;
const initTime = Date.now();
let blinkRec = false;
function draw() {
ctx.clearRect(0, 0, viewerWidth, viewerHeight);
// Base dark background
ctx.fillStyle = "#050505";
ctx.fillRect(0, 0, viewerWidth, viewerHeight);
// Dynamic panning bounds logic
const iw = originalImg.width * zoom;
const ih = originalImg.height * zoom;
const minX = Math.min(0, viewerWidth - iw);
const maxX = Math.max(0, (viewerWidth - iw) / 2);
const minY = Math.min(0, viewerHeight - ih);
const maxY = Math.max(0, (viewerHeight - ih) / 2);
if (iw >= viewerWidth) {
panX = Math.max(minX, Math.min(0, panX));
} else {
panX = (viewerWidth - iw) / 2;
}
if (ih >= viewerHeight) {
panY = Math.max(minY, Math.min(0, panY));
} else {
panY = (viewerHeight - ih) / 2;
}
// Draw the main panning image
ctx.drawImage(originalImg, panX, panY, iw, ih);
// Adding Cinematic Cold Grade
const gradient = ctx.createRadialGradient(
viewerWidth / 2, viewerHeight / 2, viewerWidth * 0.2,
viewerWidth / 2, viewerHeight / 2, viewerWidth * 0.8
);
gradient.addColorStop(0, "rgba(0, 20, 50, 0.05)");
gradient.addColorStop(1, "rgba(0, 10, 25, 0.6)");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, viewerWidth, viewerHeight);
// "New Year" Studio Snow / Bokeh Overlay logic (Parallax)
ctx.fillStyle = "rgba(255, 255, 255, 0.65)";
for(let i = 0; i < particlesCount; i++) {
const sx = ((i * 137 + panX * (0.2 + (i % 3) * 0.15)) % viewerWidth + viewerWidth) % viewerWidth;
const sy = ((i * 313 + panY * (0.2 + (i % 3) * 0.15)) % viewerHeight + viewerHeight) % viewerHeight;
const radius = (i % 4) + 1;
ctx.beginPath();
ctx.arc(sx, sy, radius, 0, Math.PI * 2);
ctx.fill();
}
// Top and Bottom Letterboxing UI Blocks
const barHeight = viewerHeight * 0.1;
ctx.fillStyle = "rgba(0, 0, 0, 0.85)";
ctx.fillRect(0, 0, viewerWidth, barHeight);
ctx.fillRect(0, viewerHeight - barHeight, viewerWidth, barHeight);
// --- Central Viewfinder UI ---
const safeX = viewerWidth * 0.15;
const safeY = viewerHeight * 0.15;
const safeW = viewerWidth * 0.7;
const safeH = viewerHeight * 0.7;
const bracketLen = viewerWidth * 0.03;
ctx.strokeStyle = "rgba(255, 255, 255, 0.4)";
ctx.lineWidth = 3;
// Top Left Bracket
ctx.beginPath();
ctx.moveTo(safeX, safeY + bracketLen);
ctx.lineTo(safeX, safeY);
ctx.lineTo(safeX + bracketLen, safeY);
ctx.stroke();
// Top Right Bracket
ctx.beginPath();
ctx.moveTo(safeX + safeW - bracketLen, safeY);
ctx.lineTo(safeX + safeW, safeY);
ctx.lineTo(safeX + safeW, safeY + bracketLen);
ctx.stroke();
// Bottom Right Bracket
ctx.beginPath();
ctx.moveTo(safeX + safeW, safeY + safeH - bracketLen);
ctx.lineTo(safeX + safeW, safeY + safeH);
ctx.lineTo(safeX + safeW - bracketLen, safeY + safeH);
ctx.stroke();
// Bottom Left Bracket
ctx.beginPath();
ctx.moveTo(safeX + bracketLen, safeY + safeH);
ctx.lineTo(safeX, safeY + safeH);
ctx.lineTo(safeX, safeY + safeH - bracketLen);
ctx.stroke();
// Center Crosshair
ctx.beginPath();
ctx.moveTo(viewerWidth / 2 - 20, viewerHeight / 2);
ctx.lineTo(viewerWidth / 2 + 20, viewerHeight / 2);
ctx.moveTo(viewerWidth / 2, viewerHeight / 2 - 20);
ctx.lineTo(viewerWidth / 2, viewerHeight / 2 + 20);
ctx.stroke();
// --- Texts and HUD overlays ---
ctx.fillStyle = "#ffffff";
ctx.font = `bold ${Math.max(14, barHeight * 0.35)}px "Courier New", Courier, monospace`;
ctx.textBaseline = "middle";
// Title Text
ctx.textAlign = "left";
ctx.fillText(titleText, viewerWidth * 0.02, barHeight / 2);
// Timecode and REC logic
const elapsedTime = Date.now() - initTime;
const totalSecs = Math.floor(elapsedTime / 1000);
const hh = Math.floor(totalSecs / 3600);
const mm = Math.floor(totalSecs / 60) % 60;
const ss = totalSecs % 60;
// simulating frames at 24fps
const ff = Math.floor((elapsedTime % 1000) / (1000/24));
const pad = (n) => String(n).padStart(2, "0");
const timecode = `REC ${pad(hh)}:${pad(mm)}:${pad(ss)}:${pad(ff)}`;
ctx.textAlign = "right";
ctx.fillText(timecode, viewerWidth * 0.98, barHeight / 2);
// Blinking REC dot
if (blinkRec) {
ctx.fillStyle = "#ff0000";
const textWidth = ctx.measureText(timecode).width;
const dotSize = barHeight * 0.12;
ctx.beginPath();
ctx.arc(viewerWidth * 0.98 - textWidth - dotSize * 2, barHeight / 2, dotSize, 0, Math.PI * 2);
ctx.fill();
}
// Footer Details Info
ctx.fillStyle = "#999999";
ctx.textAlign = "center";
ctx.font = `bold ${Math.max(12, barHeight * 0.25)}px "Courier New", Courier, monospace`;
ctx.fillText(`CAM A [ FOV: ${(zoom * 100).toFixed(0)}% ] STUDIO LENS ISO 800 NEW YEAR PREVIEW COMP`, viewerWidth / 2, viewerHeight - barHeight / 2);
}
// Core interactivity & animation logic
// We use an interval to drive timecode without spinning an endless heavy requestAnimationFrame
const clockInterval = setInterval(() => {
// Discard gracefully if DOM node dies and passes ~5s lifecycle buffer
if (!canvas.isConnected && (Date.now() - initTime > 5000)) {
clearInterval(clockInterval);
return;
}
blinkRec = !blinkRec;
draw();
}, 500);
// Initial draw
draw();
// Mapping mouse events to pan original source gracefully over viewport
canvas.addEventListener("mousedown", (e) => {
isDragging = true;
canvas.style.cursor = "grabbing";
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
startX = (e.clientX - rect.left) * scaleX - panX;
startY = (e.clientY - rect.top) * scaleY - panY;
});
canvas.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
panX = (e.clientX - rect.left) * scaleX - startX;
panY = (e.clientY - rect.top) * scaleY - startY;
draw();
});
canvas.addEventListener("mouseup", () => {
isDragging = false;
canvas.style.cursor = "grab";
});
canvas.addEventListener("mouseleave", () => {
isDragging = false;
canvas.style.cursor = "grab";
});
// Add graceful touch gestures support
canvas.addEventListener("touchstart", (e) => {
e.preventDefault();
isDragging = true;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
startX = (e.touches[0].clientX - rect.left) * scaleX - panX;
startY = (e.touches[0].clientY - rect.top) * scaleY - panY;
}, {passive: false});
canvas.addEventListener("touchmove", (e) => {
e.preventDefault();
if (!isDragging) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
panX = (e.touches[0].clientX - rect.left) * scaleX - startX;
panY = (e.touches[0].clientY - rect.top) * scaleY - startY;
draw();
}, {passive: false});
canvas.addEventListener("touchend", () => {
isDragging = false;
});
return canvas;
}
Apply Changes