相信大家都对黑客帝国电影里的矩阵雨印象非常深刻,就是下面这个效果。 效果非常酷炫,我看了一下相关实现库的代码,也非常简单,核心就是用好命令行的控制字符,这里分享一下。 在matrixrain的源代码中,总共只有两个文件,ansi。js和index。js,非常小巧。控制字符和控制序列 ansi。js中定义了一些命令行的操作方法,也就是对控制字符做了一些方法封装,代码如下:constctlEsc〔;constansi{reset:(){ctlEsc}c,clearScreen:(){ctlEsc}2J,cursorHome:(){ctlEsc}H,cursorPos:(row,col){ctlEsc}{row};{col}H,cursorVisible:(){ctlEsc}?25h,cursorInvisible:(){ctlEsc}?25l,useAltBuffer:(){ctlEsc}?47h,useNormalBuffer:(){ctlEsc}?47l,underline:(){ctlEsc}4m,off:(){ctlEsc}0m,bold:(){ctlEsc}1m,color:c{ctlEsc}{c};1m,colors:{fgRgb:(r,g,b){ctlEsc}38;2;{r};{g};{b}m,bgRgb:(r,g,b){ctlEsc}48;2;{r};{g};{b}m,fgBlack:()ansi。color(30),fgRed:()ansi。color(31),fgGreen:()ansi。color(32),fgYellow:()ansi。color(33),fgBlue:()ansi。color(34),fgMagenta:()ansi。color(35),fgCyan:()ansi。color(36),fgWhite:()ansi。color(37),bgBlack:()ansi。color(40),bgRed:()ansi。color(41),bgGreen:()ansi。color(42),bgYellow:()ansi。color(43),bgBlue:()ansi。color(44),bgMagenta:()ansi。color(45),bgCyan:()ansi。color(46),bgWhite:()ansi。color(47),},};module。exportsansi; 这里面ansi对象上的每一个方法不做过多解释了。我们看到,每个方法都是返回一个奇怪的字符串,通过这些字符串可以改变命令行的显示效果。 这些字符串其实是一个个控制字符组成的控制序列。那什么是控制字符呢?我们应该都知道ASC字符集,这个字符集里面除了定义了一些可见字符以外,还有很多不可见的字符,就是控制字符。这些控制字符可以控制打印机、命令行等设备的显示和动作。 有两个控制字符集,分别是CO字符集和C1字符集。C0字符集是0x00到0x1F这两个十六进制数范围内的字符,而C1字符集是0x80到0x9F这两个十六进制数范围内的字符。C0和C1字符集内的字符和对应的功能可以在这里查到,我们不做详细描述了。 上面代码中,〔其实是一个组合,定义了ESC键,后跟〔表示这是一个控制序列导入器(ControlSequenceIntroducer,CSI)。在〔后面的所有字符都会被命令行解析为控制字符。 常用的控制序列有这些: 序列 功能 CSInA 向上移动n(默认为1)个单元 CSInA 向下移动n(默认为1)个单元 CSInC 向前移动n(默认为1)个单元 CSInD 向后移动n(默认为1)个单元 CSInE 将光标移动到n(默认为1)行的下一行行首 CSInF 将光标移动到n(默认为1)行的前一行行首 CSInG 将光标移动到当前行的第n(默认为1)列 CSIn;mH 移动光标到指定位置,第n行,第m列。n和m默认为1,即CSI;5H与CSI1;5H等同。 CSInJ 清空屏幕。如果n为0(或不指定),则从光标位置开始清空到屏幕末尾;如果n为1,则从光标位置清空到屏幕开头;如果n为2,则清空整个屏幕;如果n为3,则不仅清空整个屏幕,同时还清空滚动缓存。 CSInK 清空行,如果n为0(或不指定),则从光标位置清空到行尾;如果n为1,则从光标位置清空到行头;如果n为2,则清空整行,光标位置不变。 CSInS 向上滚动n(默认为1)行 CSInT 向下滚动n(默认为1)行 CSIn;mf 与CSIn;mH功能相同 CSInm 设置显示效果,如CSI1m表示设置粗体,CSI4m为添加下划线。 我们可以通过CSInm控制序列来控制显示效果,在设置一种显示以后,后续字符都会沿用这种效果,直到我们改变了显示效果。可以通过CSI0m来清楚显示效果。常见的显示效果可以在SGR(SelectGraphicRendition)parameters查到,这里受篇幅限制就不做赘述了。 上面的代码中,还定义了一些颜色,我们看到颜色的定义都是一些数字,其实每一个数字都对应一种颜色,这里列一下常见的颜色。 前景色 背景色 名称 前景色 背景色 名称 30hr40hr黑色 90hr100hr亮黑色 31hr41hr红色 91hr101hr亮红色 32hr42hr绿色 92hr102hr亮绿色 33hr43hr黄色 93hr103hr亮黄色 34hr44hr蓝色 94hr104hr亮蓝色 35hr45hr品红色(Magenta) 95hr105hr亮品红色(Magenta) 36hr46hr青色(Cyan) 96hr106hr亮青色(Cyan) 37hr47hr白色 97hr107hr亮白色 上面的代码中,使用了CSIn;1m的形式来定义颜色,其实是两种效果的,一个是具体颜色值,一个是加粗,一些命令行实现中会使用加粗效果来定义亮色。比如,如果直接定义CSI32m可能最终展示的是暗绿色,我们改成CSI32;1m则将显示亮绿色。 颜色支持多种格式,上面的是3bit和4bit格式,同时还有8bit和24bit。代码中也有使用样例,这里不再赘述了。矩阵渲染 在matrixrain的代码中,index。js里的核心功能是MatrixRain这个类:classMatrixRain{constructor(opts){this。transposeopts。directionh;this。coloropts。color;this。charRangeopts。charRange;this。maxSpeed20;this。colDroplets〔〕;this。numCols0;this。numRows0;handlereadingfromfileif(opts。filePath){if(!fs。existsSync(opts。filePath)){thrownewError({opts。filePath}doesntexist);}this。fileCharsfs。readFileSync(opts。filePath,utf8)。trim()。split();this。filePos0;this。charRangefile;}}generateChars(len,charRange){bydefaultcharRangeasciiletcharsnewArray(len);if(charRangeascii){for(leti0;ilen;i){chars〔i〕String。fromCharCode(rand(0x21,0x7E));}}elseif(charRangebraille){for(leti0;ilen;i){chars〔i〕String。fromCharCode(rand(0x2840,0x28ff));}}elseif(charRangekatakana){for(leti0;ilen;i){chars〔i〕String。fromCharCode(rand(0x30a0,0x30ff));}}elseif(charRangeemoji){emojisaretwocharacterwidths,souseaprefixconstemojiPrefixString。fromCharCode(0xd83d);for(leti0;ilen;i){chars〔i〕emojiPrefixString。fromCharCode(rand(0xde01,0xde4a));}}elseif(charRangefile){for(leti0;ilen;i,this。filePos){this。filePosthis。filePosthis。fileChars。length?this。filePos:0;chars〔i〕this。fileChars〔this。filePos〕;}}returnchars;}makeDroplet(col){return{col,alive:0,curRow:rand(0,this。numRows),height:rand(this。numRows2,this。numRows),speed:rand(1,this。maxSpeed),chars:this。generateChars(this。numRows,this。charRange),};}resizeDroplets(){〔this。numCols,this。numRows〕process。stdout。getWindowSize();transposefordirectionif(this。transpose){〔this。numCols,this。numRows〕〔this。numRows,this。numCols〕;}Createdropletspercolumnaddremovedropletstomatchcolumnsizeif(this。numColsthis。colDroplets。length){for(letcolthis。colDroplets。length;colthis。numCols;col){maketwodropletsperrowthatstartinrandompositionsthis。colDroplets。push(〔this。makeDroplet(col),this。makeDroplet(col)〕);}}else{this。colDroplets。splice(this。numCols,this。colDroplets。lengththis。numCols);}}writeAt(row,col,str,color){Onlyoutputifinviewportif(row0rowthis。numRowscol0colthis。numCols){constposthis。transpose?ansi。cursorPos(col,row):ansi。cursorPos(row,col);write({pos}{color}{str});}}renderFrame(){constansiColoransi。colors〔fg{this。color。charAt(0)。toUpperCase()}{this。color。substr(1)}〕();for(constdropletsofthis。colDroplets){for(constdropletofdroplets){const{curRow,col:curCol,height}droplet;droplet。alive;if(droplet。alivedroplet。speed0){this。writeAt(curRow1,curCol,droplet。chars〔curRow1〕,ansiColor);this。writeAt(curRow,curCol,droplet。chars〔curRow〕,ansi。colors。fgWhite());this。writeAt(curRowheight,curCol,);droplet。curRow;}if(curRowheightthis。numRows){resetdropletObject。assign(droplet,this。makeDroplet(droplet。col),{curRow:0});}}}flush();}} 还有几个工具方法:SimplestringstreambufferstdoutflushatonceletoutBuffer〔〕;functionwrite(chars){returnoutBuffer。push(chars);}functionflush(){process。stdout。write(outBuffer。join());returnoutBuffer〔〕;}functionrand(start,end){returnstartMath。floor(Math。random()(endstart));} matrixrain的启动代码如下:constargsargParser。parseArgs();constmatrixRainnewMatrixRain(args);functionstart(){if(!process。stdout。isTTY){console。error(Error:Outputisnotatextterminal);process。exit(1);}clearterminalandusealtbufferprocess。stdin。setRawMode(true);write(ansi。useAltBuffer());write(ansi。cursorInvisible());write(ansi。colors。bgBlack());write(ansi。colors。fgBlack());write(ansi。clearScreen());flush();matrixRain。resizeDroplets();}functionstop(){write(ansi。cursorVisible());write(ansi。clearScreen());write(ansi。cursorHome());write(ansi。useNormalBuffer());flush();process。exit();}process。on(SIGINT,()stop());process。stdin。on(data,()stop());process。stdout。on(resize,()matrixRain。resizeDroplets());setInterval(()matrixRain。renderFrame(),16);60FPSstart(); 首先初始化一个MatrixRain类,然后调用start方法。start方法中通过MatrixRain的resizeDroplets方法来初始化要显示的内容。 MatrixRain类实例中管理着一个colDroplets数组,保存这每一列的雨滴。在resizeDroplets中我们可以看到,每一列有两个雨滴。 在启动代码中我们还可以看到,每隔16毫秒会调用一次renderFrame方法来绘制页面。而renderFrame方法中,会遍历每一个colDroplet中的每一个雨滴。由于每一个雨滴的初始位置和速度都是随机的,通过droplet。alive和droplet。speed的比值来确定每一次渲染的时候是否更新这个雨滴位置,从而达到每个雨滴的下落参差不齐的效果。当雨滴已经移出屏幕可视范围后会被重置。 每一次渲染,都是通过write函数向全局的缓存中写入数据,之后通过flush函数一把更新。