🌧️ Canvas 还能这么玩?送你一场特别的数字雨!

368 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情

前言

看了这么多 CSS 实现的效果,想必大家也看累了,今天我们来玩玩 Canvas —— 使用 canvas 绘制数字雨效果。

Canvas 可能有的小伙伴不怎么了解,它提供了一个通过 Javascript 和 HTML 的 canvas 元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。一般用于 2D 元素。

思路分析

接下来我们一步一步的分析实现过程。

初始化样式

首先为了美观以及统一,我们需要先将样式初始化一下:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    background-color: #1d1f20;
    min-height: 100vh;
    overflow: hidden;
}

通配符 * 不建议大家在项目中使用哦,它是 效率很低 的一个操作符。我们可以使用 reset.css 或者 normalize.css 来统一我们的样式,使得各个浏览器展现效果一致。

这里不得不多提一嘴,咱们 码上掘金 的设置里,也是有这两个样式初始化的选项的,好!

微信截图_20221026150436.png

创建 Canvas 标签

咱们可以通过 canvas 标签来创建一个画布。整个 HTML 就这行了,嫌麻烦的小伙伴们有福了嗷。

<body>
    <canvas id="c"></canvas>
<body>

创建画布

有了 canvas 标签后,咱们可以通过 getContext 方法创建一个 context ,后续的绘制都是在 context 上进行的。

<script>
    const c = document.querySelector("#c"); // 获取 canvas 
    const ctx = c.getContext("2d"); // 获取这个元素的 context——图像稍后将在此被渲染。
</script>

需要注意的是,如果不显式设置大小的话,canvas 画布的宽高分别是 300px 和 150px,咱们要创建的画布是屏幕大小,也就是所谓的 视口大小 ,可以通过 window.innerHeightwindow.innerWidth 获取。

c.height = window.innerHeight;
c.width = window.innerWidth;

咱们看看效果:

微信截图_20221026151434.png

没啥问题,同视口大小。接下来我们要实现数字雨的效果啦,激不激动!

数字雨

行列值

由于我们的数字雨是由 01 随机排列 组成的,因此我们创建一个数组 words = ['0', '1']

同时为了将视口大小的画布都铺满雨,我们要计算列数,这个列数怎么计算呢?

我们仔细想想,一个字的大小是多少?浏览器默认是 12px,为了眼睛不那么容易看花,我们设置为 16px。那视口能够放的数字列数是不是就是 c.width / 20 了?

const words = ["0", "1"];
const fontSize = 16;
const columns = Math.floor(c.width / fontSize);

这里我们要注意使用 Math.floor 向下取整

随机抽取 01

我们的数字雨中,01 是随机排列的,因此我们要写一个函数,随机抽取 0 或 1。刚刚我们有个 words 数组存放了 01,我们只需要使用 Math.random 函数随机在 0 和 1 这两个值上就好了:

const getText = (words) => {
    return words[Math.floor(Math.random() * words.length)];
};

数字雨本雨

大家是不是以为数字雨是一列一列画的?不是的,其实它是一行一行的绘制的。

具体的,我们第一次绘制第一行,然后一定时间间隔后绘制第二行,一直绘制到行高溢出视口高度后重新开始。

首先我们设置一个行数组 drops = new Array(columns).fill(0)

接下来为了实现 每次刷新都能绘制一行 的效果,我们需要使用 setInterval 函数。

const draw = () => {
    ctx.fillStyle = "rgba(0,0,0,0.05)";
    ctx.fillRect(0, 0, c.width, c.height);
};
setInterval(draw, 120);

细心的小伙伴应该注意到了 draw 函数里的两行代码,那是为了每次绘制时,画的东西不重叠,所以每次绘制之前,都要将整个画布都清空。fillRect 函数将画布以一个矩形来清空。

我们看看如果不加这两行会是什么效果:

20221026_154126.gif

接下来我们理理思路。

刚刚我们提到,数字雨实际上是一行一行绘制的,那么怎么绘制呢?这里我们要用到 fillText 函数来绘制文字。它可以接收三个参数,分别是待绘制的数字、绘制的 x 轴的位置以及绘制的 y 轴的位置。待绘制的文字我们可以用刚刚的 getText 函数获取。

为了实现每次一行的效果,我们可以用一个 for 循环,待绘制文字的 x 轴位置就是第几列,可以用 当前待绘制文字的索引 * fontSize 得到,同理,y 轴就是 当前行数 * fontSize

一开始 drops 数组都是第 0 行,随着绘制刷新不断递增,直到超出屏幕时重新从第 0 行开始绘制。

const draw = () => {
    ...
    ctx.fillStyle = "red";
    ctx.font = `${fontSize}px arial`;
    for (let i = 0; i < drops.length; ++i) {
      ctx.fillText(getText(words), i * fontSize, drops[i] * fontSize);
      if (drops[i] * fontSize > c.height)
        drops[i] = 0;
      drops[i]++;
    }
};

其中 fillStyle 可以设置待绘制的颜色。而 ctx.font 可以设置待绘制文字的大小和字体。

我们看看此时的效果:

20221026_155452.gif

可以嘛,这雨整整齐齐的。为啥呢?

因为我们是一行一行绘制的,因此它们必然是同时溢出视口的,意味着它们也会同时从第 0 行开始。

所以我们要使一些绊子。

我们不仅是超出屏幕时重新从第 0 行开始,还要给个随机数,超过这个随机数也要重新开始!

for (let i = 0; i < drops.length; ++i) {
  ...
  if (drops[i] * fontSize > c.height && Math.random() > 0.95)
    drops[i] = 0;
}

我们看看此时的效果:

20221026_154236.gif

OK,漏趴笨。

码上掘金

Github 源码地址

关于本文的所有代码及素材,都已上传至 Github 仓库,小伙伴们有需要的自取:

juejin-demo/digit-rain-demo at main · catwatermelon/juejin-demo (github.com)

结束语

本文就到此结束了,希望大家阅读本文能所收获。

如果小伙伴们有别的想法,欢迎留言,让我们共同学习进步💪💪。

如果文中有不对的地方,或是大家有不同的见解,欢迎指出🙏🙏。

如果大家觉得所有收获,欢迎一键三连💕💕。