没有烟花的总感觉少了点年味,政策不给放?那就用代码写一场烟花秀

351 阅读5分钟

说在前面

现在很多地方过年都禁止燃放烟花了,没有烟花总感觉少了点年味,现实中我们需要遵守法律法规,无法燃放烟花,那么为什么不试试到网上来放个烟花

效果展示

效果图.png

预览地址

jyeontu.xyz/firework/

实现思路

利用 HTML5 的 Canvas 元素和 JavaScript 实现烟花从发射到绽放的动画效果。主要分为以下几个步骤:

  1. 初始化设置:包括设置画布大小、定义全局变量和封装动画帧请求函数。
  2. 创建烟花和粒子对象:定义烟花和粒子的属性和行为。
  3. 更新和绘制对象:不断更新烟花和粒子的位置,并将它们绘制到画布上。
  4. 处理用户交互:监听鼠标事件,根据用户点击位置发射烟花。
  5. 循环动画:使用 requestAnimationFrame 实现动画的循环更新。

代码实现

1. HTML 结构

<!doctype html>
<html>
  <head>
    <style>
      body {
        background: #132c33;
        margin: 0;
        color: white;
      }
      canvas {
        cursor: crosshair;
        display: block;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas">Canvas is not supported in your browser.</canvas>
    <script>
      // JavaScript 代码部分
    </script>
  </body>
</html>

HTML 部分非常简单,包含一个 canvas 元素用于绘制烟花特效,同时通过 CSS 设置了页面背景颜色和 canvas 的样式。

2. JavaScript 代码

2.1 初始化设置

//requestAnimFrame 封装,可以兼容所有浏览器
window.requestAnimFrame = (function () {
  return (
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function (callback) {
      window.setTimeout(callback, 1000 / 60);
    }
  );
})();

var canvas = document.getElementById("canvas"),
  ctx = canvas.getContext("2d"),
  cw = window.innerWidth,
  ch = window.innerHeight,
  fireworks = [],
  particles = [],
  hue = 180,
  limiterTotal = 5,
  limiterTick = 0,
  timerTotal = 40,
  timerTick = 0,
  mousedown = false,
  mx,
  my;

canvas.width = cw;
canvas.height = ch;
  • requestAnimFrame:封装了 requestAnimationFrame 函数,确保在不同浏览器中都能正常使用,实现平滑的动画效果。
  • 获取 canvas 元素和 2D 绘图上下文。
  • 获取浏览器窗口的宽度和高度,并设置 canvas 的大小。
  • 定义了一些全局变量,用于存储烟花、粒子、颜色、速度控制等信息。

2.2 辅助函数

// 随机函数
function random(min, max) {
  return Math.random() * (max - min) + min;
}

// 计算两点距离
function calculateDistance(p1x, p1y, p2x, p2y) {
  var xDistance = p1x - p2x,
    yDistance = p1y - p2y;
  return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
  • random 函数用于生成指定范围内的随机数。
  • calculateDistance 函数用于计算两点之间的距离,在烟花运动和判断是否到达目标位置时会用到。

2.3 烟花对象

// 定义烟花对象
function Firework(sx, sy, tx, ty) {
  this.x = sx;
  this.y = sy;
  this.sx = sx;
  this.sy = sy;
  this.tx = tx;
  this.ty = ty;
  this.distanceToTarget = calculateDistance(sx, sy, tx, ty);
  this.distanceTraveled = 0;
  this.coordinates = [];
  this.coordinateCount = 3;
  while (this.coordinateCount--) {
    this.coordinates.push([this.x, this.y]);
  }
  this.angle = Math.atan2(ty - sy, tx - sx);
  this.speed = 2;
  this.acceleration = 1.05;
  this.brightness = random(50, 70);
  this.targetRadius = 1;
}

//更新烟花位置
Firework.prototype.update = function (index) {
  this.coordinates.pop();
  this.coordinates.unshift([this.x, this.y]);
  if (this.targetRadius < 8) {
    this.targetRadius += 0.3;
  } else {
    this.targetRadius = 1;
  }
  this.speed *= this.acceleration;
  var vx = Math.cos(this.angle) * this.speed,
    vy = Math.sin(this.angle) * this.speed;
  this.distanceTraveled = calculateDistance(
    this.sx,
    this.sy,
    this.x + vx,
    this.y + vy
  );
  if (this.distanceTraveled >= this.distanceToTarget) {
    createParticles(this.tx, this.ty);
    fireworks.splice(index, 1);
  } else {
    this.x += vx;
    this.y += vy;
  }
};

// 烟花射线发射
Firework.prototype.draw = function () {
  ctx.beginPath();
  ctx.moveTo(
    this.coordinates[this.coordinates.length - 1][0],
    this.coordinates[this.coordinates.length - 1][1]
  );
  ctx.lineTo(this.x, this.y);
  ctx.strokeStyle = "hsl(" + hue + ", 100%, " + this.brightness + "%)";
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(this.tx, this.ty, this.targetRadius, 0, Math.PI * 2);
  ctx.stroke();
};
  • Firework 构造函数:创建一个烟花对象,包含起始位置、目标位置、运动距离、速度、亮度等属性。
  • update 方法:更新烟花的位置和状态。当烟花到达目标位置时,调用 createParticles 函数创建粒子效果,并从 fireworks 数组中移除该烟花。
  • draw 方法:绘制烟花的射线和目标位置的圆圈。

2.4 粒子对象

//烟花绽放方法
function Particle(x, y) {
  this.x = x;
  this.y = y;
  this.coordinates = [];
  this.coordinateCount = 5;
  while (this.coordinateCount--) {
    this.coordinates.push([this.x, this.y]);
  }
  this.angle = random(0, Math.PI * 2);
  this.speed = random(1, 10);
  this.friction = 0.95;
  this.gravity = 1;
  this.hue = random(hue - 20, hue + 20);
  this.brightness = random(50, 80);
  this.alpha = 1;
  this.decay = random(0.015, 0.03);
}

Particle.prototype.update = function (index) {
  this.coordinates.pop();
  this.coordinates.unshift([this.x, this.y]);
  this.speed *= this.friction;
  this.x += Math.cos(this.angle) * this.speed;
  this.y += Math.sin(this.angle) * this.speed + this.gravity;
  this.alpha -= this.decay;
  if (this.alpha <= this.decay) {
    particles.splice(index, 1);
  }
};

// 烟花绽放
Particle.prototype.draw = function () {
  ctx.beginPath();
  ctx.moveTo(
    this.coordinates[this.coordinates.length - 1][0],
    this.coordinates[this.coordinates.length - 1][1]
  );
  ctx.lineTo(this.x, this.y);
  ctx.strokeStyle =
    "hsla(" +
    this.hue +
    ", 100%, " +
    this.brightness +
    "%, " +
    this.alpha +
    ")";
  ctx.stroke();
};

// 创建粒子
function createParticles(x, y) {
  var particleCount = 200;
  while (particleCount--) {
    particles.push(new Particle(x, y));
  }
}
  • Particle 构造函数:创建一个粒子对象,包含位置、运动方向、速度、颜色、透明度等属性。
  • update 方法:更新粒子的位置和透明度。当粒子的透明度小于一定值时,从 particles 数组中移除该粒子。
  • draw 方法:绘制粒子的线条。
  • createParticles 函数:在指定位置创建多个粒子对象,模拟烟花绽放的效果。

2.5 动画循环

function loop() {
  requestAnimFrame(loop);
  hue += 0.5;
  ctx.globalCompositeOperation = "destination-out";
  ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
  ctx.fillRect(0, 0, cw, ch);
  ctx.globalCompositeOperation = "lighter";
  var i = fireworks.length;
  while (i--) {
    fireworks[i].draw();
    fireworks[i].update(i);
  }
  var i = particles.length;
  while (i--) {
    particles[i].draw();
    particles[i].update(i);
  }
  if (timerTick >= timerTotal) {
    if (!mousedown) {
      fireworks.push(
        new Firework(cw / 2, ch, random(0, cw), random(0, ch / 2))
      );
      timerTick = 0;
    }
  } else {
    timerTick++;
  }
  if (limiterTick >= limiterTotal) {
    if (mousedown) {
      fireworks.push(new Firework(cw / 2, ch, mx, my));
      limiterTick = 0;
    }
  } else {
    limiterTick++;
  }
}
  • loop 函数:使用 requestAnimFrame 实现动画的循环更新。
  • 每次循环更新颜色值 hue,并通过 destination-out 合成模式和半透明黑色填充画布,实现渐变消失的效果。
  • 遍历 fireworksparticles 数组,分别调用 drawupdate 方法更新和绘制烟花及粒子。
  • 根据计时器控制自动发射烟花和根据鼠标点击发射烟花的频率。

2.6 事件监听

canvas.addEventListener("mousemove", function (e) {
  mx = e.pageX - canvas.offsetLeft;
  my = e.pageY - canvas.offsetTop;
});

canvas.addEventListener("mousedown", function (e) {
  e.preventDefault();
  mousedown = true;
});

canvas.addEventListener("mouseup", function (e) {
  e.preventDefault();
  mousedown = false;
});

window.onload = loop;
  • 监听鼠标移动事件,记录鼠标位置。
  • 监听鼠标按下和松开事件,控制是否根据鼠标点击发射烟花。
  • 页面加载完成后,调用 loop 函数开始动画循环。

源码地址

gitee

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟觉得有帮助的可以点个star~
  • 🖊有什么问题或错误可以指出,欢迎pr~
  • 📬有什么想要实现的组件或想法可以联系我~

公众号

关注公众号『前端也能这么有趣』,获取更多有趣内容。

公众号发送 加群 可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。