Please bookmark this page to avoid losing your image tool!

Image Musical Notation Filter Effect

(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.
async function processImage(originalImg, threshold = 128, staffLineColor = "black", noteColor = "black", staffLineThickness = 1, numStaffSets = 5, staffLineGap = 10, staffSetGap = 40, noteSizeRatio = 0.4, noteDensityThreshold = 0.5, stemWidth = 1, stemLengthRatio = 2.5) {

    const W = originalImg.naturalWidth || originalImg.width;
    const H = originalImg.naturalHeight || originalImg.height;

    if (W === 0 || H === 0 || numStaffSets <= 0 || staffLineGap <=0) {
        // Return a blank canvas or original image if dimensions/params are invalid
        const canvas = document.createElement('canvas');
        canvas.width = Math.max(1, W); // Ensure canvas has at least 1x1
        canvas.height = Math.max(1, H);
        const ctx = canvas.getContext('2d', { alpha: false });
        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        if (W > 0 && H > 0) { // Draw original if it's valid
            try { ctx.drawImage(originalImg, 0, 0); } catch(e) { /* ignore if img not drawable */ }
        }
        return canvas;
    }
    
    // 1. Create binarized version of the original image
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = W;
    tempCanvas.height = H;
    const tempCtx = tempCanvas.getContext('2d');
    tempCtx.drawImage(originalImg, 0, 0, W, H);
    const originalImageData = tempCtx.getImageData(0, 0, W, H);
    const data = originalImageData.data;

    for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i+1] + data[i+2]) / 3;
        const value = avg < threshold ? 0 : 255;
        data[i] = data[i+1] = data[i+2] = value;
    }

    // Helper function to check darkness of a block in binarizedImageData
    function isBlockDark(blockX, blockY, blockWidth, blockHeight, binarizedImgData, densityThr) {
        let darkPixels = 0;
        let totalPixelsInBlock = 0;
        const imgW = binarizedImgData.width;
        const imgH = binarizedImgData.height;

        const startScanX = Math.floor(blockX);
        const startScanY = Math.floor(blockY);
        const endScanX = Math.floor(blockX + blockWidth);
        const endScanY = Math.floor(blockY + blockHeight);

        for (let cY = startScanY; cY < endScanY; cY++) {
            for (let cX = startScanX; cX < endScanX; cX++) {
                if (cX >= 0 && cX < imgW && cY >= 0 && cY < imgH) {
                    const pixelStartIndex = (cY * imgW + cX) * 4;
                    if (binarizedImgData.data[pixelStartIndex] === 0) { // 0 is black
                        darkPixels++;
                    }
                    totalPixelsInBlock++;
                }
            }
        }
        if (totalPixelsInBlock === 0) return false;
        return (darkPixels / totalPixelsInBlock) >= densityThr;
    }

    // 2. Prepare output canvas
    const canvas = document.createElement('canvas');
    canvas.width = W;
    canvas.height = H;
    const ctx = canvas.getContext('2d', { alpha: false });
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, W, H);

    // 3. Draw staff lines
    ctx.strokeStyle = staffLineColor;
    ctx.lineWidth = staffLineThickness;
    
    const staffMiddleLinesY = []; 
    const finalNoteSlots = new Set();

    const singleStaffVisualHeight = 4 * staffLineGap; // Height of the 5 lines
    let totalStaffsCombinedHeight = numStaffSets * singleStaffVisualHeight + Math.max(0, numStaffSets - 1) * staffSetGap;
        if (numStaffSets === 0) totalStaffsCombinedHeight = 0;


    let currentY = (H - totalStaffsCombinedHeight) / 2;
    if (currentY < staffLineGap * 0.5) currentY = staffLineGap * 0.5; // Ensure some top margin

    for (let i = 0; i < numStaffSets; i++) {
        const staffTopY = Math.round(currentY);
        if (staffTopY + singleStaffVisualHeight > H + staffLineGap) break; // Avoid staff mostly off-canvas

        const middleLineY = Math.round(staffTopY + 2 * staffLineGap);
        staffMiddleLinesY.push(middleLineY); 
        
        for (let j = 0; j < 5; j++) { // 5 lines
            const lineY = Math.round(staffTopY + j * staffLineGap);
            if (lineY >= 0 && lineY <= H) {
                ctx.beginPath();
                ctx.moveTo(0, lineY);
                ctx.lineTo(W, lineY);
                ctx.stroke();
                finalNoteSlots.add(lineY);
            }
            if (j < 4) { // 4 spaces within a staff
                 const spaceY = Math.round(staffTopY + j * staffLineGap + staffLineGap / 2);
                 if (spaceY >=0 && spaceY <= H) {
                    finalNoteSlots.add(spaceY);
                 }
            }
        }
        currentY = staffTopY + singleStaffVisualHeight + staffSetGap;
    }
    const sortedNoteSlotYCoords = [...finalNoteSlots].sort((a, b) => a - b);

    // 4. Draw notes
    ctx.fillStyle = noteColor;
    // Stems will use noteColor as strokeStyle

    const noteRadius = Math.max(1, noteSizeRatio * staffLineGap);
    const noteStepX = Math.max(noteRadius * 2 + 2, staffLineGap * 0.75, 5); // Horizontal spacing for potential notes
    const blockCheckSize = Math.max(1, staffLineGap); // Size of image area to check for darkness

    for (let x = noteStepX / 2; x < W - noteStepX / 2; x += noteStepX) {
        for (const slotY of sortedNoteSlotYCoords) {
            if (slotY - noteRadius < 0 || slotY + noteRadius > H) continue; 

            const imgBlockX = x - blockCheckSize / 2;
            const imgBlockY = slotY - blockCheckSize / 2;

            if (isBlockDark(imgBlockX, imgBlockY, blockCheckSize, blockCheckSize, originalImageData, noteDensityThreshold)) {
                ctx.beginPath();
                ctx.arc(x, slotY, noteRadius, 0, 2 * Math.PI);
                ctx.fill();

                let parentStaffMiddleY = -1;
                let minStaffDist = Infinity;

                for (const middleY of staffMiddleLinesY) {
                    const distToStaffCenter = Math.abs(slotY - middleY);
                    // Check if slotY is within this staff's span (+/- 2 lines + 1 space from middle)
                    if (distToStaffCenter <= (2 * staffLineGap + staffLineGap / 2 + 1)) { 
                        if (distToStaffCenter < minStaffDist) {
                           minStaffDist = distToStaffCenter;
                           parentStaffMiddleY = middleY;
                        }
                    }
                }
                
                if (parentStaffMiddleY !== -1) {
                    const stemActualLength = stemLengthRatio * staffLineGap;
                    ctx.strokeStyle = noteColor; // Ensure stem color
                    ctx.lineWidth = Math.max(1, stemWidth);
                    ctx.lineCap = 'round'; 
                    ctx.beginPath();
                    
                    // Standard rule: notes on middle line or higher = stem down. Notes below middle line = stem up.
                    // (slotY is pixel coordinate, smaller Y is higher on page/staff)
                    const stemGoesDown = slotY <= parentStaffMiddleY;

                    let stemXPos;
                    if (stemGoesDown) { // Stem on LEFT side of note head
                        stemXPos = x - noteRadius; 
                        ctx.moveTo(stemXPos, slotY); 
                        ctx.lineTo(stemXPos, slotY + stemActualLength);
                    } else { // Stem UP, on RIGHT side of note head
                        stemXPos = x + noteRadius; 
                        ctx.moveTo(stemXPos, slotY);
                        ctx.lineTo(stemXPos, slotY - stemActualLength);
                    }
                    ctx.stroke();
                    ctx.lineCap = 'butt'; // Reset lineCap
                }
            }
        }
    }
    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

The Image Musical Notation Filter Effect is a tool that processes images to simulate musical notation on a staff. It analyzes the original image and applies a binarization technique to identify dark areas that can represent musical notes. Users can customize parameters such as staff line color, note size, spacing, and the number of staff sets to tailor the output to their preference. This tool is useful for musicians and educators who want to create artistic representations of music, generate visual aids for teaching, or transform photographs into musically themed graphics.

Leave a Reply

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