Canvas 直线点击事件处理优化

在平常Canvas开发中,经常会遇到直线的点击事件问题,对于这类问题通常的做法就是使用isPointInStroke,但直接使用存在一个问题就是直线的宽度较小时,鼠标点击不太容易选中。下面是针对这类问题总结的一些优化方法。

使用isPointInStroke

平常开发中,经常使用isPointInStroke方法判断鼠标点击位置是否位于直线上,常规代码如下:

vue 复制代码
<script setup>
    import { ref, onMounted } from 'vue';
    
    const canvasRef = ref();
    let ctx;
    let isLineSelected = false;
    
    // 直线的起点和终点坐标
    const lineStart = { x: 100, y: 200 };
    const lineEnd = { x: 500, y: 200 };
    
    const clear = () => {
        // 清除画布
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    }
    
    // 绘制直线的函数
    const drawLine = () => {
    
        // 设置线条样式
        ctx.strokeStyle = isLineSelected ? '#ff0000' : '#000000';
        ctx.lineWidth = isLineSelected ? 4 : 2;
    
        // 绘制直线
        ctx.beginPath();
        ctx.moveTo(lineStart.x, lineStart.y);
        ctx.lineTo(lineEnd.x, lineEnd.y);
        ctx.stroke();
    };
    onMounted(() => {
        if (canvasRef.value) {
            const canvas = canvasRef.value;
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            ctx = canvas.getContext('2d');
    
            drawLine();
    
            // 添加鼠标点击事件监听器
            canvasRef.value.addEventListener('click', e => {
                const rect = canvasRef.value.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
    
                if (ctx.isPointInStroke(x, y)) {
                    isLineSelected = !isLineSelected;
                    clear()
                    drawLine();
                }
            });
        }
    });
</script>    

这样我们就可以实现鼠标点击的选中效果,但是这种方法并不完美,当线的宽度较小时,这是就很难选中这条线。 下面我们来优化一下,依旧使用isPointInStroke这个方法,代码如下:

javascript 复制代码
// 检测点击是否在直线上的函数
const isPointOnLine = (x, y) => {
    if (!ctx) return false;

    // 创建直线路径
    ctx.beginPath();
    ctx.moveTo(lineStart.x, lineStart.y);
    ctx.lineTo(lineEnd.x, lineEnd.y);

    // 设置鼠标点击时的容错率
    ctx.lineWidth = 10;

    // 使用 Canvas API 的 isPointInStroke 方法检测点击是否在直线上
    return ctx.isPointInStroke(x, y);
};
if (isPointOnLine(x, y)) {
    isLineSelected = !isLineSelected;
    clear()
    drawLine();
}

我们把判断条件写成一个方法,在判断之前模拟一条起始坐标和终点坐标相同的线,为了解决线的宽度较小时不太容易选中的问题, 我们在模拟这条线是设置一个较大的宽度,这样就可以优化鼠标点击时不容易选中的问题了。

使用点到直线的距离公式

除了使用isPointInStroke方法判断鼠标点击位置是否位于直线上,我们还可以使用点到直线的距离公式判断鼠标点击位置是否位于直线上。计算点到直线的距离公式有很多种方法,比如一般式、参数式、向量式等。因为这里我们已知直线的两个坐标和鼠标点击 位置的坐标,使用向量叉积来计算点到直线的距离更为方便。

点到直线的距离公式如下: 其中, <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 1 , y 1 ) (x1, y1) </math>(x1,y1) 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 2 , y 2 ) (x2, y2) </math>(x2,y2) 是直线的两个坐标, <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 0 , y 0 ) (x0, y0) </math>(x0,y0) 是鼠标点击位置的坐标。代码实现如下:

javascript 复制代码
/**
 * 计算点到直线的距离
 * @param x0 点的 x 坐标
 * @param y0 点的 y 坐标
 * @param x1 直线上一点的 x 坐标
 * @param y1 直线上一点的 y 坐标
 * @param x2 直线上另一点的 x 坐标
 * @param y2 直线上另一点的 y 坐标
 * @param threshold 距离阈值,默认为 10
 * @returns 点到直线的距离是否小于阈值
 */
function pointToLineDistance(x0, y0, x1, y1, x2, y2, threshold = 10) {
  // 计算向量 AB
  const vectorABx = x2 - x1;
  const vectorABy = y2 - y1;

  // 计算向量 AP
  const vectorAPx = x0 - x1;
  const vectorAPy = y0 - y1;

  // 计算叉乘的绝对值(点到直线的距离的分子)
  const crossProduct = Math.abs(vectorABx * vectorAPy - vectorABy * vectorAPx);

  // 计算线段 AB 的长度
  const segmentLength = Math.hypot(vectorABx, vectorABy);

  // 处理线段长度为 0 的情况(两点重合)
  if (segmentLength < 1e-6) {
    // 计算点到点的距离
    const pointDistance = Math.hypot(vectorAPx, vectorAPy);
    return pointDistance < threshold;
  }

  // 计算点到直线的距离
  const distance = crossProduct / segmentLength;

  return distance < threshold;
}

总结

这两种方法都可以解决线的宽度较小时鼠标点击不容易选中的问题。在数据量不是很大的时候,推荐使用isPointInStroke方法, 在Canvas中直线是最小的单位,创建和绘制直线都是非常快的操作,不会对性能造成太大影响。当数据量大的时候,频繁的创建也会导致性能问题,这时候使用数学方法计算点到直线的距离会更加高效,不依赖 Canvas 状态,计算精确,可定制性强。

相关推荐
TA远方15 小时前
【JavaScript】Promise对象使用方式研究和理解
javascript·编程·脚本·web·js·promise·委托
用户1257585243615 小时前
写了三年定时任务还在手改 Cron 表达式?这个 GoFrame 后台框架帮你全闭环了
vue.js
ZC跨境爬虫15 小时前
跟着 MDN 学 HTML day_36:(深入理解 Comment 接口与 DOM 注释节点)
前端·javascript·ui·html·音视频·视频编解码
前端那点事16 小时前
Vue3自定义Hooks保姆级教程!从原理到企业级实战,告别混乱代码
前端·vue.js
前端那点事16 小时前
别再乱用Vue3响应式!ref、reactive、toRef、toRefs完整区别+企业级落地实战
前端·vue.js
閞杺哋笨小孩16 小时前
从脚手架到构建注入:Vue 多租户「入驻」工程实践
vue.js·vite
栉甜16 小时前
Js进阶(4)
开发语言·javascript·原型模式
村上小树17 小时前
非常简单地学习一下shareDB的原理
前端·javascript
卤蛋fg617 小时前
VxeTable 实现表尾合计行并支持数据实时统计
vue.js
Hilaku17 小时前
求求你们🙏 ,别再换打包工具了?
前端·javascript·程序员