動機
最近重溫黑客帝國,發現這個數字雨特效很炫酷,之前也看到網路上有相關類似的程式碼,我先自己思考了一種實現方式,最後參考網上給出的一種思路,最後寫成了一個vue外掛放在npm上,下面先上特效gif
是不是和電影裡的很像,不過還是有點差距
自己的實現方法(失敗)
對於這種較為複雜的動畫特效,canvas是首選,當然css肯定也可以做,不過肯定超級複雜,程式碼量巨大。首先我第一眼看到這個特效,思路是這樣的:
(1) 一般canvas用於繪製靜態的影象,由於本例是動畫效果,肯定得呼叫setTimeout或者setInterval或者raf,這裡採用raf,不斷繪製影象達到動態的效果,而且應當採用raf,它優於setTimeout/setInterval的地方在於它是由瀏覽器專門為動畫提供的API,在執行時瀏覽器會自動優化方法的呼叫,並且如果頁面不是啟用狀態下的話,動畫會自動暫停,有效節省了CPU開銷
(2) 繪製一個黑色的背景
(3) 由於數字雨看上去是獨立的一條條的向下運動的數字字母序列,因此我需要新建一個DigitRain類,裡面設定了該數字雨的各種屬性,來控制該條數字雨的運動特性,程式碼見下面
//數字雨類(引數是配置物件)
function DigitRain(configObj){
//數字雨的位置(x軸)
this.digitRainXPos = configObj.digitRainXPos,
//數字雨的位置(y軸)
this.digitRainYPos = configObj.digitRainYPos,
//數字雨的下落速度
this.rainVelocity = configObj.rainVelocity,
//數字雨的顏色
this.rainColor = configObj.rainColor,
//數字雨的拖尾長度
this.rainTailLength = configObj.rainTailLength,
//數字雨的文字內容
this.rainText = configObj.rainText,
...
}
複製程式碼
(4)然後寫一個draw方法來控制其運動,最終在canvas裡面呼叫fillText來畫出文字
最終我寫了一會發現困難太多,特別是文字拖尾效果的處理很麻煩,而且達不到效果,於是便作罷
換一種思路
參考了網上的一種思路,這種思路可謂是化繁為簡,而且很容易理解,不得不佩服
(1) 同樣是採用raf實現動畫效果,首先根據canvas寬度和字型大小計算出雨滴下落的列數(寬度/字型大小),採用一個rainDropArray
(長度是列數)記錄下每個列的文字的y軸的位置,初始都為0,核心資料結構就是這個rainDropArray
(2) requestAnimationFrame
的引數函式裡,用for迴圈遍歷rainDropArray
,然後用fillText
向canvas畫上文字,x軸位置就是陣列的index*字型大小,y軸位置就是rainDropArray[i]
的值,而且每次fillText都用封裝的random方法獲取字串的隨機數字字母
(3)拖尾效果的處理:這裡很巧妙,對於拖尾效果,只需要在requestAnimationFrame
的引數函式裡fillRect(0,0,.canvas.width,canvas.height)
即可,而fillStyle
設定為rgba(0,0,0,alpha)
,這樣每次畫圖時都會畫這麼一個黑色背景,從而覆蓋了之前畫的字母,讓字母顏色變淡,達到拖尾效果,通過控制alpha的值的大小來控制拖尾的長短,注意畫圖時沒有用clearReact清除上次所畫的內容,每次都是疊加上次所畫的效果
requestAnimationFrame
每次只畫了紅圈內的字母,也就是對應每列的字母,其餘顏色變淡的字母都是requestAnimationFrame
以前畫出來的,只不過被新畫的黑色背景遮住了從而變暗,這樣就完美的實現了拖尾效果
(4)最開始時rainDropArray
的每個值都是0,且所有列下落速度一樣,因此動畫剛開始是會是如下效果
整整齊齊的下落,因此需要在字母到達canvas底部時做處理,讓其有先後順序,程式碼如下,觸底後給定一定概率讓其的y軸位置重新置位0,從而達到該列迴圈下落的效果
if(textYPostion>this.canvasHeight){
if(Math.random()>0.9){
this.rainDropPositionArray[i]=0;
}
}
複製程式碼
vue外掛封裝後的程式碼
總體程式碼量不多,不到150行,template部分就一個canvas
<template>
<canvas id="vue-matrix-raindrop"></canvas>
</template>
<script>
export default {
name: 'vue-matrix-raindrop',
//外掛的各種引數
props:{
//canvas寬度
canvasWidth:{
type:Number,
default:800
},
//canvas高度
canvasHeight:{
type:Number,
default:600
},
//下落字型大小
fontSize:{
type:Number,
default:20
},
//字型型別
fontFamily:{
type:String,
default:'arial'
},
//字型文字內容,會隨機從字串裡取一個
textContent:{
type:String,
default:'abcdefghijklmnopqrstuvwxyz'
},
//字型顏色
textColor:{
type:String,
default:'#0F0',
validator:function(value){
var colorReg = /^#([0-9a-fA-F]{6})|([0-9a-fA-F]{3})$/g
return colorReg.test(value)
}
},
//canvas背景顏色,可自定義
backgroundColor:{
type:String,
default:'rgba(0,0,0,0.1)',
validator:function(value){
var reg = /^[rR][gG][Bb][Aa][\(]((2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),){2}(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?),?(0\.\d{1,2}|1|0)?[\)]{1}$/;
return reg.test(value);
}
},
//下落速度
speed:{
type:Number,
default:2,
validator:function(value){
return value%1 === 0;
}
}
},
mounted:function(){
this.initRAF();
this.initCanvas();
this.initRainDrop();
this.animationUpdate();
},
methods:{
//初始化requestAnitaionFrame,注意相容性
initRAF(){
window.requestAnimationFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
window.cancelAnimationFrame = (function () {
return window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
function (id) {
window.clearTimeout(id);
};
})();
},
//初始化canvas
initCanvas(){
this.canvas = document.getElementById('vue-matrix-raindrop');
//需要判斷獲取到的canvas是否是真的canvas
if(this.canvas.tagName.toLowerCase() !== 'canvas'){
console.error("Error! Invalid canvas! Please check the canvas's id!")
}
this.canvas.width = this.canvasWidth;
this.canvas.height = this.canvasHeight;
this.canvasCtx = this.canvas.getContext('2d');
this.canvasCtx.font = this.fontSize+'px '+this.fontFamily;
this.columns = this.canvas.width / this.fontSize;
},
//初始化數字雨下落的初始y軸位置
initRainDrop(){
for(var i=0;i<this.columns;i++){
this.rainDropPositionArray.push(0);
}
},
//核心動畫函式,控制數字雨下落
animationUpdate(){
//控制雨滴下落的速度
this.speedCnt++;
//speed為1最快,越大越慢
if(this.speedCnt===this.speed){
this.speedCnt = 0;
//繪製背景
this.canvasCtx.fillStyle=this.backgroundColor;
this.canvasCtx.fillRect(0,0,this.canvas.width,this.canvas.height);
//繪製文字
this.canvasCtx.fillStyle=this.textColor;
//遍歷每一列的數字雨,然後在canvas上繪製該數字字母
for(var i=0,len=this.rainDropPositionArray.length;i<len;i++){
this.rainDropPositionArray[i]++;
var randomTextIndex = Math.floor(Math.random()*this.textContent.length);
var randomText = this.textContent[randomTextIndex];
var textYPostion = this.rainDropPositionArray[i]*this.fontSize;
this.canvasCtx.fillText(randomText,i*this.fontSize,textYPostion);
//數字雨觸碰canvas底部則一定概率重新回到頂部繼續下落
if(textYPostion>this.canvasHeight){
if(Math.random()>0.9){
this.rainDropPositionArray[i]=0;
}
}
}
}
window.requestAnimationFrame(this.animationUpdate)
}
},
data () {
return {
canvasCtx:null,
canvas:null,
columns:0,
rainDropPositionArray:[],
speedCnt:0
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
複製程式碼
github地址點這裡