Please bookmark this page to avoid losing your image tool!

Image To Spotify Voice Overlay Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

This tool allows you to overlay a Spotify-style music player interface onto any image. You can customize the track title, artist name, and song duration to create a personalized aesthetic. The tool automatically adds a professional dark gradient at the bottom of your image to ensure the text and playback controls—including a play button, skip buttons, shuffle, repeat, and a visual soundwave—are clearly visible. This is ideal for creating social media posts, aesthetic wallpapers, or custom graphics that mimic a music streaming interface.

Leave a Reply

Your email address will not be published. Required fields are marked *