canvas實現炫酷的黑客帝國數字雨特效

超級索尼發表於2019-03-01

動機

最近重溫黑客帝國,發現這個數字雨特效很炫酷,之前也看到網路上有相關類似的程式碼,我先自己思考了一種實現方式,最後參考網上給出的一種思路,最後寫成了一個vue外掛放在npm上,下面先上特效gif

canvas實現炫酷的黑客帝國數字雨特效

是不是和電影裡的很像,不過還是有點差距

自己的實現方法(失敗)

對於這種較為複雜的動畫特效,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清除上次所畫的內容,每次都是疊加上次所畫的效果

canvas實現炫酷的黑客帝國數字雨特效
如上圖,雖然看著圖中有很多字母,其實requestAnimationFrame每次只畫了紅圈內的字母,也就是對應每列的字母,其餘顏色變淡的字母都是requestAnimationFrame以前畫出來的,只不過被新畫的黑色背景遮住了從而變暗,這樣就完美的實現了拖尾效果 (4)最開始時rainDropArray的每個值都是0,且所有列下落速度一樣,因此動畫剛開始是會是如下效果

canvas實現炫酷的黑客帝國數字雨特效
整整齊齊的下落,因此需要在字母到達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地址點這裡

相關文章