Search code examples
javascriptcanvasgetimagedataputimagedata

Canvas: How to modify ImageData pixels that exist within a custom path?


I'm trying to create an experience where a user can move a custom cursor over an image and alter the pixels that live within that custom cursor.

In the mousemove event, I try to recreate this custom cursor path and use isPointInPath to determine if the custom cursor is "over" a given point in the main canvas's context.

The code does alter some pixels in the main canvas. However, the following unexpected behaviors are listed below:

  • moving the cursor alters the pixels of more than one area in the main canvas
  • the x and y coordinates of the main canvas (derived from imageData) are not correct

(See the image below)

example altered pixels showing the unexpected behavior of the code

Why is this happening? How could the code be changed such that the custom cursor (aka circle) only changes pixels corresponding to that part of the main canvas?

let image;
let imageData;

const cursorCanvas = document.createElement("canvas");
const cursorCanvasContext = cursorCanvas.getContext("2d");
cursorCanvas.width = 100;
cursorCanvas.height = 100;
drawCursor();

const imageCanvas = document.querySelector("canvas");
imageCanvas.width = window.innerWidth;
imageCanvas.height = window.innerHeight;
const imageCanvasContext = imageCanvas.getContext("2d");

document.querySelector("img").addEventListener("load", (event) => {
  image = event.target;
  image.crossOrigin = "Anonymous";
  drawBackground();
  imageData = imageCanvasContext.getImageData(
    0,
    0,
    imageCanvas.width,
    imageCanvas.height
  );
});

window.addEventListener("mousemove", (event) => {
  requestAnimationFrame(() => {
    if (imageData) {
      imageCanvasContext.beginPath();
      imageCanvasContext.arc(
        event.clientX,
        event.clientY,
        cursorCanvas.width / 2,
        0,
        2 * Math.PI
      );
      imageCanvasContext.closePath();

      for (let i = 0; i < imageData.data.length; i += 4) {
        const y = Math.floor(i / imageData.width);
        const x = i - y * imageData.width;

        if (imageCanvasContext.isPointInPath(x, y)) {
          imageData.data[i] = 40;
          imageData.data[i + 1] = 40;
          imageData.data[i + 2] = 40;
        }
      }
      imageCanvasContext.putImageData(imageData, 0, 0);
    }

    imageCanvasContext.drawImage(
      cursorCanvas,
      event.clientX - cursorCanvas.width / 2,
      event.clientY - cursorCanvas.height / 2
    );
  });
});

function drawBackground() {
  imageCanvasContext.clearRect(
    0,
    0,
    imageCanvas.width,
    imageCanvas.height
  );

  if (image) {
    const ratio =
      window.innerWidth > window.innerHeight ?
      window.innerWidth / window.innerHeight :
      window.innerHeight / window.innerWidth;

    imageCanvasContext.drawImage(
      image,
      0,
      0,
      image.width,
      image.height,
      0,
      0,
      ratio * imageCanvas.width,
      ratio * imageCanvas.height
    );
  }
}

function drawCursor() {
  cursorCanvasContext.beginPath();
  cursorCanvasContext.arc(
    cursorCanvas.width / 2,
    cursorCanvas.width / 2,
    cursorCanvas.width / 2,
    0,
    2 * Math.PI
  );
  cursorCanvasContext.stroke();
  cursorCanvasContext.closePath();
}
html,
body {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

body {
  font-family: sans-serif;
  margin: 0;
}

img {
  visibility: hidden;
  position: absolute;
}

canvas {
  cursor: none;
}
<img src="https://i.imgur.com/vzGhITt.jpeg" />
<canvas />


Solution

  • Ah, it seems that I have miscalculated the x and y coordinates of the imageData.

    Incorrect:

    const y = Math.floor(i / imageData.width);
    const x = i - y * imageData.width;
    

    Correct:

    const x = (i / 4) % imageData.width;
    const y = Math.floor(i / 4 / imageData.width);
    

    let image;
    let imageData;
    
    const cursorCanvas = document.createElement("canvas");
    const cursorCanvasContext = cursorCanvas.getContext("2d");
    cursorCanvas.width = 100;
    cursorCanvas.height = 100;
    drawCursor();
    
    const imageCanvas = document.querySelector("canvas");
    imageCanvas.width = window.innerWidth;
    imageCanvas.height = window.innerHeight;
    const imageCanvasContext = imageCanvas.getContext("2d");
    
    document.querySelector("img").addEventListener("load", (event) => {
      image = event.target;
      image.crossOrigin = "Anonymous";
      drawBackground();
      imageData = imageCanvasContext.getImageData(
        0,
        0,
        imageCanvas.width,
        imageCanvas.height
      );
    });
    
    window.addEventListener("mousemove", (event) => {
        if (imageData) {
          imageCanvasContext.beginPath();
          imageCanvasContext.arc(
            event.clientX,
            event.clientY,
            cursorCanvas.width / 2,
            0,
            2 * Math.PI
          );
          imageCanvasContext.closePath();
    
          for (let i = 0; i < imageData.data.length; i += 4) {
            const x = (i / 4) % imageData.width;
            const y = Math.floor(i / 4 / imageData.width);
    
            if (imageCanvasContext.isPointInPath(x, y)) {
              imageData.data[i] = 40;
              imageData.data[i + 1] = 40;
              imageData.data[i + 2] = 40;
            }
          }
          imageCanvasContext.putImageData(imageData, 0, 0);
        }
    
        imageCanvasContext.drawImage(
          cursorCanvas,
          event.clientX - cursorCanvas.width / 2,
          event.clientY - cursorCanvas.height / 2
        );
    });
    
    function drawBackground() {
      imageCanvasContext.clearRect(
        0,
        0,
        imageCanvas.width,
        imageCanvas.height
      );
    
      if (image) {
        const ratio =
          window.innerWidth > window.innerHeight ?
          window.innerWidth / window.innerHeight :
          window.innerHeight / window.innerWidth;
    
        imageCanvasContext.drawImage(
          image,
          0,
          0,
          image.width,
          image.height,
          0,
          0,
          ratio * imageCanvas.width,
          ratio * imageCanvas.height
        );
      }
    }
    
    function drawCursor() {
      cursorCanvasContext.beginPath();
      cursorCanvasContext.arc(
        cursorCanvas.width / 2,
        cursorCanvas.width / 2,
        cursorCanvas.width / 2,
        0,
        2 * Math.PI
      );
      cursorCanvasContext.stroke();
      cursorCanvasContext.closePath();
    }
    html,
    body {
      width: 100vw;
      height: 100vh;
      overflow: hidden;
    }
    
    body {
      font-family: sans-serif;
      margin: 0;
    }
    
    img {
      visibility: hidden;
      position: absolute;
    }
    
    canvas {
      cursor: none;
    }
    <img src="https://i.imgur.com/vzGhITt.jpeg" />
    <canvas />