You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现跟随鼠标的圆形碰撞后不排斥且平滑绕动?

解决Canvas双圆形平滑贴合绕动问题(类似Agar.io分裂细胞效果)

嘿,我完全懂你想要的效果——两个圆形像Agar.io里的分裂细胞那样,碰到一起时不互相排斥,而是紧紧贴合并平滑绕着对方滑动,同时还持续向鼠标位置移动。你之前尝试的布尔标记和反转加速度的思路没踩对路子,反而导致了融合或者瞬移的问题,我来帮你调整代码实现这个效果。

核心问题分析

你之前的碰撞检测只是判断了重叠,但没有正确处理位置修正和切线方向的移动逻辑:

  • 直接反转加速度会让圆来回抖动甚至“融合”,因为加速度反向后会往回跑,和鼠标的拉力冲突;
  • 简单调整位置又没考虑移动的连续性,所以出现瞬移。

正确的思路应该是:

  1. 当两圆重叠时,先把它们修正到刚好外切的位置;
  2. 让圆沿着两圆连线的切线方向移动(而不是反向),同时保留向鼠标的拉力,实现平滑绕动。

修改后的完整代码

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="cv"></canvas>
    <script>
        // Setting all up
        const canvas = document.getElementById("cv");
        const ctx = canvas.getContext("2d");
        canvas.width = innerWidth;
        canvas.height = innerHeight;

        // Math Variables and utilities
        const PI = Math.PI;
        const TWO_PI = PI * 2;
        const HALF_PI = PI / 2;
        const random = (n1, n2 = 0) => {
            return Math.random() * (n2 - n1) + n1;
        };
        const distance = (x1, y1, x2, y2) => {
            let dX = x1 - x2;
            let dY = y1 - y2;
            return Math.sqrt(dX * dX + dY * dY);
        };
        // 新增:计算向量的单位向量
        const normalize = (x, y) => {
            const mag = Math.sqrt(x*x + y*y);
            return mag === 0 ? {x:0, y:0} : {x: x/mag, y: y/mag};
        };
        // 新增:计算向量的切线方向(垂直方向)
        const getTangent = (x, y) => {
            return {x: -y, y: x};
        };

        let circles = []; // Array that stores the two circles
        let mouse = { x: canvas.width/2, y: canvas.height/2 }; // mouse object

        // Creating the circle class
        function Circle(px, py, r, ctx) {
            this.x = px; // X Position
            this.y = py; // Y Position
            this.r = r; // Radius
            this.ctx = ctx; // Canvas context
            this.acc = 0.3; // 调整加速度,让移动更流畅
            this.velocity = {x:0, y:0}; // 新增速度属性,替代直接修改位置

            // Draw circle function
            this.show = function() {
                this.ctx.beginPath();
                this.ctx.fillStyle = "#fff";
                this.ctx.arc(this.x, this.y, this.r, 0, TWO_PI, false);
                this.ctx.fill();
            };

            this.update = function(targetX, targetY) {
                // 计算指向鼠标的方向向量
                const targetDir = normalize(targetX - this.x, targetY - this.y);
                // 向鼠标方向加速
                this.velocity.x += targetDir.x * this.acc;
                this.velocity.y += targetDir.y * this.acc;
                // 限制速度,避免过快
                const speed = Math.sqrt(this.velocity.x**2 + this.velocity.y**2);
                if (speed > 2) {
                    const ratio = 2 / speed;
                    this.velocity.x *= ratio;
                    this.velocity.y *= ratio;
                }
                // 更新位置
                this.x += this.velocity.x;
                this.y += this.velocity.y;
            };

            // Circle collision handling
            this.handleCollision = function(other) {
                const d = distance(this.x, this.y, other.x, other.y);
                const minDist = this.r + other.r;

                if (d < minDist && d > 0) {
                    // 1. 修正位置:把两个圆推到刚好外切的位置
                    const overlap = minDist - d;
                    const dir = normalize(this.x - other.x, this.y - other.y);
                    // 分摊重叠距离,避免单个圆移动过多
                    this.x += dir.x * overlap * 0.5;
                    this.y += dir.y * overlap * 0.5;
                    other.x -= dir.x * overlap * 0.5;
                    other.y -= dir.y * overlap * 0.5;

                    // 2. 调整速度:保留切线方向的速度,移除径向的冲突分量
                    const tangent = getTangent(dir.x, dir.y);
                    // 计算当前速度在切线上的投影
                    const thisTangentSpeed = this.velocity.x * tangent.x + this.velocity.y * tangent.y;
                    const otherTangentSpeed = other.velocity.x * tangent.x + other.velocity.y * tangent.y;
                    // 更新速度为切线方向,同时保留向鼠标的拉力(这里用平均速度让效果更平滑)
                    this.velocity.x = tangent.x * (thisTangentSpeed + otherTangentSpeed) * 0.5;
                    this.velocity.y = tangent.y * (thisTangentSpeed + otherTangentSpeed) * 0.5;
                }
            }
        }

        // Generating the circles
        const genCircles = () => {
            for (let i = 0; i < 2; i++) {
                circles.push(new Circle(random(canvas.width / 2), random(canvas.height / 2), 50, ctx));
            }
        };
        genCircles();

        // 把鼠标事件监听移到外面,避免重复绑定
        canvas.addEventListener("mousemove", (e) => {
            mouse.x = e.x;
            mouse.y = e.y;
        }, true);

        // Displaying and updating the circles
        const showCircles = () => {
            for (let i = 0; i < circles.length; i++) {
                // 先更新每个圆的位置(向鼠标移动)
                circles[i].update(mouse.x, mouse.y);
                // 然后处理和其他圆的碰撞
                for (let j = i + 1; j < circles.length; j++) {
                    circles[i].handleCollision(circles[j]);
                }
                // 绘制
                circles[i].show();
            }
        };

        // Loop to make it run
        const update = () => {
            requestAnimationFrame(update);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.fillStyle = "#000";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            showCircles();
        };
        update();
    </script>
</body>
</html>

关键改动说明

  1. 新增速度属性:用velocity代替直接修改位置,让移动更平滑,便于后续速度调整
  2. 碰撞处理逻辑优化:
    • 位置修正:计算重叠距离,让两个圆各移动一半的重叠量,避免瞬移
    • 速度调整:提取速度的切线分量,移除径向的冲突速度,让圆沿着对方边缘滑动,而不是反向弹开
  3. 优化事件绑定:把mousemove监听移到循环外,避免每次渲染都重复绑定事件
  4. 速度限制:防止圆因为加速度累积而移动过快,保证平滑性

现在运行代码,你会看到两个圆碰到一起时会紧紧贴合并平滑绕动,同时持续向鼠标位置移动,和Agar.io里的分裂细胞效果一致。

内容的提问来源于stack exchange,提问作者rochismo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:00:44