local · browser-only · nothing uploaded

Mask Editor · timeline hidden layer

Upload an image, paint areas to keep visible. Unpainted areas become a hidden layer with 2×2 transparency pattern. Adjust brightness & retention for timeline preview. Export 4096px PNG.
📱 timeline (downsampled)
🔓 opened (original)
Upload an image to start
Brush keeps pixels opaque · Eraser makes them hidden (2×2 pattern) · Export writes 4096px long edge with indexed transparency
🖱️ Mouse click position fix — guide & code

Why the cursor and paint position don't match:

  • canvas.width = logical pixels (e.g., 800)
  • canvas.getBoundingClientRect().width = CSS rendered pixels (e.g., 400px on screen)
  • If you don't multiply by the scale factor, your click lands at half the intended position.

Fix: use the correct scale factor

function getImageCoordsFromEvent(ev){
  const rect = canvas.getBoundingClientRect();
  let localX = ev.offsetX || (ev.clientX - rect.left);
  let localY = ev.offsetY || (ev.clientY - rect.top);
  localX = Math.min(rect.width - 1, Math.max(0, localX));
  localY = Math.min(rect.height - 1, Math.max(0, localY));
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  return {
    x: Math.min(canvas.width - 1, Math.max(0, Math.round(localX * scaleX))),
    y: Math.min(canvas.height - 1, Math.max(0, Math.round(localY * scaleY))),
    clientX: ev.clientX,
    clientY: ev.clientY
  };
}