【问题标题】:Detect mouse collision canvas text (JS)检测鼠标碰撞画布文本(JS)
【发布时间】:2021-07-04 16:09:50
【问题描述】:

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

<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 找到某种旋转的边界框吗?

提前谢谢你!

【问题讨论】:

    标签: javascript canvas text mouse collision


    【解决方案1】:

    getTransform() 方法确实返回一个代表上下文当前转换的DOMMatrix 对象,它本身有一个transformPoint() 方法可以用来转换DOMPoints(实际上是任何带有@ 987654327@、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 );
    }
    &lt;canvas&gt;&lt;/canvas&gt;

    当然也可以更懒一点,让上下文的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 );
    }
    &lt;canvas&gt;&lt;/canvas&gt;

    关于与文本绘制像素的碰撞,
    canvas API 不公开文本的跟踪,因此如果您想知道鼠标何时真正位于由fillText() 方法绘制的像素上,则必须在绘制此文本后读取像素数据。

    一旦我们得到像素数据,我们只需使用与第一个 sn-p 中相同的方法,并检查变换点坐标处的像素是否被绘制。

    // 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 );
    }
    &lt;canvas&gt;&lt;/canvas&gt;

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-08-28
      • 2021-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-03
      • 1970-01-01
      相关资源
      最近更新 更多