01 不一样の烟火
anime.js 是一个强大的 JavaScript 动画库,HeartBeat 主题的背景点击特效就是借用其官网展示效果。为了学习动画库的使用,这里用 ES6 重构了烟火代码,来一场不一样的烟火。
Anime is a lightweight JavaScript animation library. It works with any CSS Properties, individual CSS transforms, SVG or any DOM attributes, and JavaScript Objects.
不一样の烟火
在开始之前,先链上 Source Code 和 在线预览。
引入 anime.min.js
首先在引入 anime.min.js,这里使用 BootCDN 外链。然后创建一个 canvas 画布,用来呈现烟火效果。在 body 标签尾部引入 index.js,接下来就是在 index.js 完成最终的烟火代码。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>不一样の烟火</title>
    <script src="https://cdn.bootcss.com/animejs/2.2.0/anime.min.js"></script>
  </head>
  <style>
    body {
      background: #000;
      overflow: hidden;
    }
  </style>
  <body>
    <canvas class="fireworks"></canvas>
    <script src="./index.js"></script>
  </body>
</html>初始化画布
在 index.js 中,新建一个 Firework 类,并初始化画布大小尺寸。
class Firework {
  constructor() {
    this.canvasEl = null // 画布元素
    this.ctx = null // 画布上下文
  }
  // Let's go
  start() {
    // 初始化画布
    this.setCanvasSize()
  }
  // 设置画布尺寸
  setCanvasSize() {
    // 获取画布元素
    const canvasEl = document.querySelector(".fireworks")
    const ctx = canvasEl.getContext("2d")
    // 窗口尺寸
    const innerWidth = window.innerWidth
    const innerHeight = window.innerHeight
    // 设置画布尺寸
    canvasEl.width = innerWidth * 2
    canvasEl.height = innerHeight * 2
    canvasEl.style.width = innerWidth + "px"
    canvasEl.style.height = innerHeight + "px"
    ctx.scale(2, 2)
    // 保存画布
    this.canvasEl = canvasEl
    this.ctx = ctx
  }
}
const margicAnime = new Firework()
margicAnime.start()绑定事件
接下来监听点击事件以绘制动画,并且监听窗口缩放事件,当窗口大小变化时重置画布尺寸。为了兼容不同浏览器,这里将事件绑定和解绑方法提取出公用方法。
/**
 * @description 绑定事件 on(element, event, handler)
 */
const on = (function () {
  if (document.addEventListener) {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.attachEvent("on" + event, handler)
      }
    }
  }
})()
/**
 * @description 解绑事件 off(element, event, handler)
 */
const off = (function () {
  if (document.removeEventListener) {
    return function (element, event, handler) {
      if (element && event) {
        element.removeEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event) {
        element.detachEvent("on" + event, handler)
      }
    }
  }
})()然后添加绑定事件,并且添加销毁方法,在销毁時解绑事件:
// 点击事件
const tap =
  "ontouchstart" in window || navigator.msMaxTouchPoints
    ? "touchstart"
    : "mousedown"
class Firework {
  // Let's go
  start() {
    // 初始化画布
    this.setCanvasSize()
    // 监听点击和窗口缩放事件
    on(document, tap, this.render.bind(this))
    on(window, "resize", this.setCanvasSize.bind(this))
  }
  // 销毁
  destroyed() {
    off(document, tap, this.render.bind(this))
    off(window, "resize", this.setCanvasSize.bind(this))
    this.tapFunc = this.resizeFunc = this.renderAnime = null
  }
  // 点击事件
  render() {}
}擦除与绘制
借助 anime.js,可以很方便在每一帧画布更新后擦除画布,通过不断清除画布内容再绘制,形成动画效果。
class Firework {
  // 点击事件
  render(e) {
    const canvasEl = this.canvasEl
    const ctx = this.ctx
    // 绘制前启用擦除动画
    if (!this.renderAnime) {
      this.renderAnime = anime({
        duration: Infinity,
        update() {
          // 擦除画布
          ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
        },
      })
    }
    this.renderAnime.play()
    // 点击坐标
    const pointerX = e.clientX || e.touches[0].clientX
    const pointerY = e.clientY || e.touches[0].clientY
    this.animateParticules(pointerX, pointerY)
  }
  // 绘制烟火
  animateParticules() {}
}绘制烟火
绘制烟火是最为核心代码,烟火由扩散圈的烟火粒子两部分组成。并在每一个动画帧更新后重新绘制粒子。
class Firework {
  constructor() {
    this.numberOfParticules = 30 // 粒子数量
    this.colors = ["#FF1461", "#18FF92", "#5A87FF", "#FBF38C"] // 粒子颜色
  }
  // 创建扩散圈
  createCircle(x, y) {
    const ctx = this.ctx
    const p = {}
    p.x = x
    p.y = y
    p.color = "#FFF"
    p.radius = 0.1
    p.alpha = 0.5
    p.lineWidth = 6
    p.draw = function () {
      ctx.globalAlpha = p.alpha
      ctx.beginPath()
      // 绘制正圆
      ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
      ctx.lineWidth = p.lineWidth
      ctx.strokeStyle = p.color
      ctx.stroke()
      ctx.globalAlpha = 1
    }
    return p
  }
  // 创建粒子
  createParticule(x, y) {
    const ctx = this.ctx
    const p = {}
    p.x = x
    p.y = y
    p.color = this.colors[anime.random(0, this.colors.length - 1)]
    p.radius = anime.random(16, 32)
    p.endPos = this.setParticuleDirection(p)
    p.draw = function () {
      ctx.beginPath()
      ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
      ctx.fillStyle = p.color
      ctx.fill()
    }
    return p
  }
  // 粒子扩散方向
  setParticuleDirection(p) {
    const angle = (anime.random(0, 360) * Math.PI) / 180
    const value = anime.random(50, 180)
    const radius = [-1, 1][anime.random(0, 1)] * value
    return {
      x: p.x + radius * Math.cos(angle),
      y: p.y + radius * Math.sin(angle),
    }
  }
  // 绘制粒子
  renderParticule(anim) {
    for (let i = 0; i < anim.animatables.length; i++) {
      anim.animatables[i].target.draw()
    }
  }
  // 绘制烟火
  animateParticules(x, y) {
    const circle = this.createCircle(x, y)
    const particules = []
    for (let i = 0; i < this.numberOfParticules; i++) {
      particules.push(this.createParticule(x, y))
    }
    const renderParticule = this.renderParticule
    anime
      .timeline()
      .add({
        targets: particules,
        x(p) {
          return p.endPos.x
        },
        y(p) {
          return p.endPos.y
        },
        radius: 0.1,
        duration: anime.random(1200, 1800),
        easing: "easeOutExpo",
        // 每一个动画帧更新后重新绘制粒子
        update: renderParticule,
      })
      .add({
        targets: circle,
        radius: anime.random(80, 160),
        lineWidth: 0,
        alpha: {
          value: 0,
          easing: "linear",
          duration: anime.random(600, 800),
        },
        duration: anime.random(1200, 1800),
        easing: "easeOutExpo",
        update: renderParticule,
        offset: 0,
      })
  }
}大功告成
最终烟火效果代码如下:
/**
 * @description 绑定事件 on(element, event, handler)
 */
const on = (function () {
  if (document.addEventListener) {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.attachEvent("on" + event, handler)
      }
    }
  }
})()
/**
 * @description 解绑事件 off(element, event, handler)
 */
const off = (function () {
  if (document.removeEventListener) {
    return function (element, event, handler) {
      if (element && event) {
        element.removeEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event) {
        element.detachEvent("on" + event, handler)
      }
    }
  }
})()
// 点击事件
const tap =
  "ontouchstart" in window || navigator.msMaxTouchPoints
    ? "touchstart"
    : "mousedown"
class Firework {
  constructor() {
    this.canvasEl = null // 画布元素
    this.ctx = null // 画布上下文
    this.numberOfParticules = 30 // 粒子数量
    this.colors = ["#FF1461", "#18FF92", "#5A87FF", "#FBF38C"] // 粒子颜色
    this.tapFunc = null
    this.resizeFunc = null
    this.renderAnime = null
  }
  // Let's go
  start() {
    // 初始化画布
    this.setCanvasSize()
    // 监听点击和窗口缩放事件
    on(document, tap, this.render.bind(this))
    on(window, "resize", this.setCanvasSize.bind(this))
  }
  // 销毁
  destroyed() {
    off(document, tap, this.render.bind(this))
    off(window, "resize", this.setCanvasSize.bind(this))
    this.tapFunc = this.resizeFunc = this.renderAnime = null
  }
  // 设置画布尺寸
  setCanvasSize() {
    // 获取画布元素
    const canvasEl = document.querySelector(".fireworks")
    const ctx = canvasEl.getContext("2d")
    // 窗口尺寸
    const innerWidth = window.innerWidth
    const innerHeight = window.innerHeight
    // 设置画布尺寸
    canvasEl.width = innerWidth * 2
    canvasEl.height = innerHeight * 2
    canvasEl.style.width = innerWidth + "px"
    canvasEl.style.height = innerHeight + "px"
    ctx.scale(2, 2)
    // 保存画布
    this.canvasEl = canvasEl
    this.ctx = ctx
  }
  // 创建点击扩散圈
  createCircle(x, y) {
    const ctx = this.ctx
    const p = {}
    p.x = x
    p.y = y
    p.color = "#FFF"
    p.radius = 0.1
    p.alpha = 0.5
    p.lineWidth = 6
    p.draw = function () {
      ctx.globalAlpha = p.alpha
      ctx.beginPath()
      ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
      ctx.lineWidth = p.lineWidth
      ctx.strokeStyle = p.color
      ctx.stroke()
      ctx.globalAlpha = 1
    }
    return p
  }
  // 创建点击粒子
  createParticule(x, y) {
    const ctx = this.ctx
    const p = {}
    p.x = x
    p.y = y
    p.color = this.colors[anime.random(0, this.colors.length - 1)]
    p.radius = anime.random(16, 32)
    p.endPos = this.setParticuleDirection(p)
    p.draw = function () {
      ctx.beginPath()
      ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
      ctx.fillStyle = p.color
      ctx.fill()
    }
    return p
  }
  // 粒子扩散方向
  setParticuleDirection(p) {
    const angle = (anime.random(0, 360) * Math.PI) / 180
    const value = anime.random(50, 180)
    const radius = [-1, 1][anime.random(0, 1)] * value
    return {
      x: p.x + radius * Math.cos(angle),
      y: p.y + radius * Math.sin(angle),
    }
  }
  // 绘制粒子
  renderParticule(anim) {
    for (let i = 0; i < anim.animatables.length; i++) {
      anim.animatables[i].target.draw()
    }
  }
  // 绘制烟火
  animateParticules(x, y) {
    const circle = this.createCircle(x, y)
    const particules = []
    for (let i = 0; i < this.numberOfParticules; i++) {
      particules.push(this.createParticule(x, y))
    }
    const renderParticule = this.renderParticule
    anime
      .timeline()
      .add({
        targets: particules,
        x(p) {
          return p.endPos.x
        },
        y(p) {
          return p.endPos.y
        },
        radius: 0.1,
        duration: anime.random(1200, 1800),
        easing: "easeOutExpo",
        update: renderParticule,
      })
      .add({
        targets: circle,
        radius: anime.random(80, 160),
        lineWidth: 0,
        alpha: {
          value: 0,
          easing: "linear",
          duration: anime.random(600, 800),
        },
        duration: anime.random(1200, 1800),
        easing: "easeOutExpo",
        update: renderParticule,
        offset: 0,
      })
  }
  // 点击事件
  render(e) {
    const canvasEl = this.canvasEl
    const ctx = this.ctx
    // 绘制前启用擦除画布
    if (!this.renderAnime) {
      this.renderAnime = anime({
        duration: Infinity,
        update() {
          ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
        },
      })
    }
    this.renderAnime.play()
    // 点击坐标
    const pointerX = e.clientX || e.touches[0].clientX
    const pointerY = e.clientY || e.touches[0].clientY
    this.animateParticules(pointerX, pointerY)
  }
}
const margicAnime = new Firework()
margicAnime.start()Just enjoy it 😃!
最后更新于
这有帮助吗?
