在看这篇文章时,里面有个动画的示例(如上图),然后感觉有点很酷炫,就打算了解一下怎么写的。( 先上代码示例链接)
前景提要
需要先确保你还记得三角函数的知识。
对 Canvas 的 API 有点了解,且稍微了解其中的 globalCompositeOperation API (不了解的话,可以看下这个文章,基本可以有点感觉)。
代码分析
尽我所能,我尽量在代码里关键地方都增加了注释。(可能有些描述表达不够好,请见谅~)
var w = (c.width = window.innerWidth),
h = (c.height = window.innerHeight),
ctx = c.getContext("2d"),
//一些配置项
opts = {
len: 20, //线长
count: 50, //线总数
baseTime: 10, //线停留基础时间
addedTime: 10, //线额外停留时间
dieChance: 0.05, //线重置的概率
spawnChance: 1, //线生成的概率
sparkChance: 0.1, //火花生成的概率
sparkDist: 10, //火花距离线的距离
sparkSize: 2, //火花大小
color: "hsl(hue,100%,light%)", //hsl() 函数使用色相、饱和度、亮度来定义颜色。
baseLight: 50, //基础的颜色亮度
addedLight: 10, // [50-10,50+10]
shadowToTimePropMult: 6, //阴影的模糊级别
baseLightInputMultiplier: 0.01, //基础亮度
addedLightInputMultiplier: 0.02, //额外亮度
cx: w / 2,
cy: h / 2,
repaintAlpha: 0.04,
hueChange: 0.1,
},
tick = 0, //控制颜色色相
lines = [],
dieX = w / 2 / opts.len,
dieY = h / 2 / opts.len,
baseRad = (Math.PI * 2) / 6;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, w, h);
function loop() {
//浏览器下次重绘前调用该方法
window.requestAnimationFrame(loop);
//循环过程中更改生成的霓虹灯颜色色相
++tick;
/* 目标图像 = 已经放置在画布上的绘图。
源图像 = 打算放置到画布上的绘图。 */
ctx.globalCompositeOperation = "source-over"; //目标图像上显示源图像
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(0,0,0,alp)".replace("alp", opts.repaintAlpha);
ctx.fillRect(0, 0, w, h);
ctx.globalCompositeOperation = "lighter"; //显示源图像 + 目标图像(重叠图形的颜色是通过颜色值相加来确定)
//保持生成的霓虹灯线共有 count 个
if (lines.length < opts.count && Math.random() < opts.spawnChance)
lines.push(new Line());
lines.map(function (line) {
line.step();
});
}
function Line() {
//生成霓虹灯线时进行初始化
this.reset();
}
//初始化,重置
Line.prototype.reset = function () {
this.x = 0;
this.y = 0;
this.addedX = 0;
this.addedY = 0;
this.rad = 0;
//亮度
this.lightInputMultiplier =
opts.baseLightInputMultiplier +
opts.addedLightInputMultiplier * Math.random();
this.color = opts.color.replace("hue", tick * opts.hueChange);
this.cumulativeTime = 0; //累计的时间
this.beginPhase();
};
//霓虹灯线每一步的开始前规划阶段
Line.prototype.beginPhase = function () {
this.x += this.addedX;
this.y += this.addedY;
this.time = 0;
//霓虹灯线每一步的停留时间
this.targetTime = (opts.baseTime + opts.addedTime * Math.random()) | 0;
//随机六边形路线方向
this.rad += baseRad * (Math.random() < 0.5 ? 1 : -1);
this.addedX = Math.cos(this.rad);
this.addedY = Math.sin(this.rad);
//霓虹灯线消失重置的条件
if (
Math.random() < opts.dieChance ||
this.x > dieX ||
this.x < -dieX ||
this.y > dieY ||
this.y < -dieY
)
this.reset();
};
//行走一步
Line.prototype.step = function () {
++this.time;
++this.cumulativeTime;
//超过行走时间,规划下一步
if (this.time >= this.targetTime) this.beginPhase();
var prop = this.time / this.targetTime,
wave = Math.sin((prop * Math.PI) / 2), //sin90°=1
x = this.addedX * wave, //cos(R)=b/c
y = this.addedY * wave; //sin(R)=a/c
ctx.shadowBlur = prop * opts.shadowToTimePropMult; //阴影的模糊级别
//模糊和填充的颜色
ctx.fillStyle = ctx.shadowColor = this.color.replace(
"light",
opts.baseLight +
opts.addedLight *
Math.sin(this.cumulativeTime * this.lightInputMultiplier)
);
//绘制霓虹灯线
ctx.fillRect(
opts.cx + (this.x + x) * opts.len,
opts.cy + (this.y + y) * opts.len,
2,
2
);
//随机生成火花
if (Math.random() < opts.sparkChance)
ctx.fillRect(
opts.cx +
(this.x + x) * opts.len +
Math.random() * opts.sparkDist * (Math.random() < 0.5 ? 1 : -1) -
opts.sparkSize / 2,
opts.cy +
(this.y + y) * opts.len +
Math.random() * opts.sparkDist * (Math.random() < 0.5 ? 1 : -1) -
opts.sparkSize / 2,
opts.sparkSize,
opts.sparkSize
);
};
loop();
//监听浏览器窗口调整,重置
window.addEventListener("resize", function () {
w = c.width = window.innerWidth;
h = c.height = window.innerHeight;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, w, h);
opts.cx = w / 2;
opts.cy = h / 2;
dieX = w / 2 / opts.len;
dieY = h / 2 / opts.len;
});
简单来描述下,上面的主要代码:
- 每一次浏览器重绘前都调用 loop() 函数。
- 在 loop() 函数里,保持共有 count 个实例化的 Line 。
- 在实例化时,调用 reset() 函数进行一些属性的初始化。
- 在初始化完成后,调用 beginPhase() 函数进行下一步绘制的路线规划。(其中霓虹灯线触发重置条件时,调用 reset() 函数,进行属性的数值化)
- 回到 loop() 函数,遍历每一个示例 Line ,调用 step() 函数,进行绘制霓虹灯线和线周围的火花。(其中超过每一步规定的停留时间后,调用 beginPhase() 函数,规划下一步。)
问题
这个动画,让我一开始感觉到厉害的地方是,霓虹灯线行走的尾部,有个渐渐的变暗淡的过程。所以,让人感觉这个动画,就很酷炫。
而这个是怎么做的呢?我上面描述刻意没有讲到。可以看下代码,思考下,思路感觉挺微妙的。(我是重新看了下代码才明白的)
答案
关键点就在于 loop() 函数里的这两行代码。
ctx.fillStyle = "rgba(0,0,0,alp)".replace("alp", opts.repaintAlpha);
ctx.fillRect(0, 0, w, h);
通过每一次的层层叠加上有一定透明度的黑色,从而达到了后面尾巴逐渐消灭的效果。(如果你开始一眼就发现了,打扰了,献丑了)
最后
酷炫的 Canvas 从来没有写过,也没接触过。这次试着分析这个酷炫动画代码,算是对如何用 Canvas 画动画有了点感觉了吧。
另外,虽然看懂了代码,但似乎不是知道 Canvas 怎么画动画就能写出这个效果的,总感觉里面似乎蕴涵了一些数学功底~