You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, title = "Voice note", artist = "Unknown Artist", duration = "1:24") {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set dimensions to original image
canvas.width = originalImg.width;
canvas.height = originalImg.height;
// Draw original image as the background layer
ctx.drawImage(originalImg, 0, 0);
// Determine dynamic UI scale based on image dimensions
const baseSize = Math.min(canvas.width, canvas.height);
const scale = Math.max(baseSize / 1080, 0.4);
// Create dark gradient at the bottom for readability
const gradientHeight = 500 * scale;
const gradientStart = Math.max(0, canvas.height - gradientHeight);
const gradient = ctx.createLinearGradient(0, gradientStart, 0, canvas.height);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(0.3, 'rgba(0, 0, 0, 0.4)');
gradient.addColorStop(0.7, 'rgba(0, 0, 0, 0.7)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.9)');
ctx.fillStyle = gradient;
ctx.fillRect(0, gradientStart, canvas.width, canvas.height - gradientStart);
// Baseline layout for overlay elements (bottom up placement)
const ctrlY = canvas.height - 80 * scale;
const timeY = ctrlY - 55 * scale;
const progressY = timeY - 45 * scale;
const artistY = progressY - 60 * scale;
const titleY = artistY - 60 * scale;
// Horizontal layout mapping
const idealPadding = 60 * scale;
let layoutWidth = canvas.width - (idealPadding * 2);
if (layoutWidth > 1200 * scale) layoutWidth = 1200 * scale;
const barTotalSpace = 12 * scale;
const barsCount = Math.floor(layoutWidth / barTotalSpace);
const actualWidth = barsCount * barTotalSpace;
const progressLeft = (canvas.width - actualWidth) / 2;
// Define brand font family
const fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
// ---- 1. Draw Text Information ---- //
// Track title
ctx.fillStyle = "#ffffff";
ctx.font = `bold ${52 * scale}px ${fontFamily}`;
ctx.textAlign = "left";
ctx.fillText(title, progressLeft, titleY, actualWidth - 60 * scale);
// Track artist
ctx.fillStyle = "#B3B3B3";
ctx.font = `${36 * scale}px ${fontFamily}`;
ctx.fillText(artist, progressLeft, artistY, actualWidth - 60 * scale);
// ---- 2. Draw 'Liked' Heart Icon ---- //
const heartX = progressLeft + actualWidth - 15 * scale;
const heartY = titleY - 26 * scale;
const heartSize = 32 * scale;
ctx.fillStyle = "#1DB954";
ctx.save();
ctx.translate(heartX, heartY);
ctx.beginPath();
ctx.moveTo(0, heartSize/4);
ctx.bezierCurveTo(0, 0, -heartSize/2, 0, -heartSize/2, heartSize/4);
ctx.bezierCurveTo(-heartSize/2, heartSize/2, 0, heartSize*3/4, 0, heartSize);
ctx.bezierCurveTo(0, heartSize*3/4, heartSize/2, heartSize/2, heartSize/2, heartSize/4);
ctx.bezierCurveTo(heartSize/2, 0, 0, 0, 0, heartSize/4);
ctx.fill();
ctx.restore();
// ---- 3. Render Timestamps ---- //
function parseTime(timeStr) {
if (timeStr.includes(':')) {
const parts = timeStr.split(':');
return (parseInt(parts[0]) || 0) * 60 + (parseInt(parts[1]) || 0);
} else if (!isNaN(timeStr)) {
return parseInt(timeStr) || 60;
}
return 60;
}
const totalSeconds = parseTime(duration);
const progressPercent = 0.35; // The overlay visually sits at 35% progression
const currentSeconds = Math.floor(totalSeconds * progressPercent);
function formatTime(secs) {
return Math.floor(secs / 60) + ":" + (secs % 60).toString().padStart(2, '0');
}
const currentTimeStr = formatTime(currentSeconds);
const durationStr = formatTime(totalSeconds);
ctx.fillStyle = "#B3B3B3";
ctx.font = `${22 * scale}px ${fontFamily}`;
ctx.textAlign = "left";
ctx.fillText(currentTimeStr, progressLeft, timeY);
ctx.textAlign = "right";
ctx.fillText(durationStr, progressLeft + actualWidth, timeY);
// ---- 4. Render Voice Soundwave (Audio progress) ---- //
const maxWaveHeight = 60 * scale;
const minWaveHeight = 4 * scale;
const activeLimit = barsCount * progressPercent;
const barW = 6 * scale;
for (let i = 0; i < barsCount; i++) {
const normalized = i / (barsCount - 1);
const envelope = Math.sin(normalized * Math.PI); // Naturally taper wave edges to look organic
const wave = Math.abs(Math.sin(normalized * 35) * 0.7 + Math.cos(normalized * 50) * 0.3);
let h = wave * envelope * (maxWaveHeight - minWaveHeight) + minWaveHeight;
h = Math.max(h, minWaveHeight);
const cx = progressLeft + i * barTotalSpace + barW / 2;
ctx.fillStyle = i <= activeLimit ? "#ffffff" : "rgba(255, 255, 255, 0.3)";
ctx.beginPath();
// Shape for smooth rounded vertical pill bars
ctx.arc(cx, progressY - h/2 + barW/2, barW/2, Math.PI, 0);
ctx.lineTo(cx + barW/2, progressY + h/2 - barW/2);
ctx.arc(cx, progressY + h/2 - barW/2, barW/2, 0, Math.PI);
ctx.closePath();
ctx.fill();
}
// ---- 5. Playback UI Controls ---- //
const cX = canvas.width / 2;
ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
ctx.lineWidth = 3 * scale;
ctx.lineCap = "round";
ctx.lineJoin = "round";
// Play button
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(cX, ctrlY, 35 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#000000";
ctx.beginPath();
ctx.moveTo(cX + 12 * scale, ctrlY);
ctx.lineTo(cX - 6 * scale, ctrlY - 12 * scale);
ctx.lineTo(cX - 6 * scale, ctrlY + 12 * scale);
ctx.fill();
// Prev Button
ctx.fillStyle = "#ffffff";
const prevX = cX - 120 * scale;
ctx.fillRect(prevX - 16 * scale, ctrlY - 12 * scale, 4 * scale, 24 * scale);
ctx.beginPath();
ctx.moveTo(prevX - 10 * scale, ctrlY);
ctx.lineTo(prevX + 4 * scale, ctrlY - 12 * scale);
ctx.lineTo(prevX + 4 * scale, ctrlY + 12 * scale);
ctx.fill();
ctx.beginPath();
ctx.moveTo(prevX + 6 * scale, ctrlY);
ctx.lineTo(prevX + 20 * scale, ctrlY - 12 * scale);
ctx.lineTo(prevX + 20 * scale, ctrlY + 12 * scale);
ctx.fill();
// Next Button
const nextX = cX + 120 * scale;
ctx.beginPath();
ctx.moveTo(nextX - 6 * scale, ctrlY);
ctx.lineTo(nextX - 20 * scale, ctrlY - 12 * scale);
ctx.lineTo(nextX - 20 * scale, ctrlY + 12 * scale);
ctx.fill();
ctx.beginPath();
ctx.moveTo(nextX + 10 * scale, ctrlY);
ctx.lineTo(nextX - 4 * scale, ctrlY - 12 * scale);
ctx.lineTo(nextX - 4 * scale, ctrlY + 12 * scale);
ctx.fill();
ctx.fillRect(nextX + 12 * scale, ctrlY - 12 * scale, 4 * scale, 24 * scale);
// Shuffle Icon
const shufX = cX - 240 * scale;
ctx.beginPath();
ctx.moveTo(shufX - 10 * scale, ctrlY + 6 * scale);
ctx.lineTo(shufX - 2 * scale, ctrlY + 6 * scale);
ctx.lineTo(shufX + 6 * scale, ctrlY - 6 * scale);
ctx.lineTo(shufX + 14 * scale, ctrlY - 6 * scale);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(shufX + 10 * scale, ctrlY - 10 * scale);
ctx.lineTo(shufX + 14 * scale, ctrlY - 6 * scale);
ctx.lineTo(shufX + 10 * scale, ctrlY - 2 * scale);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(shufX - 10 * scale, ctrlY - 6 * scale);
ctx.lineTo(shufX - 2 * scale, ctrlY - 6 * scale);
ctx.lineTo(shufX + 6 * scale, ctrlY + 6 * scale);
ctx.lineTo(shufX + 14 * scale, ctrlY + 6 * scale);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(shufX + 10 * scale, ctrlY + 10 * scale);
ctx.lineTo(shufX + 14 * scale, ctrlY + 6 * scale);
ctx.lineTo(shufX + 10 * scale, ctrlY + 2 * scale);
ctx.stroke();
// Repeat Icon
const repX = cX + 240 * scale;
ctx.beginPath();
ctx.moveTo(repX - 10 * scale, ctrlY - 2 * scale);
ctx.lineTo(repX - 10 * scale, ctrlY - 8 * scale);
ctx.lineTo(repX + 10 * scale, ctrlY - 8 * scale);
ctx.lineTo(repX + 10 * scale, ctrlY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(repX + 6 * scale, ctrlY - 4 * scale);
ctx.lineTo(repX + 10 * scale, ctrlY);
ctx.lineTo(repX + 14 * scale, ctrlY - 4 * scale);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(repX + 10 * scale, ctrlY + 2 * scale);
ctx.lineTo(repX + 10 * scale, ctrlY + 8 * scale);
ctx.lineTo(repX - 10 * scale, ctrlY + 8 * scale);
ctx.lineTo(repX - 10 * scale, ctrlY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(repX - 6 * scale, ctrlY + 4 * scale);
ctx.lineTo(repX - 10 * scale, ctrlY);
ctx.lineTo(repX - 14 * scale, ctrlY + 4 * scale);
ctx.stroke();
return canvas;
}
Apply Changes