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

如何实现DOM小球间的碰撞交互?现有弹跳小球代码求助

实现小球碰撞效果的解决方案

我想实现两个小球之间的碰撞效果,考虑过用Intersection Observer,但不知道怎么用。试过追踪所有小球,但没成功,现有弹跳小球代码如下:

现有代码

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title></title>
    <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
    <script type="text/javascript" src="script.js"></script>
</body>
</html>

style.css

*,*::after,*::before {box-sizing: border-box;}:root {--hue: 200;--saturation: 50%;--foreground-color: hsl(var(--hue), var(--saturation), 75%);--background-color: hsl(var(--hue), var(--saturation), 20%);}body {margin: 0;overflow: hidden;background-color: black;}.ball {--x: 50;--y: 50;position: absolute;background-color: lightgrey;left: calc(var(--x) * 1vw);top: calc(var(--y) * 1vh);border-radius: 50%;transform: translate(-50%, -50%);width: 2vh;height: 2vh;}

原script.js

const INITIAL_VELOCITY = 0.085
const VELOCITY_CHANGE_RATE = 0
const NUMBER_OF_BALLS = 2
class Ball {constructor(ballElem) {this.ballElem = ballElem;this.reset();}get x() {return parseFloat(getComputedStyle(this.ballElem).getPropertyValue("--x"));}set x(value) {this.ballElem.style.setProperty("--x", value);}get y() {return parseFloat(getComputedStyle(this.ballElem).getPropertyValue("--y"));}set y(value) {this.ballElem.style.setProperty("--y", value);}rect() {return this.ballElem.getBoundingClientRect();}reset() {this.x = 50;this.y = 50;this.direction = {x: 0};while (Math.abs(this.direction.x) <= 0.1 || Math.abs(this.direction.x) >= 0.9) {const heading = randomNumberBetween(0, 2 * Math.PI);this.direction = {x: Math.cos(heading),y: Math.sin(heading)};}this.velocity = INITIAL_VELOCITY;}update(delta) {this.x += this.direction.x * this.velocity * delta;this.y += this.direction.y * this.velocity * delta;const rect = this.rect();if (rect.bottom >= window.innerHeight || rect.top <= 0) {this.direction.y *= -1;}if (rect.right >= window.innerWidth || rect.left <= 0) {this.direction.x *= -1;}}}function randomNumberBetween(min, max) {return Math.random() * (max - min) + min;}var ballarr = [];for (var i = 0; i < NUMBER_OF_BALLS; i++) {const ballEl = document.createElement('div');ballEl.classList.add('ball');document.body.appendChild(ballEl);const ball = new Ball(ballEl);ballarr.push(ball);}let lastTime;function updateTime(time) {if (lastTime != null) {const delta = time - lastTime;ballarr.forEach(ball => ball.update(delta));}lastTime = time;window.requestAnimationFrame(updateTime);}updateTime(lastTime);

解决方案

Intersection Observer并不适合实时碰撞检测——它是异步触发的,响应延迟会导致碰撞判断不及时,甚至漏判。正确的做法是用几何碰撞检测:计算两个球心之间的距离,当距离小于两个球的半径之和时判定碰撞,再用弹性碰撞公式调整速度方向。

修改后的script.js

const INITIAL_VELOCITY = 0.085
const VELOCITY_CHANGE_RATE = 0
const NUMBER_OF_BALLS = 2
// 球的半径:2vh的一半,转成像素(1vh = window.innerHeight/100)
const BALL_RADIUS = window.innerHeight / 100

class Ball {
  constructor(ballElem) {
    this.ballElem = ballElem;
    this.radius = BALL_RADIUS;
    this.reset();
  }

  get x() {
    return parseFloat(getComputedStyle(this.ballElem).getPropertyValue("--x"));
  }

  set x(value) {
    this.ballElem.style.setProperty("--x", value);
  }

  get y() {
    return parseFloat(getComputedStyle(this.ballElem).getPropertyValue("--y"));
  }

  set y(value) {
    this.ballElem.style.setProperty("--y", value);
  }

  // 获取球心的像素坐标
  get pixelX() {
    return (this.x / 100) * window.innerWidth;
  }

  get pixelY() {
    return (this.y / 100) * window.innerHeight;
  }

  rect() {
    return this.ballElem.getBoundingClientRect();
  }

  reset() {
    this.x = 50;
    this.y = 50;
    this.direction = { x: 0 };
    while (Math.abs(this.direction.x) <= 0.1 || Math.abs(this.direction.x) >= 0.9) {
      const heading = randomNumberBetween(0, 2 * Math.PI);
      this.direction = {
        x: Math.cos(heading),
        y: Math.sin(heading)
      };
    }
    this.velocity = INITIAL_VELOCITY;
  }

  update(delta) {
    this.x += this.direction.x * this.velocity * delta;
    this.y += this.direction.y * this.velocity * delta;
    const rect = this.rect();
    if (rect.bottom >= window.innerHeight || rect.top <= 0) {
      this.direction.y *= -1;
    }
    if (rect.right >= window.innerWidth || rect.left <= 0) {
      this.direction.x *= -1;
    }
  }
}

function randomNumberBetween(min, max) {
  return Math.random() * (max - min) + min;
}

// 碰撞检测与处理函数
function checkCollisions(balls) {
  for (let i = 0; i < balls.length; i++) {
    for (let j = i + 1; j < balls.length; j++) {
      const ball1 = balls[i];
      const ball2 = balls[j];

      // 计算两个球心的距离(平方,避免开根号提升性能)
      const dx = ball1.pixelX - ball2.pixelX;
      const dy = ball1.pixelY - ball2.pixelY;
      const distanceSquared = dx * dx + dy * dy;
      const minDistance = ball1.radius + ball2.radius;

      // 判断是否碰撞
      if (distanceSquared <= minDistance * minDistance) {
        // 弹性碰撞公式:交换速度分量(假设质量相同)
        const tempX = ball1.direction.x;
        const tempY = ball1.direction.y;
        ball1.direction.x = ball2.direction.x;
        ball1.direction.y = ball2.direction.y;
        ball2.direction.x = tempX;
        ball2.direction.y = tempY;

        // 避免小球重叠:稍微分离两个球
        const overlap = minDistance - Math.sqrt(distanceSquared);
        const separationX = (dx / minDistance) * overlap * 0.5;
        const separationY = (dy / minDistance) * overlap * 0.5;
        ball1.x += (separationX / window.innerWidth) * 100;
        ball1.y += (separationY / window.innerHeight) * 100;
        ball2.x -= (separationX / window.innerWidth) * 100;
        ball2.y -= (separationY / window.innerHeight) * 100;
      }
    }
  }
}

var ballarr = [];
for (var i = 0; i < NUMBER_OF_BALLS; i++) {
  const ballEl = document.createElement('div');
  ballEl.classList.add('ball');
  document.body.appendChild(ballEl);
  const ball = new Ball(ballEl);
  ballarr.push(ball);
}

let lastTime;
function updateTime(time) {
  if (lastTime != null) {
    const delta = time - lastTime;
    ballarr.forEach(ball => ball.update(delta));
    // 每次更新位置后检测碰撞
    checkCollisions(ballarr);
  }
  lastTime = time;
  window.requestAnimationFrame(updateTime);
}

updateTime(lastTime);

关键说明

  • 半径计算:把vh单位转成像素,确保距离计算准确
  • 碰撞判断:用距离平方代替距离,减少开根号运算,提升性能
  • 弹性碰撞处理:假设两个球质量相同,直接交换速度方向;如果需要不同质量,可调整弹性碰撞公式
  • 重叠修正:碰撞后稍微分离小球,避免因帧间隔导致的持续重叠问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 19:25:41