開始
最終效果: codepen
一開始都是一個單一的用例,定位在畫布中央,再擴充套件開來
先獲取canvas元素及可視寬高
let canvas = document.querySelector('#canvas')
let context = canvas.getContext('2d')
let cw = canvas.width = window.innerWidth
let ch = canvas.height = window.innerHeight
複製程式碼
開始繪製
第一部分-定位的用的閃爍的圓
// 建立一個閃爍圓的類
class Kirakira {
constructor(){
// 目標點,這裡先指定為螢幕中央
this.targetLocation = {x: cw/2, y: ch/2}
this.radius = 1
}
draw() {
// 繪製一個圓
context.beginPath()
context.arc(this.targetLocation.x, this.targetLocation.y, 5, 0, Math.PI * 2)
context.lineWidth = 2
context.strokeStyle = '#FFFFFF';
context.stroke()
}
update(){
if(this.radius < 5){
this.radius += 0.3
}else{
this.radius = 1
}
}
init() {
this.draw()
}
}
class Animate {
run() {
window.requestAnimationFrame(this.run.bind(this))
if(o){
o.init()
}
}
}
let o = new Kirakira()
let a = new Animate()
a.run()
複製程式碼
由此,可以看到一個由小到大擴張的圓。由於沒有擦除上一幀,每一幀的繪製結果都顯示出來,所以呈現出來的是一個實心的圓。我想繪製的是一個閃爍的圓,那麼可以把上一幀給擦除。
context.clearRect(0, 0, cw, ch)
複製程式碼
第二部分-畫射線
首先,先畫一由底部到畫布中央的延伸線。既然是運動的延伸線條,那起碼會有一個起點座標和一個終點座標
class Biubiubiu {
constructor(startX, startY, targetX, targetY){
this.startLocation = {x: startX, y: startY}
// 運動當前的座標,初始預設為起點座標
this.nowLoaction = {x: startX, y: startY}
this.targetLocation = {x: targetX, y: targetY}
}
draw(){
context.beginPath()
context.moveTo(this.startLocation.x, this.startLocation.y)
context.lineWidth = 3
context.lineCap = 'round'
// 線條需要定位到當前的運動座標,才能使線條運動起來
context.lineTo(this.nowLoaction.x, this.nowLoaction.y)
context.strokeStyle = '#FFFFFF'
context.stroke()
}
update(){}
init(){
this.draw()
this.update()
}
}
class Animate {
run() {
window.requestAnimationFrame(this.run.bind(this))
context.clearRect(0, 0, cw, ch)
if(b){
b.init()
}
}
}
// 這裡的打算是定位起點在畫布的底部隨機位置, 終點在畫布中央。
let b = new Biubiubiu(Math.random()*(cw/2), ch, cw/2, ch/2)
let a = new Animate()
a.run()
複製程式碼
說說三角函式
已知座標起點和座標終點, 那麼問題來了,要怎麼知道從起點到終點的每一幀的座標呢
如圖。大概需要做判斷的目標有- 線條運動的距離是否超出起點到終點的距離,如超出則需要停止運動
- 每一幀運動到達的座標
計算距離
對於座標間距離的計算,很明顯的可以使用勾股定理完成。
設起點座標為x0, y0
, 終點座標為x1, y1
,即可得 distance = √(x1-x0)² + (y1-y0)²
,用程式碼表示則是Math.sqrt(Math.pow((x1-x0), 2) + Math.pow((y1-y0), 2))
計算座標
上一幀的總距離(d) + 當前幀下走過的路程(v) = 當前幀的距離(D)
假設一個速度 speed = 2
, 起點和終點形成的角度為(θ), 路程(v)的座標分別為vx, vy
那麼 vx = cos(θ) * speed, vy = sin(θ) * speed
由於起點(x0, y0)
和終點(x1, y1)
已知,由圖可知,通過三角函式中的tan
可以取到兩點成線和水平線之間的夾角角度,程式碼表示為Math.atan2(y1 - y0, x1 - x0)
回到繪製延伸線的程式碼。 給Biubiubiu類新增上角度和距離的計算,
class Biubiubiu {
constructor(startX, startY, targetX, targetY){
...
// 到目標點的距離
this.targetDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.targetLocation.x, this.targetLocation.y);
// 速度
this.speed = 2
// 角度
this.angle = Math.atan2(this.targetLocation.y - this.startLocation.y, this.targetLocation.x - this.startLocation.x)
// 是否到達目標點
this.arrived = false
}
draw(){ ... }
update(){
// 計算當前幀的路程v
let vx = Math.cos(this.angle) * this.speed
let vy = Math.sin(this.angle) * this.speed
// 計算當前運動距離
let nowDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.nowLoaction.x+vx, this.nowLoaction.y+vy)
// 如果當前運動的距離超出目標點距離,則不需要繼續運動
if(nowDistance >= this.targetDistance){
this.arrived = true
}else{
this.nowLoaction.x += vx
this.nowLoaction.y += vy
this.arrived = false
}
}
getDistance(x0, y0, x1, y1) {
// 計算兩座標點之間的距離
let locX = x1 - x0
let locY = y1 - y0
// 勾股定理
return Math.sqrt(Math.pow(locX, 2) + Math.pow(locY, 2))
}
init(){
this.draw()
this.update()
}
}
class Animate { ... }
// 這裡的打算是定位起點在畫布的底部隨機位置, 終點在畫布中央。
let b = new Biubiubiu(Math.random()*(cw/2), ch, cw/2, ch/2)
let a = new Animate()
a.run()
複製程式碼
由於speed
是固定的,這裡呈現的是勻速運動。可以加個加速度``,使其改變為變速運動。
我的目標效果並不是一整條線條,而是當前執行的一截線段軌跡。這裡有個思路,把一定量的座標點存為一個陣列,在繪製的時候可以由陣列內的座標指向當前運動的座標,並在隨著幀數變化不停對陣列進行資料更替,由此可以繪製出一小截的運動線段
實現程式碼:
class Biubiubiu {
constructor(startX, startY, targetX, targetY) {
...
// 線段集合, 每次存10個,取10個幀的距離
this.collection = new Array(10)
}
draw() {
context.beginPath()
// 這裡改為由集合的第一位開始定位
try{
context.moveTo(this.collection[0][0], this.collection[0][1])
}catch(e){
context.moveTo(this.nowLoaction.x, this.nowLoaction.y)
}
...
}
update(){
// 對集合進行資料更替,彈出陣列第一個資料,並把當前運動的座標push到集合。只要取陣列的頭尾兩個座標相連,則是10個幀的長度
this.collection.shift()
this.collection.push([this.nowLoaction.x, this.nowLoaction.y])
// 給speed新增加速度
this.speed *= this.acceleration
...
}
}
複製程式碼
第三部分-畫一個爆炸的效果
由上面的延伸線的程式碼,擴充套件開來,如果不取10幀,取個兩三幀的小線段,然後改變延伸方向,多條射線組合,就可以形成了爆炸效果。火花是會受重力,摩擦力等影響到,擴散趨勢是偏向下的,所以需要加上一些重力,摩擦力系數
class Boom {
// 爆炸物是沒有確定的結束點座標, 這個可以通過設定一定的閥值來限定
constructor(startX, startY){
this.startLocation = {x: startX, y: startY}
this.nowLocation = {x: startX, y: startY}
// 速度
this.speed = Math.random()*10+2
// 加速度
this.acceleration = 0.95
// 沒有確定的結束點,所以沒有固定的角度,可以隨機角度擴散
this.angle = Math.random()*Math.PI*2
// 這裡設定閥值為100
this.targetCount = 100
// 當前計算為1,用於判斷是否會超出閥值
this.nowNum = 1
// 透明度
this.alpha = 1
// 重力系數
this.gravity = 0.98
this.decay = 0.015
// 線段集合, 每次存10個,取10個幀的距離
this.collection = new Array(CONFIG.boomCollectionCont)
// 是否到達目標點
this.arrived = false
}
draw(){
context.beginPath()
try{
context.moveTo(this.collection[0][0], this.collection[0][1])
}catch(e){
context.moveTo(this.nowLocation.x, this.nowLocation.y)
}
context.lineWidth = 3
context.lineCap = 'round'
context.lineTo(this.nowLocation.x, this.nowLocation.y)
// 設定由透明度減小產生的漸隱效果,看起來沒這麼突兀
context.strokeStyle = `rgba(255, 255, 255, ${this.alpha})`
context.stroke()
}
update(){
this.collection.shift()
this.collection.push([this.nowLocation.x, this.nowLocation.y])
this.speed *= this.acceleration
let vx = Math.cos(this.angle) * this.speed
// 加上重力系數,運動軌跡會趨向下
let vy = Math.sin(this.angle) * this.speed + this.gravity
// 當前計算大於閥值的時候的時候,開始進行漸隱處理
if(this.nowNum >= this.targetCount){
this.alpha -= this.decay
}else{
this.nowLocation.x += vx
this.nowLocation.y += vy
this.nowNum++
}
// 透明度為0的話,可以進行移除處理,釋放空間
if(this.alpha <= 0){
this.arrived = true
}
}
init(){
this.draw()
this.update()
}
}
class Animate {
constructor(){
// 定義一個陣列做為爆炸點的集合
this.booms = []
// 避免每幀都進行繪製導致的過量繪製,設定閥值,到達閥值的時候再進行繪製
this.timerTarget = 80
this.timerNum = 0
}
pushBoom(){
// 例項化爆炸效果,隨機條數的射線擴散
for(let bi = Math.random()*10+20; bi>0; bi--){
this.booms.push(new Boom(cw/2, ch/2))
}
}
run() {
window.requestAnimationFrame(this.run.bind(this))
context.clearRect(0, 0, cw, ch)
let bnum = this.booms.length
while(bnum--){
// 觸發動畫
this.booms[bnum].init()
if(this.booms[bnum].arrived){
// 到達目標透明度後,把炸點給移除,釋放空間
this.booms.splice(bnum, 1)
}
}
if(this.timerNum >= this.timerTarget){
// 到達閥值,進行爆炸效果的例項化
this.pushBoom()
this.timerNum = 0
}else{
this.timerNum ++
}
}
}
let a = new Animate()
a.run()
複製程式碼
第四部分-合併程式碼,並且由一到多
合併程式碼的話,主要是個順序問題。
地點上,閃爍圓的座標點即是射線的目標終點,同時也是爆炸效果的座標起點。 時間上,在和射線到達終點後,再觸發爆炸方法即可。
let canvas = document.querySelector('#canvas')
let context = canvas.getContext('2d')
let cw = canvas.width = window.innerWidth
let ch = canvas.height = window.innerHeight
function randomColor(){
// 返回一個0-255的數值,三個隨機組合為一起可定位一種rgb顏色
let num = 3
let color = []
while(num--){
color.push(Math.floor(Math.random()*254+1))
}
return color.join(', ')
}
class Kirakira {
constructor(targetX, targetY){
// 指定產生的座標點
this.targetLocation = {x: targetX, y: targetY}
this.radius = 1
}
draw() {
// 繪製一個圓
context.beginPath()
context.arc(this.targetLocation.x, this.targetLocation.y, this.radius, 0, Math.PI * 2)
context.lineWidth = 2
context.strokeStyle = `rgba(${randomColor()}, 1)`;
context.stroke()
}
update(){
// 讓圓進行擴張,實現閃爍效果
if(this.radius < 5){
this.radius += 0.3
}else{
this.radius = 1
}
}
init() {
this.draw()
this.update()
}
}
class Biubiubiu {
constructor(startX, startY, targetX, targetY) {
this.startLocation = {x: startX, y: startY}
this.targetLocation = {x: targetX, y: targetY}
// 運動當前的座標,初始預設為起點座標
this.nowLoaction = {x: startX, y: startY}
// 到目標點的距離
this.targetDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.targetLocation.x, this.targetLocation.y);
// 速度
this.speed = 2
// 加速度
this.acceleration = 1.02
// 角度
this.angle = Math.atan2(this.targetLocation.y - this.startLocation.y, this.targetLocation.x - this.startLocation.x)
// 線段集合
this.collection = []
// 線段集合, 每次存10個,取10個幀的距離
this.collection = new Array(CONFIG.biuCollectionCont)
// 是否到達目標點
this.arrived = false
}
draw() {
context.beginPath()
try{
context.moveTo(this.collection[0][0], this.collection[0][1])
}catch(e){
context.moveTo(this.nowLoaction.x, this.nowLoaction.y)
}
context.lineWidth = 3
context.lineCap = 'round'
context.lineTo(this.nowLoaction.x, this.nowLoaction.y)
context.strokeStyle = `rgba(${randomColor()}, 1)`;
context.stroke()
}
update() {
this.collection.shift()
this.collection.push([this.nowLoaction.x, this.nowLoaction.y])
this.speed *= this.acceleration
let vx = Math.cos(this.angle) * this.speed
let vy = Math.sin(this.angle) * this.speed
let nowDistance = this.getDistance(this.startLocation.x, this.startLocation.y, this.nowLoaction.x+vx, this.nowLoaction.y+vy)
if(nowDistance >= this.targetDistance){
this.arrived = true
}else{
this.nowLoaction.x += vx
this.nowLoaction.y += vy
this.arrived = false
}
}
getDistance(x0, y0, x1, y1) {
// 計算兩座標點之間的距離
let locX = x1 - x0
let locY = y1 - y0
// 勾股定理
return Math.sqrt(Math.pow(locX, 2) + Math.pow(locY, 2))
}
init() {
this.draw()
this.update()
}
}
class Boom {
// 爆炸物是沒有確定的結束點座標, 這個可以通過設定一定的閥值來限定
constructor(startX, startY){
this.startLocation = {x: startX, y: startY}
this.nowLocation = {x: startX, y: startY}
// 速度
this.speed = Math.random()*10+2
// 加速度
this.acceleration = 0.95
// 沒有確定的結束點,所以沒有固定的角度,可以隨機角度擴散
this.angle = Math.random()*Math.PI*2
// 這裡設定閥值為100
this.targetCount = 100
// 當前計算為1,用於判斷是否會超出閥值
this.nowNum = 1
// 透明度
this.alpha = 1
// 透明度減少梯度
this.grads = 0.015
// 重力系數
this.gravity = 0.98
// 線段集合, 每次存10個,取10個幀的距離
this.collection = new Array(10)
// 是否到達目標點
this.arrived = false
}
draw(){
context.beginPath()
try{
context.moveTo(this.collection[0][0], this.collection[0][1])
}catch(e){
context.moveTo(this.nowLoaction.x, this.nowLoaction.y)
}
context.lineWidth = 3
context.lineCap = 'round'
context.lineTo(this.nowLocation.x, this.nowLocation.y)
// 設定由透明度減小產生的漸隱效果,看起來沒這麼突兀
context.strokeStyle = `rgba(${randomColor()}, ${this.alpha})`
context.stroke()
}
update(){
this.collection.shift()
this.collection.push([this.nowLocation.x, this.nowLocation.y])
this.speed *= this.acceleration
let vx = Math.cos(this.angle) * this.speed
// 加上重力系數,運動軌跡會趨向下
let vy = Math.sin(this.angle) * this.speed + this.gravity
// 當前計算大於閥值的時候的時候,開始進行漸隱處理
if(this.nowNum >= this.targetCount){
this.alpha -= this.grads
}else{
this.nowLocation.x += vx
this.nowLocation.y += vy
this.nowNum++
}
// 透明度為0的話,可以進行移除處理,釋放空間
if(this.alpha <= 0){
this.arrived = true
}
}
init(){
this.draw()
this.update()
}
}
class Animate {
constructor(){
// 用於記錄當前例項化的座標點
this.startX = null
this.startY = null
this.targetX = null
this.targetY = null
// 定義一個陣列做為閃爍球的集合
this.kiras = []
// 定義一個陣列做為射線類的集合
this.bius = []
// 定義一個陣列做為爆炸類的集合
this.booms = []
// 避免每幀都進行繪製導致的過量繪製,設定閥值,到達閥值的時候再進行繪製
this.timerTarget = 80
this.timerNum = 0
}
pushBoom(x, y){
// 例項化爆炸效果,隨機條數的射線擴散
for(let bi = Math.random()*10+20; bi>0; bi--){
this.booms.push(new Boom(x, y))
}
}
run() {
window.requestAnimationFrame(this.run.bind(this))
context.clearRect(0, 0, cw, ch)
let biuNum = this.bius.length
while(biuNum-- ){
this.bius[biuNum].init()
this.kiras[biuNum].init()
if(this.bius[biuNum].arrived){
// 到達目標後,可以開始繪製爆炸效果, 當前線條的目標點則是爆炸例項的起始點
this.pushBoom(this.bius[biuNum].nowLoaction.x, this.bius[biuNum].nowLoaction.y)
// 到達目標後,把當前類給移除,釋放空間
this.bius.splice(biuNum, 1)
this.kiras.splice(biuNum, 1)
}
}
let bnum = this.booms.length
while(bnum--){
// 觸發動畫
this.booms[bnum].init()
if(this.booms[bnum].arrived){
// 到達目標透明度後,把炸點給移除,釋放空間
this.booms.splice(bnum, 1)
}
}
if(this.timerNum >= this.timerTarget){
// 到達閥值後開始繪製例項化射線
this.startX = Math.random()*(cw/2)
this.startY = ch
this.targetX = Math.random()*cw
this.targetY = Math.random()*(ch/2)
let exBiu = new Biubiubiu(this.startX, this.startY, this.targetX, this.targetY)
let exKira = new Kirakira(this.targetX, this.targetY)
this.bius.push(exBiu)
this.kiras.push(exKira)
// 到達閥值後把當前計數重置一下
this.timerNum = 0
}else{
this.timerNum ++
}
}
}
let a = new Animate()
a.run()
複製程式碼
製作過程中衍生出來的比較好玩的效果