如何实现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
相关产品推荐
相关产品推荐

