头图

今年杭州又开始办烟花大会了,那烟花真是漂亮。没有机会现场看的话,作为前端,可以自己实现一个烟花页面。
来跟我一起,把这个烟花代码学废。

功能拆分

常见的烟花都是从一个地方往上发出,到一定高度后,爆炸成一圈光点。高度有不同,圈的大小有不同,光点的颜色也会有不同。
所以一个烟花的过程可以划分成2步:1. 从某个位置P0上升到P1;2. 从P1爆炸成一圈光点,光点向外扩散,光点扩散到一定程度,开始渐渐消失。

逐步写代码

这个烟花,我们需要用canvas来画,所以我们需要先把画布弄好:

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
// 清空画布
function clearCanvas() {
  ctx.fillStyle = '#000000';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}
clearCanvas();

烟花升空

先把烟花的基础信息设置好,包括烟花粒子的大小,开始放烟花的位置。

const size = 2; // 烟花粒子的大小
const start = {
  x: canvas.width / 2,
  y: canvas.height - 100,
};

然后做一个鼠标点击事件,当点击canvas时,就会开始播放烟花(fire方法)。在fire方法中,不断的去更新当前烟花粒子的位置,形成一个烟花往上的效果。

let rid = 0;
function fire() {
  const p = {x: start.x, y: start.y}
  function tick() {
    clearCanvas();
    step1(p);
    rid = requestAnimationFrame(tick);
  }
  cancelAnimationFrame(rid);
  tick()
}

// 烟花第一步,升空
function step1(p) {
  p.y = p.y - 4; // 每一帧都改变粒子的位置
  ctx.beginPath();
  ctx.arc(p.x, p.y, size, 0, Math.PI*2, false)
  ctx.closePath();
  ctx.fillStyle = "#ffffff";
  ctx.fill();
}

function mouseDownHandler(e) {
  fire();
}
document.addEventListener('mousedown', mouseDownHandler, false);


当烟花上升过程中,速度会越来越慢,当到达一定高度时,上升速度会到0,并且进入第二阶段。所以p.y的改变需要改成变化的,然后加一个进入第二状态的控制。

function tick() {
  clearCanvas();
  // 根据状态控制阶段
  if (p.state === 1) {
    step1(p);
  } else if(p.state === 2) {
    step2(p);
  }
}
function step1(p) {
  if (p.upSpeed < 0.1) { // 速度很低时,进入第二阶段
    p.state = 2;
    return;
  }
  p.upSpeed = p.upSpeed - 0.05; // 速度变化逐步降低
  p.y = p.y - p.upSpeed;
  ...
}

烟花炸开

进入第二阶段后,需要炸开一堆往四周扩散的粒子。我们先做出一圈粒子,再增加动画和粒子。

let radius = 10; // 粒子环绕半径
function step2(p) {
  const count = 10; // 10个粒子
  radius++ // 半径不断变大,形成动画
  for (var i = 0; i < count; i++) {
    var angle = 360/count * i;
    let radians = angle * Math.PI / 180;
    let vx = Math.cos(radians) * radius;
    let vy = Math.sin(radians) * radius;

    ctx.beginPath();
    ctx.arc(p.x - 5 + vx, p.y - 5 + vy, size, 0, Math.PI * 2, false);
    ctx.closePath();
    ctx.fillStyle = '#ffffff';
    ctx.fill();
  }
}

然后现在我们要增加粒子的个数,这里改变count就行,每一个粒子并不是平均分布的,所以angle和radius要随机。有step2是每次刷新都会重新运行的,所以粒子的生产需要单独出一个方法来,不然每次刷新,粒子都要重新随机产生,这样粒子就乱飞了。

// 改造方法,把烟花的创建放到一起。支出同时多个烟花
var fireworks = [];
function createFireworks(p) {
  const firework = {
    x: start.x,
    y: start.y,
    upSpeed: 12,
    state: 1,
    particles: [], // 炸开后的粒子
  };
  var count = Math.floor(Math.random() * 100) + 80;
  for (var i = 0; i < count; i++) {
    var p = {vx:0, vy:0}; // 每一个粒子,vx,vy表示相对暴涨位置的偏移
    var angle = Math.floor(Math.random() * 360);
    p.radians = angle * Math.PI / 180;
    p.speed = (Math.random() * 5) + .4;
    p.radius = p.speed;
    p.size = Math.floor(Math.random() * 3) + 1;
    firework.particles.push(p);
  }
}

function step2(f) {
  const particles = f.particles;
  for (let i = 0, len = particles.length; i < len; i++) {
    const p = particles[i];
    let vx = Math.cos(p.radians) * p.radius;
    let vy = Math.sin(p.radians) * p.radius + 0.4;

    p.vx += vx;
    p.vy += vy;
    p.radius *= 1 - p.speed / 100; // 逐步变慢
    ctx.beginPath();
    ctx.arc(f.x - 5 + p.vx, f.y - 5 + p.vy, size, 0, Math.PI * 2, false);
    ctx.closePath();
    ctx.fillStyle = '#ffffff';
    ctx.fill();
  }
}

到这一步,烟花基本样子已经OK了,后面就是在一段时间后,烟花需要消失,以及对烟花做不同的样式用于区分,最好是发射出去的角度也要有不同,起始位置也要有所不同。

烟花颜色不同以及消失

烟花消失比较简单,对每一个粒子做一个透明度就行,透明度逐渐变小,最终消失。颜色的话,可以通过hsla来做随机。

function createFireworks() {
  const firework = {...};
  let count = Math.floor(Math.random() * 100) + 80;
  let hue = Math.floor(Math.random() * 6) * 60; // 分6种主颜色
  for (let i = 0; i < count; i++) {
    ...
    p.hue = Math.floor(Math.random() * 30) + hue; // 每一种主颜色上略微变一变
    p.brightness = Math.floor(Math.random() * 30) + 70;
    p.alpha = (Math.floor(Math.random() * 61) + 40) / 100;
    ...
  }
}
function step2(f) {
  for (let i = 0, len = particles.length; i < len; i++) {
    ...
    p.alpha -= 0.005; // 逐渐消失
    ...
    ctx.fillStyle = 'hsla(' + p.hue + ', 100%, ' + p.brightness + '%, ' + p.alpha + ')'; // hsla颜色
  }
}

烟花起始位置和发射角度随机

最后就是把烟花的发出位置和发射角度做一定的随机。

function createFireworks() {
  const firework = {
    x: start.x + Math.floor(Math.random() * 100) - 50, // x上的位置随机
    ...
  };
  ...
}
function step1(p) {
  ...
  p.x = p.x + p.xAngle; // 上升过程中,x方向的变化
  ...
}

完美,已经比得上烟花大会的烟花效果了。最后就是加一些不同类型的烟花,我这里简单弄一个心型的,复杂的就靠各位了。

不同类型的烟花

如果要增加一个爱心型的烟花,要怎么做呢,首先需要把爆炸的step2给拆分下,支持多种类型的渲染。

function step2(f) {
  const particles = f.particles;
  for (let i = 0, len = particles.length; i < len; i++) {
    const p = particles[i];
    if (p.alpha <= 0) {
      continue;
    }
    // 根据不同的烟花类型,采用不同的渲染方法
    switch(f.type){
      case 1:
        step2_circle(p, f.x, f.y); // 这个是原先的圆形烟花
        break;
      case 2:
        step2_heart(p, f.x, f.y);
        break;
      default:
        step2_circle(p, f.x, f.y);
        break;
    }
  }
}

// createFireworks生产的firework添加一个type属性,用于控制不同的烟花类型
function createFireworks(type) {
  const firework = {
    ...
    type: Math.floor(Math.random() * 2) + 1,
  };
}

爱心型

最后就是把爱心的烟花做一个单独的渲染方法:

// 爆炸后的效果是爱心
function step2_heart(p, x, y) {
  const t = p.radians
  let vx = p.radius * Math.pow(Math.sin(t), 3);
  let vy = p.radius / 1.2 * Math.cos(t)
    - p.radius / 3.2 * Math.cos(2*t) 
    - p.radius / 8 * Math.cos(3*t) 
    - p.radius / 16 * Math.cos(4*t) 
    + p.radius / 6.4;

  p.vx += vx;
  p.vy -= vy;
  p.radius *= 1 - p.speed / 100; // 逐步变慢
  p.alpha -= 0.005;
  ctx.beginPath();
  ctx.arc(x - 5 + p.vx, y - 5 + p.vy, size, 0, Math.PI * 2, false);
  ctx.closePath();
  ctx.fillStyle = `hsla(${p.hue}, 100%, ${p.brightness}%, ${p.alpha})`;
  ctx.fill();
}

最后

最后有一个疑问,如果要实现烟花炸开是一段文字,该怎么做呢?

结束

好了,本文到此结束,希望本文对你有所帮助 :-)
最近新弄了一个公众号:写代码的浩,求关注 😄。后面会逐步把掌握的前端知识以及职场知识沉淀下来。
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


写代码的浩
80 声望7 粉丝