原理很简单,1.从屏幕底部中心射出烟花,2.烟花移动到随机目标点后 产生向四周爆开的粒子效果
github源码地址
首先设置画布全屏
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const canvasWidth = window.innerWidth
const canvasHeight = window.innerHeight
// canvas全屏
canvas.width = canvasWidth
canvas.height = canvasHeight
创建两个数组分别存放生成的烟花和粒子
let fireworks = [] // 烟花集合
let particles = [] // 粒子集合
需要用到的两个工具函数
// 获取范围内的随机数
function randomRange(min, max) {
return Math.random() * (max - min) + min
}
// 计算两点之间/起始点到目标点的距离
//(公式:求两个向量之间距离)
function calcPointsDistance(sx, sy, tx, ty) {
return Math.sqrt(Math.pow((tx - sx), 2) + Math.pow((ty - sy), 2))
}
构建粒子类 Particle
class Particle {
// 初始时的x,y坐标
constructor(x, y, hue) {
this.x = x
this.y = y
// 粒子坐标集合
this.coords = [[x, y], [x, y], [x, y]]
// 随机弧度
this.angle = randomRange(0, Math.PI * 2)
// 随机基本速度
this.speed = randomRange(1, 10)
// 摩擦系数、重力(减缓粒子速度、模拟抛物线下坠)
this.friction = 0.95 // 百分比 不同材质的物体摩擦系数不同(有现成值)
this.gravity = 1 // 作用于y轴加速度 模拟往下坠
// 随机色调(基础色调-20和+20之间)
this.hue = randomRange(hue - 20, hue + 20)
// 随机亮度
this.brightness = randomRange(50, 80)
// 初始透明度
this.alpha = 1
// 随机的透明度衰变系数(透明度减淡)
this.alphaDecay = randomRange(0.015, 0.03)
}
// 更新某个(索引)粒子属性
update(index) {
// 删掉最后一项 在最前面塞入一项
this.coords.pop()
this.coords.unshift([this.x, this.y])
this.speed *= this.friction // 先减速
// 由弧度计算出当前x,y坐标值
// 难点:看原理/正弦余弦理解.png
this.x += Math.cos(this.angle) * this.speed
this.y += Math.sin(this.angle) * this.speed + this.gravity
// 透明度衰减
this.alpha -= this.alphaDecay
// 当透明度小于最小衰减值 就把这个例子对象删除
if (this.alpha < this.alphaDecay) {
particles.splice(index, 1)
}
}
// 绘制粒子(line的方式)
draw() {
ctx.beginPath()
// 从集合中最后一个项开始
const [ startX, startY ] = this.coords[this.coords.length - 1]
ctx.moveTo(startX, startY)
ctx.lineTo(this.x, this.y)
// hsla的颜色模式
ctx.strokeStyle = `hsla(${this.hue}, 100%, ${this.brightness}%, ${this.alpha}`
ctx.lineWidth = 3
ctx.lineCap = 'round'
ctx.stroke()
}
}
构建烟花类 Firework
在烟花移动到目标点坐标时 创建一些粒子对象
class Firework {
// 起始点坐标sx,sy 目标点坐标tx,ty
constructor(sx, sy, tx, ty) {
// 当前坐标
this.x = sx
this.y = sy
// 起始点坐标
this.sx = sx
this.sy = sy
// 目标点坐标
this.tx = tx
this.ty = ty
// 起始点到目标点的距离
this.distanceToTarget = calcPointsDistance(sx, sy, tx, ty)
// 移动后的距离
this.distanceTraveled = 0
// 烟花的轨迹坐标
this.coords = [[this.x, this.y]]
// Math.atan2是计算某点到原点0,0与x正轴的弧度值,传入y,x
// 难点:看原理/向量坐标的关系.png 目标点向量为两个点的向量相加 已经有了起始点 那就求出另一个点
// 求出弧度后为了update中分解为vx,vy服务
this.angle = Math.atan2(ty - sy, tx - sx)
this.speed = 2 // 基础移动速度为2
this.acceleration = 1.05 // 加速度系数
this.hue = randomRange(0, 360)
this.brightness = randomRange(50, 70) // 随机亮度
}
// 更新某个烟花属性(烟花移动是加速度的)
update(index) {
// 删掉最后一项 在最前面塞入一项
this.coords.pop()
this.coords.unshift([this.x, this.y])
this.speed *= this.acceleration // 进行加速度
const vx = Math.cos(this.angle) * this.speed
const vy = Math.sin(this.angle) * this.speed
// 计算出移动后的距离
this.distanceTraveled = calcPointsDistance(this.sx, this.sy, this.x + vx, this.y + vy)
// 如果移动到目标点 就创建50个爆炸粒子对象,并删除这个射出的烟火对象
// 否则继续更新this.x,this.y
if (this.distanceTraveled >= this.distanceToTarget) {
for (let i = 0; i < 75; i++) {
particles.push(new Particle(this.tx, this.ty, this.hue))
}
fireworks.splice(index, 1)
} else {
this.x += vx
this.y += vy
}
}
// 绘制烟火
draw() {
ctx.beginPath()
const [ startX, startY ] = this.coords[this.coords.length - 1]
ctx.moveTo(startX, startY)
ctx.lineTo(this.x, this.y)
ctx.strokeStyle = `hsla(${this.hue}, 100%, ${this.brightness}%, 60%` // 仅亮度会变化
ctx.lineWidth = 1
ctx.stroke()
}
}
requestAnimationFrame
创建动画的好处是 可以随着屏幕刷新率调用渲染函数 不会出现 setInterval
方式的丢帧,动画会看起来更流畅
创建动画渲染函数render
function render() {
window.requestAnimationFrame(render)
// 制造拖尾效果,不使用clearRect 每次覆盖一层带透明度的底色
ctx.globalCompositeOperation = 'destination-out' // 现有内容保持在新图形不重叠的地方
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
ctx.globalCompositeOperation = 'lighter'
// 循环绘制和更新
for (let i = 0; i < fireworks.length; i++) {
fireworks[i].draw()
fireworks[i].update(i)
}
for (let i = 0; i < particles.length; i++) {
particles[i].draw()
particles[i].update(i)
}
}
两种方式可以生成烟花:鼠标点击 和自动生成
增加一些全局变量
// 防抖的标记
let timerTick = 0
let limiterTick = 0
// 用于鼠标点击生成烟花
let mouseCoord = {x: 0, y: 0} // 当前鼠标坐标
let isMousedown = false // 鼠标是否按下
修改下render函数,这里让每循环调用80次 自动生成8个烟花,点击时做个防抖 循环5次时才生成一个烟花
function render() {
// 前面省略…
// 函数循环80次自动发射8支烟花
if (timerTick >= 80) {
if (!isMousedown) {
for (let i = 0; i < 8; i++) {
fireworks.push(new Firework(canvasWidth / 2, canvasHeight, randomRange(0, canvasWidth), randomRange(0, canvasHeight / 2)))
}
timerTick = 0
}
} else {
timerTick++
}
// 达到循环5次且鼠标按下时 发射一个烟火
if (limiterTick >= 5) {
if (isMousedown) {
fireworks.push(new Firework(canvasWidth / 2, canvasHeight, mouseCoord.x, mouseCoord.y))
limiterTick = 0 // 清0
}
} else {
limiterTick++
}
}
后面会继续更新,请关注