开始
最终效果: 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()
制作过程中衍生出来的比较好玩的效果