检测鼠标碰撞 canvas 文本 (JS)
Detect mouse collision canvas text (JS)
如何检测我的鼠标是否在 Canvas 上呈现的一段文本上?例如:
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;"></canvas>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = '30px Arial'
ctx.fillText('Test', 100, 50)
如果无法检测鼠标是否在实际文本上,我考虑过使用 .measureText 来查找渲染文本的边界框,并改为使用它。但这对于旋转的文本效果不佳,我不确定如何找到旋转的边界框。
总结一下:
- 是否可以在 canvas 上呈现的文本上检测鼠标悬停事件?
- 如果不是,有没有什么方法可以使用 .measureText 找到某种旋转的边界框?
提前致谢!
getTransform()
method does return a DOMMatrix object representing the context's current transformation, which itself has a transformPoint()
方法可以用来转换 DOMPoints(实际上任何带有 x
、y
、z
或 w
数字 属性).
因此,如果您想检查一个点是否在转换后的 BBox 中,您只需使用反向当前上下文的转换来转换该点,并检查此转换后的点是否适合原始 BBox。
function checkCollision() {
const mat = ctx.getTransform().inverse()
const pt = mat.transformPoint( viewport_point );
const x = pt.x - text_pos.x;
const y = pt.y - text_pos.y;
const collides = x >= text_bbox.left &&
x <= text_bbox.right &&
y >= text_bbox.top &&
y <= text_bbox.bottom;
//...
}
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const width = canvas.width = 500;
const height = canvas.height = 180;
const text = "Hello world";
ctx.font = "800 40px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const text_pos = { x: width / 2, y: height / 2 };
const text_bbox = getTextBBox( ctx, text );
const bbox_path = drawBBoxPath( text_bbox, text_pos );
const start_time = performance.now();
ctx.strokeStyle = "red";
let viewport_point = new DOMPoint(0, 0);
onmousemove = ( { clientX, clientY } ) => {
const canvas_bbox = canvas.getBoundingClientRect();
viewport_point = new DOMPoint(
clientX - canvas_bbox.left,
clientY - canvas_bbox.top
);
};
anim();
function getTextBBox( ctx, text ) {
const metrics = ctx.measureText( text );
const left = metrics.actualBoundingBoxLeft * -1;
const top = metrics.actualBoundingBoxAscent * -1;
const right = metrics.actualBoundingBoxRight;
const bottom = metrics.actualBoundingBoxDescent;
const width = right - left;
const height = bottom - top;
return { left, top, right, bottom, width, height };
}
function drawBBoxPath( bbox, offset = { x: 0, y: 0 } ) {
const path = new Path2D();
const { left, top, width, height } = bbox;
path.rect( left + offset.x, top + offset.y, width, height );
return path;
}
function anim( t ) {
clear();
updateTransform( t );
checkCollision();
drawText();
drawBoundingBox();
requestAnimationFrame( anim );
}
function clear() {
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, width, height );
}
function updateTransform( t ) {
const dur = 10000;
const delta = (t - start_time) % dur;
const pos = Math.PI * 2 / dur * delta;
const angle = Math.cos( pos );
const scale = Math.sin( pos ) * 4;
ctx.translate( width / 2, height / 2 );
ctx.rotate( angle );
ctx.scale( scale, 1 );
ctx.translate( -width / 2, -height / 2 );
}
function checkCollision() {
const mat = ctx.getTransform().inverse()
const pt = mat.transformPoint( viewport_point );
const x = pt.x - text_pos.x;
const y = pt.y - text_pos.y;
const collides = x >= text_bbox.left &&
x <= text_bbox.right &&
y >= text_bbox.top &&
y <= text_bbox.bottom;
ctx.fillStyle = collides ? "green" : "blue";
}
function drawText() {
ctx.fillText( text, text_pos.x, text_pos.y );
}
function drawBoundingBox() {
ctx.stroke( bbox_path );
}
<canvas></canvas>
当然也可以更懒惰,让上下文的 isPointInPath()
方法为我们完成所有这些:
function checkCollision() {
const collides = ctx.isPointInPath( bbox_path, viewport_point.x, viewport_point.y );
//...
}
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const width = canvas.width = 500;
const height = canvas.height = 180;
const text = "Hello world";
ctx.font = "800 40px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const text_pos = { x: width / 2, y: height / 2 };
const text_bbox = getTextBBox( ctx, text );
const bbox_path = drawBBoxPath( text_bbox, text_pos );
const start_time = performance.now();
ctx.strokeStyle = "red";
let viewport_point = new DOMPoint(0, 0);
onmousemove = ( { clientX, clientY } ) => {
const canvas_bbox = canvas.getBoundingClientRect();
viewport_point = new DOMPoint(
clientX - canvas_bbox.left,
clientY - canvas_bbox.top
);
};
anim();
function getTextBBox( ctx, text ) {
const metrics = ctx.measureText( text );
const left = metrics.actualBoundingBoxLeft * -1;
const top = metrics.actualBoundingBoxAscent * -1;
const right = metrics.actualBoundingBoxRight;
const bottom = metrics.actualBoundingBoxDescent;
const width = right - left;
const height = bottom - top;
return { left, top, right, bottom, width, height };
}
function drawBBoxPath( bbox, offset = { x: 0, y: 0 } ) {
const path = new Path2D();
const { left, top, width, height } = bbox;
path.rect( left + offset.x, top + offset.y, width, height );
return path;
}
function anim( t ) {
clear();
updateTransform( t );
checkCollision();
drawText();
drawBoundingBox();
requestAnimationFrame( anim );
}
function clear() {
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, width, height );
}
function updateTransform( t ) {
const dur = 10000;
const delta = (t - start_time) % dur;
const pos = Math.PI * 2 / dur * delta;
const angle = Math.cos( pos );
const scale = Math.sin( pos ) * 4;
ctx.translate( width / 2, height / 2 );
ctx.rotate( angle );
ctx.scale( scale, 1 );
ctx.translate( -width / 2, -height / 2 );
}
function checkCollision() {
const collides = ctx.isPointInPath( bbox_path, viewport_point.x, viewport_point.y );
ctx.fillStyle = collides ? "green" : "blue";
}
function drawText() {
ctx.fillText( text, text_pos.x, text_pos.y );
}
function drawBoundingBox() {
ctx.stroke( bbox_path );
}
<canvas></canvas>
关于与文本绘制像素的碰撞,
canvas API 不公开文本描摹,因此如果您想知道鼠标何时真正位于 fillText()
方法绘制的像素上,您必须阅读绘制此文本后的像素数据。
一旦我们得到像素数据,我们只需要使用与第一个片段中相同的方法,并检查变换点坐标处的像素是否被绘制。
// at init, draw once, untransformed,
// ensure it's the only thing being painted on the canvas
clear();
drawText();
// grab the pixels data, once
const img_data = ctx.getImageData( 0, 0, width, height );
const pixels_data = new Uint32Array( img_data.data.buffer );
function checkCollision() {
const mat = ctx.getTransform().inverse();
const { x, y } = mat.transformPoint( viewport_point );
const index = (Math.floor( y ) * width) + Math.floor( x );
const collides = !!pixels_data[ index ];
//...
}
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const width = canvas.width = 500;
const height = canvas.height = 180;
const text = "Hello world";
const font_settings = {
font: "800 40px sans-serif",
textAlign: "center",
textBaseline: "middle"
};
Object.assign( ctx, font_settings );
const text_pos = {
x: width / 2,
y: height / 2
};
let clicked = false;
onclick = e => clicked = true;
// grab the pixels data
const pixels_data = (() => {
// draw once, untransformed on a new context
// getting the image data of a context will mark it as
// deaccelerated and since we only want to do this once
// it's not a good idea to do it on our visible canvas
// also it helps ensure it's the only thing being painted on the canvas
const temp_canvas = canvas.cloneNode();
const temp_ctx = temp_canvas.getContext("2d");
Object.assign( temp_ctx, font_settings);
drawText(temp_ctx);
const img_data = temp_ctx.getImageData( 0, 0, width, height );
// Safari has issues releasing canvas buffers...
temp_canvas.width = temp_canvas.height = 0;
return new Uint32Array( img_data.data.buffer );
})();
const start_time = performance.now();
let viewport_point = new DOMPoint( 0, 0 );
onmousemove = ( { clientX, clientY } ) => {
const canvas_bbox = canvas.getBoundingClientRect();
viewport_point = new DOMPoint(
clientX - canvas_bbox.left,
clientY - canvas_bbox.top
);
};
anim();
function anim( t ) {
clear();
updateTransform( t );
checkCollision();
drawText();
requestAnimationFrame( anim );
}
function clear() {
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, width, height );
}
function updateTransform( t ) {
const dur = 10000;
const delta = (t - start_time) % dur;
const pos = Math.PI * 2 / dur * delta;
const angle = Math.cos( pos );
const scale = Math.sin( pos ) * 4;
ctx.translate( width / 2, height / 2 );
ctx.rotate( angle );
ctx.scale( scale, 1 );
ctx.translate( -width / 2, -height / 2 );
}
function checkCollision() {
const mat = ctx.getTransform().inverse();
const { x, y } = mat.transformPoint( viewport_point );
const index = (Math.floor( y ) * width) + Math.floor( x );
const collides = !!pixels_data[ index ];
ctx.fillStyle = collides ? "green" : "blue";
}
function drawBoundingBox() {
ctx.stroke( bbox_path );
}
// used by both 'ctx' and 'temp_ctx'
function drawText(context = ctx) {
context.fillText( text, text_pos.x, text_pos.y );
}
<canvas></canvas>
如何检测我的鼠标是否在 Canvas 上呈现的一段文本上?例如:
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;"></canvas>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = '30px Arial'
ctx.fillText('Test', 100, 50)
如果无法检测鼠标是否在实际文本上,我考虑过使用 .measureText 来查找渲染文本的边界框,并改为使用它。但这对于旋转的文本效果不佳,我不确定如何找到旋转的边界框。
总结一下:
- 是否可以在 canvas 上呈现的文本上检测鼠标悬停事件?
- 如果不是,有没有什么方法可以使用 .measureText 找到某种旋转的边界框?
提前致谢!
getTransform()
method does return a DOMMatrix object representing the context's current transformation, which itself has a transformPoint()
方法可以用来转换 DOMPoints(实际上任何带有 x
、y
、z
或 w
数字 属性).
因此,如果您想检查一个点是否在转换后的 BBox 中,您只需使用反向当前上下文的转换来转换该点,并检查此转换后的点是否适合原始 BBox。
function checkCollision() {
const mat = ctx.getTransform().inverse()
const pt = mat.transformPoint( viewport_point );
const x = pt.x - text_pos.x;
const y = pt.y - text_pos.y;
const collides = x >= text_bbox.left &&
x <= text_bbox.right &&
y >= text_bbox.top &&
y <= text_bbox.bottom;
//...
}
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const width = canvas.width = 500;
const height = canvas.height = 180;
const text = "Hello world";
ctx.font = "800 40px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const text_pos = { x: width / 2, y: height / 2 };
const text_bbox = getTextBBox( ctx, text );
const bbox_path = drawBBoxPath( text_bbox, text_pos );
const start_time = performance.now();
ctx.strokeStyle = "red";
let viewport_point = new DOMPoint(0, 0);
onmousemove = ( { clientX, clientY } ) => {
const canvas_bbox = canvas.getBoundingClientRect();
viewport_point = new DOMPoint(
clientX - canvas_bbox.left,
clientY - canvas_bbox.top
);
};
anim();
function getTextBBox( ctx, text ) {
const metrics = ctx.measureText( text );
const left = metrics.actualBoundingBoxLeft * -1;
const top = metrics.actualBoundingBoxAscent * -1;
const right = metrics.actualBoundingBoxRight;
const bottom = metrics.actualBoundingBoxDescent;
const width = right - left;
const height = bottom - top;
return { left, top, right, bottom, width, height };
}
function drawBBoxPath( bbox, offset = { x: 0, y: 0 } ) {
const path = new Path2D();
const { left, top, width, height } = bbox;
path.rect( left + offset.x, top + offset.y, width, height );
return path;
}
function anim( t ) {
clear();
updateTransform( t );
checkCollision();
drawText();
drawBoundingBox();
requestAnimationFrame( anim );
}
function clear() {
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, width, height );
}
function updateTransform( t ) {
const dur = 10000;
const delta = (t - start_time) % dur;
const pos = Math.PI * 2 / dur * delta;
const angle = Math.cos( pos );
const scale = Math.sin( pos ) * 4;
ctx.translate( width / 2, height / 2 );
ctx.rotate( angle );
ctx.scale( scale, 1 );
ctx.translate( -width / 2, -height / 2 );
}
function checkCollision() {
const mat = ctx.getTransform().inverse()
const pt = mat.transformPoint( viewport_point );
const x = pt.x - text_pos.x;
const y = pt.y - text_pos.y;
const collides = x >= text_bbox.left &&
x <= text_bbox.right &&
y >= text_bbox.top &&
y <= text_bbox.bottom;
ctx.fillStyle = collides ? "green" : "blue";
}
function drawText() {
ctx.fillText( text, text_pos.x, text_pos.y );
}
function drawBoundingBox() {
ctx.stroke( bbox_path );
}
<canvas></canvas>
当然也可以更懒惰,让上下文的 isPointInPath()
方法为我们完成所有这些:
function checkCollision() {
const collides = ctx.isPointInPath( bbox_path, viewport_point.x, viewport_point.y );
//...
}
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const width = canvas.width = 500;
const height = canvas.height = 180;
const text = "Hello world";
ctx.font = "800 40px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const text_pos = { x: width / 2, y: height / 2 };
const text_bbox = getTextBBox( ctx, text );
const bbox_path = drawBBoxPath( text_bbox, text_pos );
const start_time = performance.now();
ctx.strokeStyle = "red";
let viewport_point = new DOMPoint(0, 0);
onmousemove = ( { clientX, clientY } ) => {
const canvas_bbox = canvas.getBoundingClientRect();
viewport_point = new DOMPoint(
clientX - canvas_bbox.left,
clientY - canvas_bbox.top
);
};
anim();
function getTextBBox( ctx, text ) {
const metrics = ctx.measureText( text );
const left = metrics.actualBoundingBoxLeft * -1;
const top = metrics.actualBoundingBoxAscent * -1;
const right = metrics.actualBoundingBoxRight;
const bottom = metrics.actualBoundingBoxDescent;
const width = right - left;
const height = bottom - top;
return { left, top, right, bottom, width, height };
}
function drawBBoxPath( bbox, offset = { x: 0, y: 0 } ) {
const path = new Path2D();
const { left, top, width, height } = bbox;
path.rect( left + offset.x, top + offset.y, width, height );
return path;
}
function anim( t ) {
clear();
updateTransform( t );
checkCollision();
drawText();
drawBoundingBox();
requestAnimationFrame( anim );
}
function clear() {
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, width, height );
}
function updateTransform( t ) {
const dur = 10000;
const delta = (t - start_time) % dur;
const pos = Math.PI * 2 / dur * delta;
const angle = Math.cos( pos );
const scale = Math.sin( pos ) * 4;
ctx.translate( width / 2, height / 2 );
ctx.rotate( angle );
ctx.scale( scale, 1 );
ctx.translate( -width / 2, -height / 2 );
}
function checkCollision() {
const collides = ctx.isPointInPath( bbox_path, viewport_point.x, viewport_point.y );
ctx.fillStyle = collides ? "green" : "blue";
}
function drawText() {
ctx.fillText( text, text_pos.x, text_pos.y );
}
function drawBoundingBox() {
ctx.stroke( bbox_path );
}
<canvas></canvas>
关于与文本绘制像素的碰撞,
canvas API 不公开文本描摹,因此如果您想知道鼠标何时真正位于 fillText()
方法绘制的像素上,您必须阅读绘制此文本后的像素数据。
一旦我们得到像素数据,我们只需要使用与第一个片段中相同的方法,并检查变换点坐标处的像素是否被绘制。
// at init, draw once, untransformed,
// ensure it's the only thing being painted on the canvas
clear();
drawText();
// grab the pixels data, once
const img_data = ctx.getImageData( 0, 0, width, height );
const pixels_data = new Uint32Array( img_data.data.buffer );
function checkCollision() {
const mat = ctx.getTransform().inverse();
const { x, y } = mat.transformPoint( viewport_point );
const index = (Math.floor( y ) * width) + Math.floor( x );
const collides = !!pixels_data[ index ];
//...
}
const canvas = document.querySelector( "canvas" );
const ctx = canvas.getContext( "2d" );
const width = canvas.width = 500;
const height = canvas.height = 180;
const text = "Hello world";
const font_settings = {
font: "800 40px sans-serif",
textAlign: "center",
textBaseline: "middle"
};
Object.assign( ctx, font_settings );
const text_pos = {
x: width / 2,
y: height / 2
};
let clicked = false;
onclick = e => clicked = true;
// grab the pixels data
const pixels_data = (() => {
// draw once, untransformed on a new context
// getting the image data of a context will mark it as
// deaccelerated and since we only want to do this once
// it's not a good idea to do it on our visible canvas
// also it helps ensure it's the only thing being painted on the canvas
const temp_canvas = canvas.cloneNode();
const temp_ctx = temp_canvas.getContext("2d");
Object.assign( temp_ctx, font_settings);
drawText(temp_ctx);
const img_data = temp_ctx.getImageData( 0, 0, width, height );
// Safari has issues releasing canvas buffers...
temp_canvas.width = temp_canvas.height = 0;
return new Uint32Array( img_data.data.buffer );
})();
const start_time = performance.now();
let viewport_point = new DOMPoint( 0, 0 );
onmousemove = ( { clientX, clientY } ) => {
const canvas_bbox = canvas.getBoundingClientRect();
viewport_point = new DOMPoint(
clientX - canvas_bbox.left,
clientY - canvas_bbox.top
);
};
anim();
function anim( t ) {
clear();
updateTransform( t );
checkCollision();
drawText();
requestAnimationFrame( anim );
}
function clear() {
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
ctx.clearRect( 0, 0, width, height );
}
function updateTransform( t ) {
const dur = 10000;
const delta = (t - start_time) % dur;
const pos = Math.PI * 2 / dur * delta;
const angle = Math.cos( pos );
const scale = Math.sin( pos ) * 4;
ctx.translate( width / 2, height / 2 );
ctx.rotate( angle );
ctx.scale( scale, 1 );
ctx.translate( -width / 2, -height / 2 );
}
function checkCollision() {
const mat = ctx.getTransform().inverse();
const { x, y } = mat.transformPoint( viewport_point );
const index = (Math.floor( y ) * width) + Math.floor( x );
const collides = !!pixels_data[ index ];
ctx.fillStyle = collides ? "green" : "blue";
}
function drawBoundingBox() {
ctx.stroke( bbox_path );
}
// used by both 'ctx' and 'temp_ctx'
function drawText(context = ctx) {
context.fillText( text, text_pos.x, text_pos.y );
}
<canvas></canvas>