You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, spotlightIntensity = "0.2") {
const w_img = originalImg.width;
const h_img = originalImg.height;
// We pad the canvas proportionally so the original image isn't cropped
// and correctly looks like a backdrop screen on a stage.
const padX = Math.round(w_img * (0.2 / 0.6));
const padYTop = Math.round(h_img * (0.225 / 0.625));
const padYBottom = Math.round(h_img * (0.15 / 0.625));
const w = padX + w_img + padX;
const h = padYTop + h_img + padYBottom;
const w_c = padX;
const floorY = padYTop + h_img;
const h_v = Math.round(padYTop / 1.5);
const screenX = padX;
const screenY = padYTop;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
// 1. Back Wall (Dark brick texture)
ctx.fillStyle = '#0f1112';
ctx.fillRect(0, 0, w, floorY);
ctx.strokeStyle = '#050505';
ctx.lineWidth = Math.max(1, w * 0.002);
let bWidth = Math.max(20, w * 0.04);
let bHeight = Math.max(10, w * 0.015);
for (let j = 0; j <= floorY; j += bHeight) {
ctx.beginPath();
ctx.moveTo(0, j);
ctx.lineTo(w, j);
ctx.stroke();
}
for (let j = 0; j <= floorY; j += bHeight) {
let offset = (Math.round(j / bHeight) % 2 === 0) ? 0 : bWidth / 2;
ctx.beginPath();
for (let i = offset; i <= w; i += bWidth) {
ctx.moveTo(i, j);
ctx.lineTo(i, j + bHeight);
}
ctx.stroke();
}
// 2. Stage Floor (Wood perspective)
let floorGrad = ctx.createLinearGradient(0, floorY, 0, h);
floorGrad.addColorStop(0, '#0a0500');
floorGrad.addColorStop(1, '#3b2313');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, floorY, w, h - floorY);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.6)';
ctx.lineWidth = Math.max(2, w * 0.002);
let vpX = w / 2;
let vpY = h * 0.4;
for (let i = -20; i <= 20; i++) {
let targetX = vpX + i * (w * 0.05);
let startX = vpX + (targetX - vpX) * ((floorY - vpY) / (h - vpY));
ctx.beginPath();
ctx.moveTo(startX, floorY);
ctx.lineTo(targetX, h);
ctx.stroke();
}
let currentY = floorY;
while (currentY < h) {
ctx.beginPath();
ctx.moveTo(0, currentY);
ctx.lineTo(w, currentY);
ctx.stroke();
let step = (currentY - vpY) * 0.08;
currentY += Math.max(1, step);
}
// 3. Movie Screen Frame
ctx.fillStyle = '#444';
let frameOuter = Math.max(2, w * 0.01);
ctx.fillRect(screenX - frameOuter, screenY - frameOuter, w_img + frameOuter * 2, h_img + frameOuter * 2);
ctx.fillStyle = '#222';
let frameInner = Math.max(1, w * 0.005);
ctx.fillRect(screenX - frameInner, screenY - frameInner, w_img + frameInner * 2, h_img + frameInner * 2);
// 4. Original Image (Projected Screen)
ctx.drawImage(originalImg, screenX, screenY, w_img, h_img);
// 5. Footlights
ctx.beginPath();
ctx.moveTo(0, floorY);
ctx.lineTo(w, floorY);
ctx.strokeStyle = '#050505';
ctx.lineWidth = Math.max(3, w * 0.005);
ctx.stroke();
let nl = 15;
for (let i = 1; i <= nl; i++) {
let lx = i * (w / (nl + 1));
let ly = floorY;
ctx.beginPath();
ctx.arc(lx, ly, Math.max(2, w * 0.015), Math.PI, 0);
ctx.fillStyle = '#111';
ctx.fill();
ctx.beginPath();
ctx.arc(lx, ly, Math.max(1, w * 0.005), Math.PI, 0);
ctx.fillStyle = '#ffffe0';
ctx.fill();
let glow = ctx.createRadialGradient(lx, ly, 0, lx, ly, w * 0.04);
glow.addColorStop(0, 'rgba(255, 255, 200, 0.4)');
glow.addColorStop(1, 'rgba(255, 255, 200, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(lx, ly, w * 0.04, Math.PI, 0);
ctx.fill();
}
// 6. Spotlights
let intensity = Math.min(1, Math.max(0, parseFloat(spotlightIntensity) || 0.2));
if (intensity > 0) {
ctx.globalCompositeOperation = 'screen';
let spotStartL = `rgba(255, 255, 240, ${intensity})`;
let spotEnd = 'rgba(255, 255, 240, 0)';
let spotL = ctx.createLinearGradient(w * 0.05, 0, w * 0.5, h * 0.8);
spotL.addColorStop(0, spotStartL);
spotL.addColorStop(1, spotEnd);
ctx.fillStyle = spotL;
ctx.beginPath();
ctx.moveTo(w * 0.05, 0);
ctx.lineTo(w * 0.3, h * 0.8);
ctx.lineTo(w * 0.7, h * 0.8);
ctx.closePath();
ctx.fill();
let spotR = ctx.createLinearGradient(w * 0.95, 0, w * 0.5, h * 0.8);
spotR.addColorStop(0, spotStartL);
spotR.addColorStop(1, spotEnd);
ctx.fillStyle = spotR;
ctx.beginPath();
ctx.moveTo(w * 0.95, 0);
ctx.lineTo(w * 0.7, h * 0.8);
ctx.lineTo(w * 0.3, h * 0.8);
ctx.closePath();
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
}
// 7. Left Curtain + Right Curtain
let folds = 7;
// Left Curtain Setup
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
ctx.shadowBlur = Math.max(10, w * 0.015);
ctx.shadowOffsetX = Math.max(3, w * 0.005);
ctx.shadowOffsetY = Math.max(3, h * 0.005);
let leftGrad = ctx.createLinearGradient(0, 0, w_c, 0);
for (let i = 0; i <= folds * 2; i++) {
leftGrad.addColorStop(i / (folds * 2), i % 2 === 0 ? '#2b0000' : '#bc0000');
}
ctx.fillStyle = leftGrad;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(w_c, 0);
ctx.quadraticCurveTo(w_c, h * 0.3, w_c * 0.3, h * 0.6);
ctx.quadraticCurveTo(w_c * 0.3, h * 0.8, w_c * 0.7, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(w_c, 0);
ctx.quadraticCurveTo(w_c, h * 0.3, w_c * 0.3, h * 0.6);
ctx.quadraticCurveTo(w_c * 0.3, h * 0.8, w_c * 0.7, h);
ctx.strokeStyle = '#DAA520';
ctx.lineWidth = Math.max(2, h * 0.008);
ctx.lineCap = 'round';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, h * 0.58);
ctx.lineTo(w_c * 0.35, h * 0.6);
ctx.lineTo(0, h * 0.62);
ctx.fillStyle = '#DAA520';
ctx.fill();
// Right Curtain Setup
ctx.shadowOffsetX = -Math.max(3, w * 0.005);
let rightGrad = ctx.createLinearGradient(w - w_c, 0, w, 0);
for (let i = 0; i <= folds * 2; i++) {
rightGrad.addColorStop(i / (folds * 2), i % 2 === 0 ? '#2b0000' : '#bc0000');
}
ctx.fillStyle = rightGrad;
ctx.beginPath();
ctx.moveTo(w, 0);
ctx.lineTo(w - w_c, 0);
ctx.quadraticCurveTo(w - w_c, h * 0.3, w - w_c * 0.3, h * 0.6);
ctx.quadraticCurveTo(w - w_c * 0.3, h * 0.8, w - w_c * 0.7, h);
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(w - w_c, 0);
ctx.quadraticCurveTo(w - w_c, h * 0.3, w - w_c * 0.3, h * 0.6);
ctx.quadraticCurveTo(w - w_c * 0.3, h * 0.8, w - w_c * 0.7, h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(w, h * 0.58);
ctx.lineTo(w - w_c * 0.35, h * 0.6);
ctx.lineTo(w, h * 0.62);
ctx.fillStyle = '#DAA520';
ctx.fill();
// 8. Top Valance
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = Math.max(5, h * 0.01);
let numScallops = 9;
let sw = w / numScallops;
let valanceGrad = ctx.createLinearGradient(0, 0, w, 0);
for (let i = 0; i <= numScallops * 2; i++) {
valanceGrad.addColorStop(i / (numScallops * 2), i % 2 === 0 ? '#2b0000' : '#bc0000');
}
ctx.fillStyle = valanceGrad;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, h_v);
for (let i = 0; i < numScallops; i++) {
ctx.quadraticCurveTo(i * sw + sw / 2, h_v * 1.5, (i + 1) * sw, h_v);
}
ctx.lineTo(w, 0);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, h_v);
for (let i = 0; i < numScallops; i++) {
ctx.quadraticCurveTo(i * sw + sw / 2, h_v * 1.5, (i + 1) * sw, h_v);
}
ctx.strokeStyle = '#DAA520';
ctx.stroke();
ctx.fillStyle = '#DAA520';
ctx.lineWidth = Math.max(1, h * 0.004);
for (let i = 0; i <= numScallops; i++) {
ctx.beginPath();
ctx.moveTo(i * sw, h_v);
ctx.lineTo(i * sw, h_v * 1.2);
ctx.stroke();
ctx.beginPath();
ctx.arc(i * sw, h_v * 1.25, ctx.lineWidth * 2, 0, Math.PI * 2);
ctx.fill();
}
// Reset shadow
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
return canvas;
}
Apply Changes