Начинать
окончательный эффект: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()
Расскажите о тригонометрических функциях.
Зная начальную точку координат и конечную точку координат, возникает вопрос, как узнать координаты каждого кадра от начальной до конечной точки?
Как показано. Цели, которые должны быть оценены, вероятно,- Превышает ли расстояние движения линии расстояние от начальной точки до конечной точки, если оно превышает, движение необходимо остановить
- Координаты прихода каждого кадра движения
Рассчитать расстояние
Для вычисления расстояния между координатами очевидно, что это можно сделать с помощью теоремы Пифагора.
Пусть координаты начальной точки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()
Более забавные эффекты, полученные в процессе производства