碰撞检测系列——线与圆碰撞/相交

此文章是碰撞检测系列的第八篇,圆和矩形碰撞检测/相交,此系列主要包含了多种形状的碰撞/相交检测方法。

预览

先查看效果吧,点击这里

碰撞/相交检测方法

首先,检查线的两端是否在圆内。如果直线比圆小得多,这种情况就很可能发生。判断点是否在圆中,参考碰撞检测系列------点与圆碰撞/相交。如果任何一端在内部,则立即返回true并跳过其余部分。

假设存在线l,两点表示法,两点分别为p1,p2;存在圆c,c包含x,y,r

kotlin 复制代码
const inside1 = this.pointCircle(p1, c);;
const inside2 = this.pointCircle(p2, c);
if (inside1 || inside2) return true;

接下来,需要找出圆心到直线上最近的点。首先,让我们用勾股定理求出直线的长度:

ini 复制代码
let distX = p1.x - p2.x;
let distY = p1.y - p2.y;
const len = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2));

然后,我们利用向量,求直线与圆的点积

scss 复制代码
const dot = (((c.x - p1.x) * (p2.x - p1.x)) + ((c.y - p1.y) * (p2.y - p1.y))) / Math.pow(len, 2);

最后,我们可以用下面方程求出直线上最近的点:

ini 复制代码
const closestX = p1.x + (dot * (p2.x - p1.x));
const closestY = p1.y + (dot * (p2.y - p1.y));

注意线和线段不同,线是可以无限延长的,我们这里的线实际上是由两点表示的线段,所以我们需要判断这个最近点是否在这个线段上。关于点是否在线段上,参考碰撞检测系列------线与点碰撞/相交

kotlin 复制代码
// 判断这个点是否在线段上
// 如果是,继续,否则返回false
const onSegment = this.linePoint(l,closePoint);
// if (!onSegment) return false;

最终的最终,计算出圆心与最近的的距离,然后与圆的半径比较,如果小于等与半径,说明相交

ini 复制代码
// 计算圆心到直线上最近点的距离
distX = closestX - c.x;
distY = closestY - c.y;
const distance = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2));
return distance <= c.r

完整代码如下:

ini 复制代码
/**
 * 
 * @param {Array} l line对象/线对象 结构[{x,y},{x,y}] 元素1:起始点; 元素2:结束点;
 * @param {Object} c 圆对象{x,y,r} x/y: 圆心坐标; r:圆半径
 * @returns boolean
 */
function lineCircle(l,c) {
    const [p1, p2] = l;
    // 检查线的两端是否在圆内
    // 如果任何一端在圆中,返回true
    const inside1 = this.pointCircle(p1, c);
    const inside2 = this.pointCircle(p2, c);
    if (inside1 || inside2) return true;

    // 计算线段长度
    let distX = p1.x - p2.x;
    let distY = p1.y - p2.y;
    const len = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2));

    // 求直线与圆的点积
    const dot = (((c.x - p1.x) * (p2.x - p1.x)) + ((c.y - p1.y) * (p2.y - p1.y))) / Math.pow(len, 2);

    // 找到这条线上最近的点
    const closestX = p1.x + (dot * (p2.x - p1.x));
    const closestY = p1.y + (dot * (p2.y - p1.y));

    const closePoint = { x: closestX, y: closestY };
    const hitPoints = hit.hitPoints || (hit.hitPoints = [])
    hitPoints.push(closePoint)
    // 判断这个点是否在线段上
    // 如果是,继续,否则返回false
    const onSegment = this.linePoint(l,closePoint);
    // if (!onSegment) return false;
    if (!onSegment) {
        hitPoints.pop();
        return false
    };

    // 计算圆心到直线上最近点的距离
    distX = closestX - c.x;
    distY = closestY - c.y;
    const distance = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2));
    return distance <= c.r
}

/**
 * 
 * @param {Object} p 点对象{x,y}
 * @param {Object} c 圆对象{x,y,r} x/y: 圆心坐标; r:圆半径
 * @returns boolean
 */
function pointCircle(p, c) {
    // get distance between the point and circle's center
    // using the Pythagorean Theorem
    const distX = p.x - c.x;
    const distY = p.y - c.y;
    const distance = Math.sqrt((distX * distX) + (distY * distY));
    // if the distance is less than the circle's
    // radius the point is inside!
    return distance <= c.r;
}

主要代码

在我的demo中,当点与矩形碰撞/相交改变固定圆的颜色,可以点上面预览进去试试。这里是部分核心代码,详细代码结构解析点击这里 这里主要是渲染和交互代码,由于baseShape和cursorShape默认形状是圆,这里opt中参数需设置baseShape为line,关于line的起始点和结束点,配置在drawOpt中;radius1为baseShape圆的半径;还有必须配置的hitFunc函数

ini 复制代码
const init = readyInit({
	baseShape: "line",radius1:40,
	drawOpt: [{x: 100, y:400},{x: 800, y:100}],
	hitFunc: (e, drawOpt, {radius1}) => {
		const c = { x:e.x, y:e.y, r:radius1 }
		return hit.lineCircle(drawOpt,c)
	}
})

// 图形渲染以及交互
function check(opt) {
    const ctx = utils.getCtx();
    const canvas = ctx.canvas;
    const zoom = opt.zoom || 1;
    const width = canvas.width / zoom;
    const height = canvas.height / zoom;
    const cp = { x: Math.round(width / 2), y: Math.round(height / 2) }
    ctx.scale(zoom, zoom)
    // 基础图形的绘制参数准备开始
    const radius = opt.radius || 10;
    const baseShape = opt.baseShape || 'circle'
    let drawOpt = opt.drawOpt;;
    if (baseShape === 'circle') {
        drawOpt = {...cp, r:radius}
    } else if (baseShape === 'rect') {
        const w = opt.w || 400;
        const h = opt.h || 200;
        drawOpt = {x:(width - w) / 2, y:(height - h) / 2, w, h}
    }
    // 基础图形的绘制参数准备结束

    // 渲染方法
    function render(colliding) {
        utils.cleanCanvas(ctx)
        ctx.fillStyle = '#0095d9E0';
        ctx.strokeStyle = '#0095d9E0';
        if (colliding) {
            // 碰撞时绘制效果
            if (opt.fillRectColliding) {
                // 碰撞时,改变背景图颜色(两点碰撞时使用,由于点太小,效果不明显)
                ctx.save()
                ctx.fillStyle = "#f6ad49";
                ctx.fillRect(0, 0, width, height);
                ctx.restore()
            } else {
                // 碰撞时,改变基础图形绘制颜色
                ctx.fillStyle = "#f6ad49E0";
                ctx.strokeStyle = "#f6ad49E0";
            }
        }
        // 相交的辅助点绘制,不是每个demo都会有
        const hitPoints = hit.hitPoints;
        if (hitPoints) {
            ctx.save()
            ctx.fillStyle = "red";
            hitPoints.forEach(p => {
                drawUtils.circle(ctx, { x: p.x, y: p.y, r: 16 })
            });
            ctx.restore()
        }
        
        ctx.lineWidth = 20;
        ctx.lineJoin = "round";
        ctx.lineCap = "round";
        // 基础图形绘制
        const drawFunc = drawUtils[baseShape];
        if (drawFunc) {
            drawFunc(ctx,drawOpt)
        }
        delete hit.hitPoints;
    }
    const radius1 = opt.radius1 || 10;
    const cursorShape = opt.cursorShape || 'circle'
    canvas.addEventListener('mousemove', (e) => {
        // 调用每个demo配置的hitFunc,检测碰撞结果
        const colliding = opt.hitFunc ? opt.hitFunc(e, drawOpt, opt) : false;
        // 移动鼠标重绘
        render(colliding);

        // 绘制鼠标图形,也就是移动的图形
        ctx.fillStyle = '#6a6868E0';
        if (cursorShape === 'rect') { 
            const w = opt.cursorW || 20;
            const h = opt.cursorH || 20;
            drawUtils.rect(ctx, { x:e.x / zoom - w/2, y:e.y / zoom - h/2, w, h })
        } else if (cursorShape === 'line') {
            ctx.strokeStyle = "#6a6868E0";
            ctx.lineWidth = 20;
            ctx.lineJoin = "round";
            ctx.lineCap = "round";
            drawUtils.line(ctx, [opt.cursorStartPoint,{ x:e.x, y:e.y}])
        } else if (cursorShape === 'polygon') {
            const { x, y } = e;
            const points = [
                { x: x - 20, y: y - 20 },
                { x: x + 40, y: y - 10 },
                { x: x + 60, y: y + 20 },
                { x: x - 20, y: y + 20 },
                {x: x - 40, y: y},
            ]
            drawUtils.polygon(ctx, points)
        } else {
            drawUtils.circle(ctx, { x:e.x / zoom, y:e.y / zoom, r:radius1 })
        }
        
    })
    render();
}

代码涉及到线段的绘制,被抽取为一个工具方法,放在init.js文件中的drawUtils工具对象中

scss 复制代码
function line(ctx, [p1, p2]) {
    ctx.beginPath();
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.stroke();
}

代码下载

以上代码只是主要代码并不是完整代码,由于完整代码较多就不贴出来了,有需要可以点击这里,这是GitHub的代码库,详细代码结构解析点击这里

相关推荐
PineappleCoder3 天前
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
前端·面试·canvas
德育处主任4 天前
p5.js 用 cylinder() 绘制 3D 圆柱体
前端·数据可视化·canvas
蛋蛋_dandan6 天前
Fabric.js从0到1实现图片框选功能
canvas
wayhome在哪8 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任8 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任9 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任11 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼11 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲12 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户25191624271114 天前
Canvas之画图板
前端·javascript·canvas