You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, tvColor = "#ce4a33", tubeColor = "#111811", crtIntensity = 1) {
const origW = originalImg.width;
const origH = originalImg.height;
// Scale up small images (like icons) so the TV detail is always visible
let scale = 1;
const maxDim = Math.max(origW, origH);
if (maxDim < 256) {
scale = 256 / maxDim;
}
const cw = Math.round(origW * scale);
const ch = Math.round(origH * scale);
// Calculate a base unit for styling proportions relative to the screen size
const u = Math.max(cw, ch) * 0.08;
// Define layout logic
const tvOuterX = Math.ceil(u);
const tvOuterY = Math.ceil(u * 2.5);
const screenW = cw;
const screenH = ch;
const tvInnerX = tvOuterX + u;
const tvInnerY = tvOuterY + u;
// Width = inner padding left (1u) + screen (cw) + control panel margin & space (3.5u)
const tvOuterWidth = screenW + u * 4.5;
// Height = inner padding top (1u) + screen (ch) + inner padding bottom (1u)
const tvOuterHeight = screenH + u * 2.0;
// Final dimensions to accommodate shadow, legs, and antennas
const finalW = Math.ceil(tvOuterWidth + u * 2.5);
const finalH = Math.ceil(tvOuterHeight + u * 4.5);
const canvas = document.createElement('canvas');
canvas.width = finalW;
canvas.height = finalH;
const ctx = canvas.getContext('2d');
// Utility path drawing function for rounded rectangles
function drawRoundRect(c, x, y, w, h, r, fillStyle, strokeStyle, lineWidth) {
c.beginPath();
c.moveTo(x + r, y);
c.lineTo(x + w - r, y);
c.quadraticCurveTo(x + w, y, x + w, y + r);
c.lineTo(x + w, y + h - r);
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
c.lineTo(x + r, y + h);
c.quadraticCurveTo(x, y + h, x, y + h - r);
c.lineTo(x, y + r);
c.quadraticCurveTo(x, y, x + r, y);
c.closePath();
if (fillStyle) {
c.fillStyle = fillStyle;
c.fill();
}
if (strokeStyle) {
if (lineWidth) c.lineWidth = lineWidth;
c.strokeStyle = strokeStyle;
c.stroke();
}
}
// --- Draw TV Components from back to front ---
// 1. Antennas
const cx = tvOuterX + tvOuterWidth / 2;
ctx.strokeStyle = '#a5a5a5';
ctx.lineWidth = u * 0.15;
ctx.lineCap = 'round';
ctx.beginPath();
// Base V
ctx.moveTo(cx, tvOuterY);
ctx.lineTo(cx - u * 2.2, tvOuterY - u * 1.8);
ctx.moveTo(cx, tvOuterY);
ctx.lineTo(cx + u * 2.2, tvOuterY - u * 1.8);
ctx.stroke();
// Antenna tips
ctx.fillStyle = '#a5a5a5';
ctx.beginPath();
ctx.arc(cx - u * 2.2, tvOuterY - u * 1.8, u * 0.25, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(cx + u * 2.2, tvOuterY - u * 1.8, u * 0.25, 0, Math.PI * 2);
ctx.fill();
// Antenna Base mounting
drawRoundRect(ctx, cx - u * 0.6, tvOuterY - u * 0.3, u * 1.2, u * 0.6, u * 0.2, '#2a2a2a');
// Enable shadows for the main body
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = u * 0.5;
ctx.shadowOffsetY = u * 0.4;
// 2. Legs
ctx.fillStyle = '#222';
// Left leg
ctx.beginPath();
ctx.moveTo(tvOuterX + u * 1.5, tvOuterY + tvOuterHeight - u * 0.5);
ctx.lineTo(tvOuterX + u * 0.2, tvOuterY + tvOuterHeight + u * 1.2);
ctx.lineTo(tvOuterX + u * 0.8, tvOuterY + tvOuterHeight + u * 1.2);
ctx.lineTo(tvOuterX + u * 2.2, tvOuterY + tvOuterHeight - u * 0.5);
ctx.fill();
// Right leg
ctx.beginPath();
ctx.moveTo(tvOuterX + tvOuterWidth - u * 1.5, tvOuterY + tvOuterHeight - u * 0.5);
ctx.lineTo(tvOuterX + tvOuterWidth - u * 0.2, tvOuterY + tvOuterHeight + u * 1.2);
ctx.lineTo(tvOuterX + tvOuterWidth - u * 0.8, tvOuterY + tvOuterHeight + u * 1.2);
ctx.lineTo(tvOuterX + tvOuterWidth - u * 2.2, tvOuterY + tvOuterHeight - u * 0.5);
ctx.fill();
// 3. TV Body Frame
drawRoundRect(ctx, tvOuterX, tvOuterY, tvOuterWidth, tvOuterHeight, u * 0.5, tvColor);
// Reset shadow for deeper flat shading
ctx.shadowColor = 'transparent';
// Highlight ridge around TV body
drawRoundRect(ctx, tvOuterX + u * 0.1, tvOuterY + u * 0.1, tvOuterWidth - u * 0.2, tvOuterHeight - u * 0.2, u * 0.4, null, 'rgba(255,255,255,0.15)', u * 0.05);
// 4. Screen Bezel & Recess
const bezelX = tvInnerX - u * 0.4;
const bezelY = tvInnerY - u * 0.4;
const bezelW = screenW + u * 0.8;
const bezelH = screenH + u * 0.8;
drawRoundRect(ctx, bezelX, bezelY, bezelW, bezelH, u * 0.3, '#1a1a1a');
drawRoundRect(ctx, bezelX + u * 0.1, bezelY + u * 0.1, bezelW - u * 0.2, bezelH - u * 0.2, u * 0.25, '#0d0d0d');
// 5. Build Final Tube/Screen Content using an off-screen canvas
const screenCanvas = document.createElement('canvas');
screenCanvas.width = cw;
screenCanvas.height = ch;
const sCtx = screenCanvas.getContext('2d');
// Base tube color
sCtx.fillStyle = tubeColor;
sCtx.fillRect(0, 0, cw, ch);
// Draw the actual image
sCtx.imageSmoothingEnabled = false; // preserve retro aesthetic for icons
sCtx.drawImage(originalImg, 0, 0, cw, ch);
if (crtIntensity > 0) {
// CRT RGB Shift & Static Noise
try {
const shiftX = Math.max(1, Math.floor(cw * 0.005 * crtIntensity));
const imgData = sCtx.getImageData(0, 0, cw, ch);
const data = imgData.data;
const tvData = new Uint8ClampedArray(data.length);
for (let y = 0; y < ch; y++) {
for (let x = 0; x < cw; x++) {
const idx = (y * cw + x) * 4;
const rX = Math.max(0, x - shiftX);
const rIdx = (y * cw + rX) * 4;
tvData[idx] = data[rIdx]; // R
tvData[idx + 1] = data[idx + 1]; // G
const bX = Math.min(cw - 1, x + shiftX);
const bIdx = (y * cw + bX) * 4;
tvData[idx + 2] = data[bIdx + 2]; // B
tvData[idx + 3] = data[idx + 3]; // A
// Static Noise
if (tvData[idx + 3] > 0) {
const noise = (Math.random() - 0.5) * 45 * crtIntensity;
tvData[idx] = Math.min(255, Math.max(0, tvData[idx] + noise));
tvData[idx + 1] = Math.min(255, Math.max(0, tvData[idx + 1] + noise));
tvData[idx + 2] = Math.min(255, Math.max(0, tvData[idx + 2] + noise));
}
}
}
sCtx.putImageData(new ImageData(tvData, cw, ch), 0, 0);
} catch (e) {
console.warn("CORS issue: Advanced CRT RGB/Noise skipped.", e);
}
// Apply Scanlines and Vignette overlay directly
sCtx.globalCompositeOperation = 'source-atop';
// Scanlines
sCtx.fillStyle = `rgba(0, 0, 0, ${0.15 + (0.1 * crtIntensity)})`;
const lineH = Math.max(2, Math.floor(ch * 0.015));
for (let y = 0; y < ch; y += lineH * 2) {
sCtx.fillRect(0, y, cw, lineH);
}
// Vignette
const gradient = sCtx.createRadialGradient(
cw / 2, ch / 2, Math.min(cw, ch) * 0.4,
cw / 2, ch / 2, Math.max(cw, ch) * 0.8
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0,${0.6 * crtIntensity})`);
sCtx.fillStyle = gradient;
sCtx.fillRect(0, 0, cw, ch);
sCtx.globalCompositeOperation = 'source-over';
}
// Mask bounds of the screen area to round the rigid corners of the canvas
ctx.save();
drawRoundRect(ctx, tvInnerX, tvInnerY, screenW, screenH, u * 0.2);
ctx.clip();
// Draw finalized screen logic
ctx.drawImage(screenCanvas, tvInnerX, tvInnerY);
// Dynamic Glass Reflection Over Screen
ctx.fillStyle = 'rgba(255, 255, 255, 0.12)';
ctx.beginPath();
ctx.moveTo(tvInnerX - u, tvInnerY - u);
ctx.lineTo(tvInnerX + screenW * 0.65, tvInnerY - u);
ctx.lineTo(tvInnerX + screenW * 0.1, tvInnerY + screenH + u);
ctx.lineTo(tvInnerX - u, tvInnerY + screenH + u);
ctx.fill();
ctx.restore();
// 6. Right Side Control Panel
const panelX = tvOuterX + u * 2 + screenW;
const panelY = tvInnerY - u * 0.2;
const panelW = u * 2.0;
const panelH = screenH + u * 0.4;
drawRoundRect(ctx, panelX, panelY, panelW, panelH, u * 0.2, '#2a2a2a');
drawRoundRect(ctx, panelX - u * 0.05, panelY - u * 0.05, panelW + u * 0.1, panelH + u * 0.1, u * 0.25, null, '#444', u * 0.1);
// Knobs Structure
const knobR = panelW * 0.35;
const knobX = panelX + panelW / 2;
const topKnobY = panelY + panelW * 0.7;
const bottomKnobY = topKnobY + panelW * 1.3;
// Knob Maker Function
function drawKnob(x, y, rotation) {
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(x, y, knobR, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#c7c7c7';
ctx.beginPath();
ctx.arc(x, y, knobR * 0.8, 0, Math.PI * 2);
ctx.fill();
// Dial gauge groove
ctx.strokeStyle = '#111';
ctx.lineWidth = Math.max(1, u * 0.1);
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(rotation) * (knobR * 0.6), y + Math.sin(rotation) * (knobR * 0.6));
ctx.stroke();
}
drawKnob(knobX, topKnobY, -Math.PI / 4);
drawKnob(knobX, bottomKnobY, Math.PI / 6);
// Speaker Grille lines below buttons
const grilleStartY = bottomKnobY + panelW;
const grilleSpacing = panelW * 0.18; // space evenly inside bottom panel layout
const numGrilles = Math.floor((panelY + panelH - grilleStartY - panelW * 0.2) / grilleSpacing);
ctx.fillStyle = '#111';
for (let i = 0; i < numGrilles; i++) {
drawRoundRect(ctx, panelX + panelW * 0.2, grilleStartY + i * grilleSpacing, panelW * 0.6, panelW * 0.08, panelW * 0.04, '#0d0d0d');
}
return canvas;
}
Apply Changes