检测鼠标冲突画布文本(JS)

2022-04-09 00:00:00 mouse text collision canvas javascript

如何检测鼠标是否位于画布上呈现的文本上?例如:

<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查找呈现文本的边界框,然后使用该边框。但这对旋转文本不是很有效,我不确定如何找到旋转的边框。

总结:

  1. 是否可以检测画布上呈现的文本上的鼠标悬停事件?
  2. 如果没有,有什么方法可以使用.measureText找到某种旋转的边界框吗?

提前感谢!


解决方案

getTransform()方法返回表示上下文的当前转换的DOMMatrix对象,该对象本身有一个transformPoint()方法可用于转换DOMPoints(实际上是具有xyzw数字属性的任何JS对象)。

因此,如果您要检查某个点是否在转换后的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;
  //...
}
数据-lang="js"数据-隐藏="真"数据-控制台="真"数据-巴贝尔="假">
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 );
  //...
}
数据-lang="js"数据-隐藏="真"数据-控制台="真"数据-巴贝尔="假">
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 ];
  //...
}
数据-lang="js"数据-隐藏="真"数据-控制台="真"数据-巴贝尔="假">
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>

相关文章