uniapp中用canvas實現小球碰撞的小動畫
- 2022 年 8 月 5 日
- 筆記
- javascript, uniapp
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>