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>
我在 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>