基于Canvas的碰撞小球
2025年7月16日大约 2 分钟
基于Canvas的碰撞小球
<!-- 使用原生JS实现canvas上绘制小球,并且做一点点物理模拟(碰壁反弹)
需求等级:
创建并放置canvas(或者在html里放置然后getElementById),在内部绘制小球(统一尺寸)
canvas尺寸匹配屏幕,并适配DPI缩放
使得小球朝向随机方向匀速运动,撞到墙壁后反弹(完全弹性碰撞,并且完全没有能量损失)
反弹后永久改变小球的颜色,类似DVD标志那样
加入重力影响
初始状态不放置小球,点击后在点击位置放置小球(统一尺寸和质量)
(Advanced) 小球之间碰撞检测,不考虑摩擦和旋转,只有完全弹性碰撞的反弹,没有能量损失
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canvas</title>
<style>
html,
body {
width: 100vw;
height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<canvas
id="canvas"
style="width: 100vw; height: 100vh; background-color: black"
/>
</body>
<script>
const throttle = function (fn, delay) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= delay) {
last = now
fn.apply(this, args)
}
}
}
const GRAVITY = 0.1 //重力因素
const balls = []
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const resizeCanvas = throttle(function () {
const dpr = window.devicePixelRatio || 1
canvas.width = window.innerWidth * dpr
canvas.height = window.innerHeight * dpr
ctx.scale(dpr, dpr)
}, 200)
window.addEventListener('resize', resizeCanvas)
resizeCanvas()
const randomColor = function () {
return `rgb(${Math.floor(Math.random() * 256)},${Math.floor(
Math.random() * 256
)},${Math.floor(Math.random() * 256)})`
}
class Ball {
constructor(x, y) {
this.x = x //初始化X坐标
this.y = y //初始化Y坐标
this.vx = (Math.random() - 0.5) * 10 //X轴移动
this.vy = (Math.random() - 0.5) * 10 //Y轴移动
this.color = randomColor() // 颜色
this.radius = Math.floor(Math.random() * 20) //半径
}
update() {
this.vy += GRAVITY
this.x += this.vx
this.y += this.vy
// 碰壁处理
if (
this.x - this.radius <= 0 ||
this.x + this.radius >= window.innerWidth
) {
this.vx *= -1
this.color = randomColor()
this.x = Math.max(
this.radius,
Math.min(this.x, window.innerWidth - this.radius)
)
}
if (
this.y - this.radius <= 0 ||
this.y + this.radius >= window.innerHeight
) {
this.vy *= -1
this.color = randomColor()
this.y = Math.max(
this.radius,
Math.min(this.y, window.innerHeight - this.radius)
)
}
}
draw(ctx) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
ctx.fillStyle = this.color
ctx.fill()
}
}
function detectCollisions() {
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const a = balls[i]
const b = balls[j]
const dx = b.x - a.x
const dy = b.y - a.y
const dist = Math.hypot(dx, dy)
if (dist < a.radius + b.radius) {
// 简单的弹性碰撞处理
const angle = Math.atan2(dy, dx)
const totalMass = 1 + 1
const v1 = rotate({ x: a.vx, y: a.vy }, angle)
const v2 = rotate({ x: b.vx, y: b.vy }, angle)
const u1 = { x: v2.x, y: v1.y }
const u2 = { x: v1.x, y: v2.y }
const vFinal1 = rotate(u1, -angle)
const vFinal2 = rotate(u2, -angle)
a.vx = vFinal1.x
a.vy = vFinal1.y
b.vx = vFinal2.x
b.vy = vFinal2.y
// 改变颜色
a.color = randomColor()
b.color = randomColor()
// 让它们分开避免粘在一起
const overlap = 0.5 * (a.radius + b.radius - dist + 1)
const offsetX = overlap * Math.cos(angle)
const offsetY = overlap * Math.sin(angle)
a.x -= offsetX
a.y -= offsetY
b.x += offsetX
b.y += offsetY
}
}
}
}
function rotate(velocity, angle) {
return {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle),
}
}
function animate() {
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight)
detectCollisions()
for (let ball of balls) {
ball.update()
ball.draw(ctx)
}
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
balls.push(...Array.from({ length: 10 }, () => new Ball(x, y)))
})
</script>
</html>