实现粒子时钟特效

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>粒子时钟</title>
    <style>
      body {
        margin: 0;
        padding: 0;
      }
      canvas {
        background: radial-gradient(#fff, #8c738c);
        display: block;
        width: 100vw;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <canvas></canvas>
    <script src="./js/1.粒子时钟.js"></script>
  </body>
</html>

const canvas = document.querySelector("canvas");
// canvas.getContext() 是一个 HTML5 API,用于获取一个 canvas 元素的绘图环境(context)。
// 通过绘图环境,可以在 canvas 上进行绘制操作。
// canvas.getContext()参数:
// 2d:2D 绘图环境,用于绘制 2D 图形。
// webgl 或 experimental-webgl:WebGL 绘图环境,用于进行 3D 渲染。
// webgl2 或 experimental-webgl2:WebGL 2 绘图环境,用于进行高级的 3D 渲染。
const ctx = canvas.getContext("2d", {
  // 配置提升渲染效率
  willReadFrequently: true,
});

// 设置canvas的宽高  *dpr是为了保持清晰度
function initCanvasSize(params) {
  canvas.width = window.innerWidth * devicePixelRatio;
  canvas.height = window.innerHeight * devicePixelRatio;
}

initCanvasSize();

/**
 * 获取范围值内的随机整数
 * @param min  最小值范围
 * @param max  最大值范围
 */
function getRandom(min, max) {
  return Math.floor(Math.random() * (max + 1 - min) * min);
}

// 生成粒子类
class Particle {
  constructor() {
    //   大小(生成范围内的随机大小)
    this.size = getRandom(2 * devicePixelRatio, 6 * devicePixelRatio);
    //   半径(canvas的最小宽高的一半)
    const r = Math.min(canvas.width, canvas.height) / 2;
    //   角度(生成0~360度之间的随机角度)
    const rad = (getRandom(0, 360) * Math.PI) / 100;
    //   外层x
    const cx = canvas.width / 2;
    //   外层y
    const cy = canvas.height / 2;
    //   X坐标(三角函数计算)
    this.x = cx + r * Math.cos(rad);
    //   y坐标(三角函数计算)
    this.y = cy + r * Math.sin(rad);
  }

  // canvas画粒子函数
  draw() {
    // 开启路径
    ctx.beginPath();
    //   画圆
    //   参数this.x和this.y表示圆心的坐标,this.size表示圆的半径,
    //   0和2 * Math.PI表示起始角度和结束角度,这里的0表示从圆的正右侧开始绘制,
    //   2 * Math.PI表示绘制完整的圆形
    ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
    //   设置填充颜色
    ctx.fillStyle = "#5445544d";
    //   填充具体路径
    ctx.fill();
  }
  // 粒子移动函数(参数:新的x,y)
  moveTo(tx, ty) {
    // 定义运动时间
    const duration = 500;
    // 记录起始位置
    const sx = this.x,
      sy = this.y;
    // 记录运动时间
    const xSpeed = (tx - sx) / duration;
    const ySpeed = (ty - sy) / duration;
    // 开始时间
    const startTime = Date.now();
    // 具体移动计算函数(计算移动后位置)
    const _move = () => {
      // 计算运动耗时
      const t = Date.now() - startTime;
      // 计算在当前耗时内运动后的位置(起始位置+速度*时间)
      const x = sx + xSpeed * t;
      const y = sy + ySpeed * t;
      // 更新坐标
      this.x = x;
      this.y = y;
      // 运动耗时≥运动时间,停止运动,将运动后的x,y赋值
      if (t >= duration) {
        this.x = tx;
        this.y = ty;
        return;
      }

      // 注册下一次移动
      requestAnimationFrame(_move);
    };
    // 调用移动计算函数
    _move();
  }
}

// 全局方法(清空画布)
function clear() {
  // 清空画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// 全局方法(获取当前时间字符串)
function getTimeText() {
  return new Date().toTimeString().substring(0, 8);
}
// 全局方法(获取所有黑色像素点坐标)
function getPoints() {
  const points = [];
  // 获取像素信息,data是数组,是一个个像素点的rgba的值,四个一组
  const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
  // 像素间隙(像素点太多,没必要那么密集,性能不好)
  const gap = 5;
  for (let i = 0; i < canvas.width; i += gap) {
    for (let j = 0; j < canvas.height; j += gap) {
      const index = (i + j * canvas.width) * 4;
      const r = data[index];
      const g = data[index + 1];
      const b = data[index + 2];
      const a = data[index + 3];
      // 判断黑色像素
      if (r === 0 && g === 0 && b === 0 && a === 255) {
        points.push([i, j]);
      }
    }
  }
  return points;
}

// 全局方法(更新粒子)
function update() {
  //获取当前时间文字
  const curTimeText = getTimeText();
  // 比较差异,没变化就不做更新
  if (timeText === curTimeText) {
    return;
  }
  // 更新记录时间文字
  timeText = curTimeText;
  // 画时间文字
  const { width, height } = canvas;
  // 字体颜色
  ctx.fillStyle = "#000";
  // 纵向对齐方式
  ctx.textBaseline = "middle";
  // 字体
  ctx.font = `${140 * devicePixelRatio}px 'DS-Digital', sans-serif`;
  // 横向对齐方式
  ctx.textAlign = "center";
  ctx.fillText(timeText, width / 2, height / 2);

  const points = getPoints();
  // 清空画布上的黑色像素时间文字(使用粒子进行绘画)
  clear();
  // 如果需要的粒子数量比现在多
  for (let i = 0; i < points.length; i++) {
    const [x, y] = points[i];
    let p = particles[i];
    // 如果粒子没匹配上,代表初始没有或者粒子不够,生成粒子添加进粒子数组中
    if (!p) {
      p = new Particle();
      particles.push(p);
    }
    p.moveTo(x, y);
  }
  // 如果需要的粒子数量比现在少
  if (points.length < particles.length) {
    particles.splice(points.length);
  }
}
// 全局方法(重新画)
// 在每次绘制之前先清空画布,然后绘制路径的内容,并不断请求浏览器进行下一次的绘制,从而实现画布内容的动态更新
function draw() {
  // 清空画布
  clear();
  // 更新粒子
  update();
  // 画所有粒子
  for (const p of particles) {
    p.draw();
  }
  // 请求动画帧,进行下一次绘制
  requestAnimationFrame(draw);
}
//粒子数组
const particles = [];
const p = new Particle();
// 记录之前的时间文字
let timeText = null;
draw();

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值