在看這篇文章時,里面有個動畫的示例(如上圖),然后感覺有點很酷炫,就打算了解一下怎么寫的笛坦。( 先上代碼示例鏈接)
前景提要
需要先確保你還記得三角函數(shù)的知識。
對 Canvas 的 API 有點了解苔巨,且稍微了解其中的 globalCompositeOperation API (不了解的話弯屈,可以看下這個文章,基本可以有點感覺)恋拷。
代碼分析
盡我所能,我盡量在代碼里關(guān)鍵地方都增加了注釋厅缺。(可能有些描述表達(dá)不夠好蔬顾,請見諒~)
var w = (c.width = window.innerWidth),
h = (c.height = window.innerHeight),
ctx = c.getContext("2d"),
//一些配置項
opts = {
len: 20, //線長
count: 50, //線總數(shù)
baseTime: 10, //線停留基礎(chǔ)時間
addedTime: 10, //線額外停留時間
dieChance: 0.05, //線重置的概率
spawnChance: 1, //線生成的概率
sparkChance: 0.1, //火花生成的概率
sparkDist: 10, //火花距離線的距離
sparkSize: 2, //火花大小
color: "hsl(hue,100%,light%)", //hsl() 函數(shù)使用色相宴偿、飽和度、亮度來定義顏色诀豁。
baseLight: 50, //基礎(chǔ)的顏色亮度
addedLight: 10, // [50-10,50+10]
shadowToTimePropMult: 6, //陰影的模糊級別
baseLightInputMultiplier: 0.01, //基礎(chǔ)亮度
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() {
//瀏覽器下次重繪前調(diào)用該方法
window.requestAnimationFrame(loop);
//循環(huán)過程中更改生成的霓虹燈顏色色相
++tick;
/* 目標(biāo)圖像 = 已經(jīng)放置在畫布上的繪圖窄刘。
源圖像 = 打算放置到畫布上的繪圖。 */
ctx.globalCompositeOperation = "source-over"; //目標(biāo)圖像上顯示源圖像
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(0,0,0,alp)".replace("alp", opts.repaintAlpha);
ctx.fillRect(0, 0, w, h);
ctx.globalCompositeOperation = "lighter"; //顯示源圖像 + 目標(biāo)圖像(重疊圖形的顏色是通過顏色值相加來確定)
//保持生成的霓虹燈線共有 count 個
if (lines.length < opts.count && Math.random() < opts.spawnChance)
lines.push(new Line());
lines.map(function (line) {
line.step();
});
}
function Line() {
//生成霓虹燈線時進(jìn)行初始化
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();
};
//霓虹燈線每一步的開始前規(guī)劃階段
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;
//超過行走時間娩践,規(guī)劃下一步
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();
//監(jiān)聽瀏覽器窗口調(diào)整,重置
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;
});
簡單來描述下,上面的主要代碼:
- 每一次瀏覽器重繪前都調(diào)用 loop() 函數(shù)烹骨。
- 在 loop() 函數(shù)里翻伺,保持共有 count 個實例化的 Line 。
- 在實例化時沮焕,調(diào)用 reset() 函數(shù)進(jìn)行一些屬性的初始化吨岭。
- 在初始化完成后,調(diào)用 beginPhase() 函數(shù)進(jìn)行下一步繪制的路線規(guī)劃峦树。(其中霓虹燈線觸發(fā)重置條件時辣辫,調(diào)用 reset() 函數(shù),進(jìn)行屬性的數(shù)值化)
- 回到 loop() 函數(shù)魁巩,遍歷每一個示例 Line 急灭,調(diào)用 step() 函數(shù),進(jìn)行繪制霓虹燈線和線周圍的火花谷遂。(其中超過每一步規(guī)定的停留時間后葬馋,調(diào)用 beginPhase() 函數(shù),規(guī)劃下一步埋凯。)
問題
這個動畫点楼,讓我一開始感覺到厲害的地方是,霓虹燈線行走的尾部白对,有個漸漸的變暗淡的過程掠廓。所以,讓人感覺這個動畫甩恼,就很酷炫蟀瞧。
而這個是怎么做的呢?我上面描述刻意沒有講到条摸≡梦郏可以看下代碼,思考下钉蒲,思路感覺挺微妙的切端。(我是重新看了下代碼才明白的)
答案
關(guān)鍵點就在于 loop() 函數(shù)里的這兩行代碼。
ctx.fillStyle = "rgba(0,0,0,alp)".replace("alp", opts.repaintAlpha);
ctx.fillRect(0, 0, w, h);
通過每一次的層層疊加上有一定透明度的黑色顷啼,從而達(dá)到了后面尾巴逐漸消滅的效果踏枣。(如果你開始一眼就發(fā)現(xiàn)了昌屉,打擾了,獻(xiàn)丑了)
最后
酷炫的 Canvas 從來沒有寫過茵瀑,也沒接觸過间驮。這次試著分析這個酷炫動畫代碼,算是對如何用 Canvas 畫動畫有了點感覺了吧马昨。
另外竞帽,雖然看懂了代碼,但似乎不是知道 Canvas 怎么畫動畫就能寫出這個效果的鸿捧,總感覺里面似乎蘊涵了一些數(shù)學(xué)功底~