动机
最近重温黑客帝国,发现这个数字雨特效很炫酷,之前也看到网络上有相关类似的代码,我先自己思考了一种实现方式,最后参考网上给出的一种思路,最后写成了一个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地址点这里
所有评论(0)