canvas 转换后,如何在 canvas 2d 上获取正在绘制的对象的二维尺寸以进行命中测试?

How to get the 2d dimensions of the object being drawn for hit test on canvas 2d after canvas transformations?

我在 2d canvas 上绘制简单的形状,同时像这样对形状应用变换:

  const rect = ({ x, y, width, height }) => {
    ctx.fillStyle = 'black';
    ctx.fillRect(x, y, width, height);
  };

  const transform = ({ translate, rotate, scale }, f) => {
    // ctx is a 2d canvas context
    ctx.save();

    if (translate) {
      ctx.translate(translate[0], translate[1]);
    }
    if (rotate) {
      ctx.rotate(rotate);
    }

    if (scale) {
      ctx.scale(scale[0], scale[1]);
    }

    f(ctx);

    ctx.restore();
  };
  const draw = () => {
     transform({ translate: [10, 10] }, () => {
        rect({ x: 0, y: 0, width: 10, height: 10 });
     });
  };

现在我需要知道 canvas space 中这个矩形的尺寸,以便我可以针对鼠标单击位置进行命中测试。

之前我问过这个关于 webgl 命中测试检测的问题 。但是解决方案在这里不适用,因为我没有转换矩阵。

一个可能的解决方案是,我在不同的 canvas 上绘制相同的对象,称为碰撞 canvas,具有与对象相关的特定颜色,稍后当我想针对某个位置进行命中测试时在 canvas 上,我查询该位置上的碰撞 canvas 颜色并查看颜色是否与对象特定颜色相匹配,这是个好主意吗?

我认为最好的解决方案是使用 ctx.currentTransform 方法。根据已知对象的尺寸,可以通过此函数找到转换后的尺寸:

function applyTransform(bounds, currentTransform) {
  bounds.x = ct.e + bounds.x * ct.a;
  bounds.y = ct.f + bounds.y * ct.d;
  bounds.width = bounds.width * ct.a;
  bounds.height = bounds.height * ct.d;
}

这真的取决于你的问题是什么。您写道:

How to get the 2d dimensions of the object being drawn

你写了

for hit testing.

你想要哪个。您想要二维尺寸还是想要命中测试?

对于尺寸,您需要在变形前自行了解形状的大小。然后你可以用 ctx.currentTransform

得到当前的变换

不幸的是,截至 2019 年 8 月,currentTransform 仅在 Chrome 上受支持,因此您需要某种 polyfill,但如果您搜索 "currentTransform polyfill",那里有几个。

对于命中测试,您可以使用 ctx.isPointInPath

你定义了一条路径。它不一定要与您正在绘制的东西相同,当然如果是这样的话它是有道理的。然后你可以调用

ctx.isPointInPath(pathToCheck, canvasRelativeX, canvasRelativeY);

const ctx = document.querySelector('canvas').getContext('2d');

const path = new Path2D();
const points = [
 [10, 0],
 [20, 0],
 [20, 10],
 [30, 10],
 [30, 20],
 [20, 20],
 [20, 30],
 [10, 30],
 [10, 20],
 [0, 20],
 [0, 10],
 [10, 10],
];
points.forEach(p => path.lineTo(...p));
path.closePath();

let mouseX;
let mouseY;

function render(time) {
  const t = time / 1000;
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.translate(
      150 + Math.sin(t * 0.1) * 100,
       75 + Math.cos(t * 0.2) * 50);
  ctx.rotate(t * 0.3);
  ctx.scale(
       2 + Math.sin(t * 0.4) * 0.5,
       2 + Math.cos(t * 0.5) * 0.5);
       
  const inpath = ctx.isPointInPath(path, mouseX, mouseY);
  ctx.fillStyle = inpath ? 'red' : 'blue';
       
  ctx.fill(path);
  ctx.setTransform(1, 0, 0, 1, 0, 0);  // reset transform
  
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

ctx.canvas.addEventListener('mousemove', (e) => {
  mouseX = e.offsetX * ctx.canvas.width / ctx.canvas.clientWidth;
  mouseY = e.offsetY * ctx.canvas.height / ctx.canvas.clientHeight;
});
canvas { border: 1px solid black; }
<canvas></canvas>