uniapp中用canvas实现小球碰撞的小动画
uniapp 我就不想喷了,踩了很多坑,把代码贡献出来让大家少踩些坑。
实现的功能:
- 生成n个球在canvas中运动,相互碰撞后会反弹,反弹后的速度计算我研究过了,可以参考代码直接用
- 防止球出边框
- 防止小球之间碰撞过度,或者说“穿模”。采用的方法是碰撞后让两个小球都多走一帧。其实这样并不能完全防止“穿模”,但可以防止小球粘在一起不停的穿模
- uniapp中的requestAnimationFrame的使用,包括开始与停止动画
- 利用四叉树优化了碰撞检测,网上有些示例是直接让区域内所有的小球之间进行一次碰撞检测
代码是vue3写的。uniapp官方说做动画推荐用什么renderjs,不知道直接这样写还会有什么坑,目前在h5中测试没问题。
四叉树类的代码见我上一篇文章
ball类的代码:
export class Ball { // 提供圆心坐标和半径 constructor(x, y, r, speedX, speedY, color, index) { this.centerX = x this.centerY = y this.r = r this.x = x - r this.y = y - r this.width = 2 * r this.height = 2 * r this.color = color this.speedX = speedX this.speedY = speedY this.index = index // 索引 } // 将球填充到canvas context fillTo(ctx) { ctx.beginPath() ctx.arc(this.centerX, this.centerY, this.r, 0, 2 * Math.PI) ctx.setFillStyle(this.color) ctx.closePath() ctx.fill() } // 判断是否与另一个球相交 intersectAt(ball2) { let dx = this.centerX - ball2.centerX let dy = this.centerY - ball2.centerY let distance = Math.sqrt(dx * dx + dy * dy) return this.r + ball2.r >= distance } // 移动 width height 是canvas的宽高 move(width, height) { this.centerX += this.speedX if (this.centerX - this.r <= 0) { this.centerX = this.r this.speedX = -this.speedX } if (this.r + this.centerX >= width) { this.centerX = width - this.r this.speedX = -this.speedX } this.centerY += this.speedY if (this.centerY - this.r <= 0) { this.centerY = this.r this.speedY = -this.speedY } if (this.centerY + this.r >= height) { this.centerY = height - this.r this.speedY = -this.speedY } this.x = this.centerX - this.r this.y = this.centerY - this.r } }
page的代码:
<template>
<view>
<view>
<button @click="animateStart">开始</button>
<button @click="animateStop">停止</button>
</view>
<canvas type="2d" :style="{'width':canvasSize+'px','height':canvasSize+'px','border':'1px solid black'}"
canvas-id="game" id="game"></canvas>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
onReady
} from '@dcloudio/uni-app'
import {
Ball
} from '@/utils/Ball.js'
import {
QuadTree
} from '@/utils/QuadTree.js'
console.log(uni.getSystemInfoSync().screenWidth)
console.log(uni.getSystemInfoSync().windowWidth)
const canvasSize = ref((uni.getSystemInfoSync().windowWidth < uni.getSystemInfoSync().windowHeight ? uni
.getSystemInfoSync()
.windowWidth : uni.getSystemInfoSync().windowHeight) - 100)
const size = canvasSize.value / 32
const ballList = []
for (let i = 0; i < 30; i++) {
let x = Math.random() * canvasSize.value,
y = Math.random() * canvasSize.value,
r = Math.random() * size / 1.5 + 10,
color = randomHexColor()
const ball = new Ball(x, y, r, Math.random() * 7 + 3, Math.random() * 5 + 5, color, i)
ballList.push(ball)
}
let ctx = null
onReady(() => {
ctx = uni.createCanvasContext('game')
console.log(ballList)
})
let rAF
const animateStart = () => {
render(ctx)
rAF = requestAnimationFrame(() => {
animateStart()
})
}
const animateStop = () =>{
cancelAnimationFrame(rAF)
}
const render = (ctx) => {
const quadTree = new QuadTree(0, 0, canvasSize.value, canvasSize.value, 0)
for (let i = 0; i < ballList.length; i++) {
quadTree.insert(ballList[i])
}
for (let q of quadTree.possibles()) {
// console.log('*', Date.now())
ctx.setFillStyle("rgba(255, 170, 80, 0.2)")
ctx.setStrokeStyle('yellow')
ctx.fillRect(q.x, q.y, q.width, q.height)
ctx.strokeRect(q.x, q.y, q.width, q.height)
for (let i = 0; i < q.sprites.length - 1; i++) {
for (let j = i + 1; j < q.sprites.length; j++) {
let a = q.sprites[i]
let b = q.sprites[j]
if (a.intersectAt(b)) {
// 假设半径就是重量
let v1, v2, m1 = a.r,
m2 = b.r
// 先算水平速度
v1 = a.speedX
v2 = b.speedX
a.speedX = (v1 * (m1 - m2) + 2 * m2 * v2) / (m1 + m2)
b.speedX = (v2 * (m2 - m1) + 2 * m1 * v1) / (m1 + m2)
// 再算垂直速度
v1 = a.speedY
v2 = b.speedY
a.speedY = (v1 * (m1 - m2) + 2 * m2 * v2) / (m1 + m2)
b.speedY = (v2 * (m2 - m1) + 2 * m1 * v1) / (m1 + m2)
//发生碰撞的小球,防止穿模,多走一步
a.move(canvasSize.value, canvasSize.value)
b.move(canvasSize.value, canvasSize.value)
}
}
}
}
ballList.forEach((ball) => {
ball.move(canvasSize.value, canvasSize.value)
ball.fillTo(ctx)
})
ctx.draw()
}
function randomHexColor() {
//生成ffffff以内16进制数
var hex = Math.floor(Math.random() * 16777216).toString(16);
//while循环判断hex位数,少于6位前面加0凑够6位
while (hex.length < 6) {
hex = '0' + hex;
}
//返回‘#'开头16进制颜色
return '#' + hex;
}
</script>
<style>
</style>

浙公网安备 33010602011771号