CHANSHIYU
GITHUBZERO
  • README
  • 時雨
    • 2017
      • 01 网站动态标题的两种方式
      • 02 RN App 外部唤醒踩坑记
    • 2018
      • 01 不一样の烟火
      • 02 Python 之禅
      • 03 Python 文件操作
    • 2019
      • 01 Aurora 食用指南
      • 02 Godaddy 域名找回记事
      • 03 一个接口的诞生
      • 04 SpringMVC 前后端传参协调
      • 05 主题集成友链访问统计
      • 06 Github Style 博客主题
      • 07 字符编码の小常识
      • 08 WSL 安装 Docker 实录
      • 09 Eriri comic reader
      • 10 Aurora 2.0
      • 11 jsDelivr 全站托管
      • 12 两年工作台变迁史
      • 13 春物
      • 14 一种优雅の笔记方式
    • 2020
      • 01 Telegram 电报机器人
      • 02 她的眼里有星辰
      • 03 文心雕龙
      • 04 软萌木子の有趣笔谈
      • 05 Telegram RSS 订阅频道
      • 06 水月雨银色飞船
      • 07 五年前旧照
    • 2021
      • 01 春宵苦短 2020
      • 02 风花
    • 2022
      • 01 小城新貌
      • 02 原神满级纪念
    • 2023
      • 01 2022 逆旅
      • 02 半透明背景图实现
      • 03 新年攒台海景房
  • 前端
    • JavaScript
      • 01 JavaScript 秘密花园
      • 02 JavaScript 正则技巧
      • 03 从浏览器解析 JS 运行机制
      • 04 Canvas 基础用法
      • 05 Blob Url And Data Url
      • 06 函数节流与函数防抖
      • 07 排序算法初探
      • 08 洗牌算法实现数组乱序
      • 09 正则匹配 match 和 exec
      • 10 正则匹配汉字
      • 11 JSX.Element vs ReactElement
      • 12 可选链与空值合并
      • 13 TypeScript 编码规范
      • 14 Typescript 中 interface 和 type 区别
      • 15 TypeScript 高级类型
      • 16 TypeScript 关键字
      • 17 TypeScript 映射类型
    • CSS
      • 01 Flex 弹性布局
      • 02 Position 定位
      • 03 CSS 逻辑属性
    • Node
      • 01 Node Tips
      • 02 七天学会 NodeJS
    • Note
      • 01 Note
      • 02 Code
      • 03 Snippets
      • 04 Git
    • React
      • 01 React Props Children 传值
      • 02 Use a Render Prop!
      • 03 React Hook
      • 04 React Hook 定时器
      • 05 Fetch data with React Hooks
      • 06 React 和 Vue 中 key 的作用
      • 07 useCallback 的正确使用方式
      • 08 useLayoutEffect 和 useEffect 的区别
      • 09 forwardRef 逃生舱
      • 10 React 条件渲染
    • Vue
      • 01 Vue Tips
      • 02 Vue 构建项目写入配置文件
      • 03 Vue 项目引入 SVG 图标
      • 04 Vue 一键导出 PDF
      • 05 动态可响应对象
      • 06 Vue 引入 SCSS
      • 07 Vue 路由权限控制
    • 实战系列
      • 01 WebSocket 心跳重连机制
      • 02 图片加解密二三事
      • 03 优雅实现 BackTop
      • 04 动态加载 JS 文件
      • 05 常用 DOM 方法比较
      • 06 AbortController 中断 fetch
      • 07 计算字符所占字节数
      • 08 Axios 自定义返回值类型
  • 后端
    • Java
      • 01 面向对象基本特征与原则
      • 02 Java 数据类型
      • 03 Java String
      • 04 Java 只有值传递
      • 05 Java final 与 static
      • 06 Java Object 通用方法
      • 07 Java 继承
      • 08 Java 反射
      • 09 Java 异常
      • 10 Java 容器
      • 11 Java 虚拟机
      • 12 Java IO
      • 13 Java HashMap
      • 14 Java List
      • 15 Java Stream
      • 16 Java 枚举
      • 17 Java 日期与时间
      • 18 Java fail fast
      • 19 Java BiFunction 和 BinaryOperator
    • 并发编程
      • 01 Java 并发
      • 02 synchronized
      • 03 volatile
      • 04 ReentrantLock
      • 05 ReadWriteLock
      • 06 StampedLock
      • 07 CompletableFuture
      • 08 ForkJoin
      • 09 ThreadLocal
      • 10 CountDownLatch
      • 11 ThreadPoolExecutor
      • 12 ExecutorService
      • 13 Atom 原子类
      • 14 BlockingQueue
    • 高效编程
      • 01 30 seconds of java8
      • 02 函数式替代 for 循环
      • 03 Java 字符串拼接
      • 04 单例模式的几种实现
      • 05 HashMap 排序
    • 理论概念
      • 01 Java Servlet
      • 02 Java 服务端分层模型
      • 03 经典排序算法
      • 04 LRU 缓存淘汰算法
      • 05 BloomFilter 判断元素存在
      • 06 Java HashMap 面试大全
      • 07 HTTP 状态码详解
      • 08 Cookie 和 Session
      • 09 基于消息队列的分布式事务解决方案
      • 10 微服务之所见
    • 实战系列
      • 01 AES CBC 加解密
      • 02 Magic 魔数获取文件类型
      • 03 获取请求 IP 地址
      • 04 Kaptcha 与数学公式验证码
      • 05 Netty 获取客户端 IP.md
      • 06 高性能无锁队列 Disruptor.md
      • 07 前后端接入阿里云盾
    • Linux
      • 01 Linux 文件权限系统
      • 02 Linux 常用软件安装
      • 03 CentOS 防火墙
    • MySQL
      • 01 MySQL
      • 02 SQL 语句 where 1=1
      • 03 truncate 和 delete
      • 04 事务
      • 05 关系模型
      • 06 Mybatis
      • 07 MySQL 查看数据库表详情
    • Nginx
      • 01 Nginx 指北
      • 02 nginx gzip 压缩
    • Note
      • 01 Vagrant
      • 02 Docker
      • 03 Lombok
      • 04 Swagger
      • 05 Redis
    • Spring
      • 01 Spring Boot
      • 02 Spring Validation
      • 03 Spring Data
      • 04 Spring 容器
      • 05 Spring AOP
      • 06 Spring Transactional 注解
      • 07 Spring Cloud Netflix
      • 08 Spring Cloud Alibaba
      • 09 Spring Security oAuth2
      • 10 Spring Boot 跨域解决方式
      • 11 Spring Boot 请求拦截
      • 12 Spring Boot 异步编程
      • 13 Spring Boot 定时任务
      • 14 Spring Boot 管理 bean
      • 15 Mybatis 逆向代码生成
      • 16 JWT
      • 17 JPA
      • 18 Apache Shiro
      • 19 Spring 异步请求
  • 书斋
    • ES6 标准入门
      • 01 变量声明与解构赋值
      • 02 语法的扩展
      • 03 数据类型与数据结构
      • 04 Proxy 和 Reflect
      • 05 异步编程 Promise
      • 06 Iterator 和 for of 循环
      • 07 Generator 函数
      • 08 Async 函数
      • 09 Class 类
    • JavaScript 设计模式
      • 01 基础知识
      • 02 设计模式(上)
      • 03 设计模式(下)
      • 04 设计原则和编程技巧
  • 纸函
    • 01 Interview
    • 02 Ceph
    • 03 动态规划
    • 04 Document.designMode
    • 2023-01-10
  • 万藏
    • 文档
      • 01 Git 文档
      • 02 Linux 命令大全
      • 03 七天学会 NodeJS
      • 04 Algorithms
    • 工具
      • 01 Nginx Config
      • 02 ProcessOn
      • 03 Flat Icon
      • 04 Regexper
      • 05 TempMail
      • 06 Carbon
由 GitBook 提供支持
在本页
  • 不一样の烟火
  • 大功告成

这有帮助吗?

  1. 時雨
  2. 2018

01 不一样の烟火

上一页2018下一页02 Python 之禅

最后更新于1年前

这有帮助吗?

是一个强大的 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.

不一样の烟火

在开始之前,先链上 和 。

引入 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 😃!

anime.js
Source Code
在线预览