【问题标题】:How to get the 2d dimensions of the object being drawn for hit test on canvas 2d after canvas transformations?如何在画布转换后获取正在绘制的对象的 2d 尺寸以在画布 2d 上进行命中测试?
【发布时间】:2020-01-02 13:28:14
【问题描述】:

我在 2d 画布上绘制简单的形状,同时对形状应用如下变换:

  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 });
     });
  };

现在我需要知道这个矩形在画布空间中的尺寸,以便我可以对鼠标单击位置进行测试。

之前我问过这个问题How to get the 2d dimensions of the object being drawn for hit test on webgl after model view transform 关于 webgl 命中测试检测。但该解决方案不适用于此处,因为我没有转换矩阵。

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

我认为最好的解决方案是使用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;
}

【问题讨论】:

  • 您当前的命中检测代码是什么?
  • 我还没有,不过我会尝试提到的解决方案

标签: javascript html node.js canvas webgl


【解决方案1】:

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

如何获取被绘制对象的二维尺寸

你写了

用于命中测试。

你想要什么。您想要 2d 维度还是想要命中测试?

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

不幸的是,截至 2019 年 8 月,Chrome 仅支持 currentTransform,因此您需要某种类型的 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>

【讨论】:

  • IsPointInPath 在视图代码中渲染时被调用,但我不想在视图代码中混合逻辑。视图可能会告诉绘制对象的尺寸,控制器稍后会检查这些尺寸是否发生任何冲突。如果我想检查两个正在碰撞的对象怎么办,现在视图代码变得一团糟,不是吗。
  • 有什么区别?您想要对变形后的形状进行命中测试。因此,您要么必须保存命中检查,要么必须保存转换本身。无论哪种方式,您都必须在绘图代码中保存与点击相关的内容。您的另一个选择是自己进行所有矩阵数学运算,然后致电ctx.setTransform(...matrixToComputedElseWhere)。然后,您可以在绘图代码之外计算这些矩阵,并在绘图代码和命中测试中使用它们。您可以自己进行命中测试,也可以在绘制代码之外使用ctx.setTransform(matrixForHitTest); const hit = ctx.isPointInPath(...)
  • 你觉得我的碰撞画布解决方案怎么样,这样你也可以真正看到碰撞箱。
  • 我认为使用画布检查碰撞会很慢,因为您必须检查每个形状的 1000 像素。做碰撞实际上是很多工作。即使是像圆到圆这样的简单碰撞最终也需要某种空间分区。就像如果你想碰撞 100 个圆圈,这将需要大约 5000 次碰撞检查,除非你找到一种在空间上划分圆圈的方法,这样你就不必检查每个圆圈与其他圆圈。这就是人们使用碰撞库的原因,因为优化工作量很大。
  • 如果我想知道该点与对象碰撞的对象的位置怎么办?
猜你喜欢
  • 2020-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-20
  • 2019-05-07
  • 1970-01-01
  • 2018-11-24
  • 2016-09-25
相关资源
最近更新 更多