How do I get the coordinates of a mouse click on a canvas element?

后端 未结 22 2381
忘掉有多难
忘掉有多难 2020-11-21 23:56

What\'s the simplest way to add a click event handler to a canvas element that will return the x and y coordinates of the click (relative to the canvas element)?

No

22条回答
  •  抹茶落季
    2020-11-22 00:42

    So this is both simple but a slightly more complicated topic than it seems.

    First off there are usually to conflated questions here

    1. How to get element relative mouse coordinates

    2. How to get canvas pixel mouse coordinates for the 2D Canvas API or WebGL

    so, answers

    How to get element relative mouse coordinates

    Whether or not the element is a canvas getting element relative mouse coordinates is the same for all elements.

    There are 2 simple answers to the question "How to get canvas relative mouse coordinates"

    Simple answer #1 use offsetX and offsetY

    canvas.addEventListner('mousemove', (e) => {
      const x = e.offsetX;
      const y = e.offsetY;
    });
    

    This answer works in Chrome, Firefox, and Safari. Unlike all the other event values offsetX and offsetY take CSS transforms into account.

    The biggest problem with offsetX and offsetY is as of 2019/05 they don't exist on touch events and so can't be used with iOS Safari. They do exist on Pointer Events which exist in Chrome and Firefox but not Safari although apparently Safari is working on it.

    Another issue is the events must be on the canvas itself. If you put them on some other element or the window you can not later choose the canvas to be your point of reference.

    Simple answer #2 use clientX, clientY and canvas.getBoundingClientRect

    If you don't care about CSS transforms the next simplest answer is to call canvas. getBoundingClientRect() and subtract the left from clientX and top from clientY as in

    canvas.addEventListener('mousemove', (e) => {
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
    });
    

    This will work as long as there are no CSS transforms. It also works with touch events and so will work with Safari iOS

    canvas.addEventListener('touchmove', (e) => {
      const rect = canvas. getBoundingClientRect();
      const x = e.touches[0].clientX - rect.left;
      const y = e.touches[0].clientY - rect.top;
    });
    

    How to get canvas pixel mouse coordinates for the 2D Canvas API

    For this we need to take the values we got above and convert from the size the canvas is displayed to the number of pixels in the canvas itself

    with canvas.getBoundingClientRect and clientX and clientY

    canvas.addEventListener('mousemove', (e) => {
      const rect = canvas.getBoundingClientRect();
      const elementRelativeX = e.clientX - rect.left;
      const elementRelativeY = e.clientY - rect.top;
      const canvasRelativeX = elementRelativeX * canvas.width / rect.width;
      const canvasRelativeY = elementRelativeY * canvas.height / rect.height;
    });
    

    or with offsetX and offsetY

    canvas.addEventListener('mousemove', (e) => {
      const elementRelativeX = e.offsetX;
      const elementRelativeX = e.offsetY;
      const canvasRelativeX = elementRelativeX * canvas.width / canvas.clientWidth;
      const canvasRelativeY = elementRelativeX * canvas.height / canvas.clientHeight;
    });
    

    Note: In all cases do not add padding or borders to the canvas. Doing so will massively complicate the code. Instead of you want a border or padding surround the canvas in some other element and add the padding and or border to the outer element.

    Working example using event.offsetX, event.offsetY

    [...document.querySelectorAll('canvas')].forEach((canvas) => {
      const ctx = canvas.getContext('2d');
      ctx.canvas.width  = ctx.canvas.clientWidth;
      ctx.canvas.height = ctx.canvas.clientHeight;
      let count = 0;
    
      function draw(e, radius = 1) {
        const pos = {
          x: e.offsetX * canvas.width  / canvas.clientWidth,
          y: e.offsetY * canvas.height / canvas.clientHeight,
        };
        document.querySelector('#debug').textContent = count;
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
        ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5);
        ctx.fill();
      }
    
      function preventDefault(e) {
        e.preventDefault();
      }
    
      if (window.PointerEvent) {
        canvas.addEventListener('pointermove', (e) => {
          draw(e, Math.max(Math.max(e.width, e.height) / 2, 1));
        });
        canvas.addEventListener('touchstart', preventDefault, {passive: false});
        canvas.addEventListener('touchmove', preventDefault, {passive: false});
      } else {
        canvas.addEventListener('mousemove', draw);
        canvas.addEventListener('mousedown', preventDefault);
      }
    });
    
    function hsl(h, s, l) {
      return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
    }
    .scene {
      width: 200px;
      height: 200px;
      perspective: 600px;
    }
    
    .cube {
      width: 100%;
      height: 100%;
      position: relative;
      transform-style: preserve-3d;
      animation-duration: 16s;
      animation-name: rotate;
      animation-iteration-count: infinite;
      animation-timing-function: linear;
    }
    
    @keyframes rotate {
      from { transform: translateZ(-100px) rotateX(  0deg) rotateY(  0deg); }
      to   { transform: translateZ(-100px) rotateX(360deg) rotateY(720deg); }
    }
    
    .cube__face {
      position: absolute;
      width: 200px;
      height: 200px;
      display: block;
    }
    
    .cube__face--front  { background: rgba(255, 0, 0, 0.2); transform: rotateY(  0deg) translateZ(100px); }
    .cube__face--right  { background: rgba(0, 255, 0, 0.2); transform: rotateY( 90deg) translateZ(100px); }
    .cube__face--back   { background: rgba(0, 0, 255, 0.2); transform: rotateY(180deg) translateZ(100px); }
    .cube__face--left   { background: rgba(255, 255, 0, 0.2); transform: rotateY(-90deg) translateZ(100px); }
    .cube__face--top    { background: rgba(0, 255, 255, 0.2); transform: rotateX( 90deg) translateZ(100px); }
    .cube__face--bottom { background: rgba(255, 0, 255, 0.2); transform: rotateX(-90deg) translateZ(100px); }

    Working example using canvas.getBoundingClientRect and event.clientX and event.clientY

    const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');
    ctx.canvas.width  = ctx.canvas.clientWidth;
    ctx.canvas.height = ctx.canvas.clientHeight;
    let count = 0;
    
    function draw(e, radius = 1) {
      const rect = canvas.getBoundingClientRect();
      const pos = {
        x: (e.clientX - rect.left) * canvas.width  / canvas.clientWidth,
        y: (e.clientY - rect.top) * canvas.height / canvas.clientHeight,
      };
      ctx.beginPath();
      ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
      ctx.fillStyle = hsl((count++ % 100) / 100, 1, 0.5);
      ctx.fill();
    }
    
    function preventDefault(e) {
      e.preventDefault();
    }
    
    if (window.PointerEvent) {
      canvas.addEventListener('pointermove', (e) => {
        draw(e, Math.max(Math.max(e.width, e.height) / 2, 1));
      });
      canvas.addEventListener('touchstart', preventDefault, {passive: false});
      canvas.addEventListener('touchmove', preventDefault, {passive: false});
    } else {
      canvas.addEventListener('mousemove', draw);
      canvas.addEventListener('mousedown', preventDefault);
    }
    
    function hsl(h, s, l) {
      return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
    }
    canvas { background: #FED; }
    
    
    canvas deliberately has differnt CSS size vs drawingbuffer size

提交回复
热议问题