You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes