满月中秋,流星夜空,要不要来许愿呢?

918 阅读4分钟

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

前言

大家好,我是追梦玩家。中秋即将到来,提前祝大家中秋快乐啊!!!19年,我写了一篇文章手把手教你实现一个canvas智绘画板,为什么要说这件事呢?没错,又要使用 canvas 来实现满月中秋,流星夜空。废话少说,赶紧开始吧。

效果展示

线上预览地址

源码地址

具体效果就是这样,其实就是这几个内容:月亮、星星、流星。

流星夜空.gif

canvas 是什么,能做什么?

mdn 文档:

<canvas> 是一个可以使用脚本(通常为JavaScript)来绘制图形的 HTML 元素.例如,它可以用于绘制图表、制作图片构图或者制作简单的(以及不那么简单的)动画

简单来说,Canvas 是 HTML5 新增的组件,它就像一块幕布,可以用 JavaScript 在上面绘制各种图表、动画等。

使用面向对象编程实现效果

书写 HTML

处理逻辑,是放在单独的 js 文件:main.js

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>明月当空,共赏花好月圆夜</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    canvas {
      position: fixed;
      top: 0;
      left: 0;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  <script src="./main.js"></script>
</body>

</html>

初始化

let canvas = document.getElementById('canvas'),
  context = canvas.getContext('2d'),
  width = window.innerWidth,
  height = window.innerHeight,
  count = 0,
  meteors = []; // 流星集合

canvas.width = width;
canvas.height = height;

月亮类

主要使用到 canvas 的 createRadialGradient 方法,实现径向渐变效果。

class Moon {
  constructor(context, width, height) {
    this.context = context;
    this.width = width;
    this.height = height;
    this.circle_x = width / 2; // 旋转轨迹圆心的 X 坐标
    this.circle_y = width; // 旋转轨迹圆心的 Y 坐标
    this.circle_r = width; // 旋转轨迹的半径
    this.angle = Math.atan2(Math.sqrt(width * width * 3 / 4), -width / 2); // 旋转轨迹的角度
    this.startAngle = Math.atan2(Math.sqrt(width * width * 3 / 4), -width / 2 - 400); // 开始旋转的角度
    this.endAngle = Math.atan2(Math.sqrt(width * width * 3 / 4), width / 2 + 200); // 结束旋转的角度
    this.x = 0; // 月亮的 X 坐标
    this.y = 0; // 月亮的 Y 坐标
  }

  draw() {
    const { context, x, y, width, height } = this;
    
    // createRadialGradient 实现径向渐变
    const gradient = context.createRadialGradient(x, y, 50, x, y, 600);
    gradient.addColorStop(0, 'rgb(255, 255, 255)');
    gradient.addColorStop(0.01, 'rgb(70, 80, 80)');
    gradient.addColorStop(0.2, 'rgb(40, 40, 50)');
    gradient.addColorStop(0.4, 'rgb(20, 20, 30)');
    gradient.addColorStop(1, '#080d23');

    // save 方法:将当前状态放入栈中,保存 canvas 全部状态的方法
    context.save();
    context.fillStyle = gradient; // 填充
    context.fillRect(0, 0, width, height); // 绘制一个填充了内容的矩形
    context.restore(); // 将 canvas 恢复到最近的保存状态
  }
  
  // 让月亮动起来
  move() {
    const { circle_x, circle_y, circle_r, angle, startAngle, endAngle } = this;
    this.angle = angle - 0.0001;

    if (this.angle <= endAngle) {
      this.angle = startAngle;
    }

    this.x = circle_x + (circle_r * Math.cos(angle));
    this.y = circle_y - (circle_r * Math.sin(angle)) + 50;
  }

}

星星类

通过 getStars 方法生成一个星星的集合,所有的星星都保存在实例的 stars 中,闪烁效果,是通过调用 blink 函数就可以实现,主要是随机改变每个星星的半径大小。

class Stars {
  constructor(context, width, height, amount) {
    this.context = context;
    this.width = width;
    this.height = height;
    // 通过方法去生成星星集合
    this.stars = this.getStars(amount);
  }

  // 获取指定数量的星星
  getStars(amount) {
    let stars = [];
    while (amount--) {
      stars.push({
        x: Math.random() * this.width,
        y: Math.random() * this.height,
        r: Math.random() + 0.5,
      })
    }
    return stars;
  }
  
  // 描绘
  draw() {
    const { context } = this;
    context.save();
    context.fillStyle = 'white';
    this.stars.forEach(star => {
      context.beginPath();
      context.arc(star.x, star.y, star.r, 0, 2 * Math.PI);
      context.fill();
    })
    context.restore();
  }

  // 闪烁,让星星半径随机变大或变小,实现一闪一闪亮晶晶的效果
  blink() {
    this.stars = this.stars.map(star => {
      const sign = Math.random() > 0.5 ? 1 : -1;
      star.r += sign * 0.2;
      if (star.r < 0) {
        star.r = -star.r;
      } else if (star.r > 1) {
        star.r -= 0.2;
      }
      return star;
    })
  }
}

流星类

flow 方法,判断当前流星是否出界,所谓出界,就是离开视野之内,就销毁流星实例,回收内存。

流星是怎么实现的呢?

流星其实有流星头和流星尾,流星头是一个半圆组成,流星尾是一个三角形组成,然后整体倾角45度,并且填充时用上一个径向渐变,实现好看的流星效果。

class Meteor {
  constructor(context, x, h) {
    this.context = context;
    this.x = x;
    this.y = 0;
    this.h = h;
    this.vx = -(5 + Math.random() * 5);
    this.vy = -this.vx;
    this.len = Math.random() * 300 + 100;
  }

  flow() {
    // 判定流星出界
    if (this.x < -this.len || this.y > this.h + this.len) {
      return false;
    }

    this.x += this.vx;
    this.y += this.vy;
    return true;
  }

  draw() {
    const { context } = this;
    // 径向渐变,从流星头尾圆心,半径越大,透明度越高
    let gradient = context.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.len);

    const PI = Math.PI;
    gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
    gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');

    context.save();
    context.fillStyle = gradient;
    context.beginPath();
    // 流星头,二分之一圆
    context.arc(this.x, this.y, 0.5, PI / 4, 5 * PI / 4);
    // 绘制流星尾,三角形
    context.lineTo(this.x + this.len, this.y - this.len);
    context.closePath();
    context.fill();
    context.restore();
  }
}

// 生成流星
const meteorGenerator = () => {
  const x = Math.random() * width;
  meteors.push(new Meteor(context, x, height));
}

每一帧动画生成函数

所有的动画都是通过帧来实现的。 调用 frame 方法,就可以生成每一帧动画。

// 实例化星星和月亮
let moon = new Moon(context, width, height),
  stars = new Stars(context, width, height, 200);
  
// 每一帧动画生成函数
const frame = () => {
  count++;
  // 每隔 10 帧星星闪烁一次,节省计算资源
  count % 10 == 0 && stars.blink();
  // 每隔 300 帧,会有流星划过夜空
  count % 300 == 0 && meteorGenerator();

  moon.move();
  moon.draw();
  stars.draw();

  meteors.forEach((meteor, index, arr) => {
    // 如果流星离开视野之内,销毁流星实例,回收内存
    if (meteor.flow()) {
      meteor.draw();
    } else {
      arr.splice(index, 1);
    }
  })

  requestAnimationFrame(frame);
}

frame();

canvas 使用到的属性和方法汇总

参考

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。