You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, callerName = "Jane Doe", callType = "Video Call") {
// 1. Dynamically inject necessary web-safe fonts (Roboto & Material Icons)
const fontStyleId = 'skype-call-fonts';
if (!document.getElementById(fontStyleId)) {
const fontStyle = document.createElement('style');
fontStyle.id = fontStyleId;
fontStyle.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Icons+Round');
`;
document.head.appendChild(fontStyle);
// Wait reasonably to ensure stylesheets are parsed and fonts begin loading
await new Promise(r => setTimeout(r, 150));
try {
await document.fonts.ready;
} catch (e) {
console.warn("Font loading delay: ", e);
}
}
// 2. Setup standard mobile portrait canvas size
const canvas = document.createElement('canvas');
canvas.width = 360;
canvas.height = 720;
const ctx = canvas.getContext('2d');
// Interpret call type
const isVideo = callType.toLowerCase().includes('video');
// Pre-calculate scaling dimensions
const bgScale = Math.max(canvas.width / originalImg.width, canvas.height / originalImg.height);
// Increase size slightly by 20% to prevent blur edge-bleed transparency
const bgW = originalImg.width * bgScale * 1.2;
const bgH = originalImg.height * bgScale * 1.2;
const avaScale = Math.max(140 / originalImg.width, 140 / originalImg.height);
const avaW = originalImg.width * avaScale;
const avaH = originalImg.height * avaScale;
// 3. Animation loop control
let isAnimating = true;
function drawFrame(time) {
if (!isAnimating) return;
if (!time) time = performance.now(); // Fallback for the first direct call
ctx.clearRect(0, 0, canvas.width, canvas.height);
// A. Background (Blurred & Darkened)
ctx.save();
ctx.filter = 'brightness(0.5) blur(20px)';
ctx.drawImage(
originalImg,
(canvas.width - bgW) / 2,
(canvas.height - bgH) / 2,
bgW,
bgH
);
ctx.restore();
// Dark gradient overlay for improved readability
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, 'rgba(0, 0, 0, 0.6)');
grad.addColorStop(0.5, 'rgba(0, 0, 0, 0.2)');
grad.addColorStop(1, 'rgba(0, 0, 0, 0.85)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// B. Mobile Status Bar
ctx.fillStyle = '#ffffff';
ctx.textBaseline = 'middle';
// Time
ctx.textAlign = 'left';
ctx.font = '500 13px Roboto, sans-serif';
const now = new Date();
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
ctx.fillText(timeStr, 20, 25);
// Icons
ctx.textAlign = 'right';
ctx.font = '16px "Material Icons Round", sans-serif';
ctx.fillText('wifi', 315, 25);
ctx.fillText('battery_full', 340, 25);
ctx.fillText('signal_cellular_4_bar', 290, 25);
// Skype Header Brand
ctx.textAlign = 'center';
ctx.fillStyle = '#00aff0'; // Classic Skype Blue
ctx.font = '500 18px Roboto, sans-serif';
ctx.fillText("Skype", 180, 65);
// C. Ringing Animation Waves (Pulsing behind avatar)
const cycle = 2500;
for (let i = 0; i < 3; i++) {
const t = ((time + i * 833) % cycle) / cycle; // staggered waves
const r = 70 + t * 130; // Radius expands from 70 to 200
const alpha = (1 - t) * 0.4;
ctx.beginPath();
ctx.arc(180, 240, r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${alpha.toFixed(3)})`;
ctx.fill();
}
// D. Avatar Profile Picture
ctx.save();
ctx.beginPath();
ctx.arc(180, 240, 70, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(originalImg, 180 - avaW / 2, 240 - avaH / 2, avaW, avaH);
ctx.restore();
// Avatar Border
ctx.beginPath();
ctx.arc(180, 240, 70, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
ctx.lineWidth = 3;
ctx.stroke();
// E. Caller ID & Call Context
ctx.fillStyle = '#ffffff';
ctx.font = '400 32px Roboto, sans-serif';
ctx.fillText(callerName, 180, 395);
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
ctx.font = '400 16px Roboto, sans-serif';
ctx.fillText(isVideo ? "Incoming video call" : "Incoming audio call", 180, 430);
// F. Call Control Buttons
const btnsY = 600;
// Decline Button (Static Red)
ctx.save();
ctx.beginPath();
ctx.arc(110, btnsY, 34, 0, Math.PI * 2);
ctx.fillStyle = '#E81123';
ctx.fill();
ctx.fillStyle = 'white';
ctx.font = '32px "Material Icons Round", sans-serif';
ctx.fillText("call_end", 110, btnsY);
ctx.restore();
// Accept Button (Bouncing and Pulsing Green)
const bounce = Math.sin(time / 150) * 4;
const pulse = 1 + ((Math.sin(time / 200) + 1) / 2) * 0.12;
ctx.save();
ctx.translate(250, btnsY + bounce);
ctx.scale(pulse, pulse);
ctx.beginPath();
ctx.arc(0, 0, 34, 0, Math.PI * 2);
ctx.fillStyle = '#00B533';
ctx.fill();
ctx.fillStyle = 'white';
ctx.font = '32px "Material Icons Round", sans-serif';
ctx.fillText(isVideo ? "videocam" : "call", 0, 0);
ctx.restore();
// Button Labels
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '400 14px Roboto, sans-serif';
ctx.fillText("Decline", 110, 660);
ctx.fillText("Accept", 250, 660);
// Keep looping to simulate "video" environment
requestAnimationFrame(drawFrame);
}
// Force draw the first frame synchronously (vital if canvas is immediately snapshot)
drawFrame(performance.now());
// Optional utility to stop animation if canvas wrapper is removed from memory
canvas.stopAnimation = () => { isAnimating = false; };
return canvas;
}
Apply Changes