Нарисовать фейерверк на холсте

внешний интерфейс Canvas

Начинать

окончательный эффект:codepen

Он начинается с одного варианта использования, расположенного в центре холста, а затем расширяется.

Сначала получите элемент холста и видимую ширину и высоту

    let canvas = document.querySelector('#canvas')
    let context = canvas.getContext('2d')
    let cw = canvas.width = window.innerWidth
    let ch = canvas.height = window.innerHeight

начать рисовать

Часть 1 - Мигающие круги для позиционирования

// 创建一个闪烁圆的类
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)

Часть 2 — Рисование лучей

Сначала проведите выносную линию снизу к центру холста. Поскольку это расширенная линия движения, будет по крайней мере координата начальной точки и координата конечной точки.

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()

Расскажите о тригонометрических функциях.

Зная начальную точку координат и конечную точку координат, возникает вопрос, как узнать координаты каждого кадра от начальной до конечной точки?

Как показано. Цели, которые должны быть оценены, вероятно,

  1. Превышает ли расстояние движения линии расстояние от начальной точки до конечной точки, если оно превышает, движение необходимо остановить
  2. Координаты прихода каждого кадра движения

Рассчитать расстояние

Для вычисления расстояния между координатами очевидно, что это можно сделать с помощью теоремы Пифагора.
Пусть координаты начальной точки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
        ...
    }
}

Часть 3 - Рисуем эффект взрыва

Из приведенного выше кода выносной линии, если вы не берете 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()

Часть 4. Слияние кода и от одного ко многим

Слияние кодовых слов, основная проблема в последовательности.
На месте сверкающая координатная точка является целевой конечной точкой луча, а также начальной точкой координаты взрыва. Время, когда конец луча достигает конца, может быть активирован метод взрыва.

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()

Более забавные эффекты, полученные в процессе производства

  1. codepen
  2. codepen