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

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

预览

先查看效果吧,点击这里

碰撞/相交检测方法

首先,检查线的两端是否在圆内。如果直线比圆小得多,这种情况就很可能发生。判断点是否在圆中,参考碰撞检测系列------点与圆碰撞/相交。如果任何一端在内部,则立即返回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的代码库,详细代码结构解析点击这里

相关推荐
普兰店拉马努金10 分钟前
【Canvas与色彩】十二等分多彩隔断圆环
canvas·圆环·隔断
某公司摸鱼前端3 天前
推荐 uniapp 相对好用的海报生成插件
小程序·uni-app·canvas
&活在当下&6 天前
js 实现视频封面截图
前端·javascript·音视频·canvas
xachary11 天前
前端使用 Konva 实现可视化设计器(23)- 绘制曲线、属性面板
前端·canvas·konva
Jiaberrr11 天前
微信小程序教程:如何在个人中心实现头像贴纸功能
前端·微信小程序·小程序·canvas·头像
xiangxiongfly91511 天前
微信小程序-canvas
微信小程序·小程序·canvas
胡西风_foxww12 天前
canvas分享,从入门到入门。
javascript·教程·canvas·入门·分享
刘好念15 天前
[OpenGL]使用OpenGL绘制带纹理三角形
c++·计算机图形学·opengl
漠河愁1 个月前
pdf文件渲染到canvas
canvas·pdf.js·fabirc.js