Node.js 控制臺(tái)動(dòng)畫,繪制跨年祝福
今天是 2021 年的最后一天了,明天就是 2022 年?;仡欉^去一年,要特別感謝大家對(duì)我的支持。
人生不過幾十年,每一年都值得紀(jì)念和祝福,所以我想用 Node.js 控制臺(tái)動(dòng)畫送上一份我的新年祝福:
2021 年的最后一天,我們來(lái)學(xué)點(diǎn) cli 的技術(shù)吧。
實(shí)現(xiàn)原理
動(dòng)畫都需要一幀幀的刷新,控制臺(tái)動(dòng)畫也不例外。
那控制臺(tái)是怎么刷新的呢?
控制臺(tái)中有一種叫做 TTY,特點(diǎn)是可以設(shè)置顏色,可以清除或修改某個(gè)位置的內(nèi)容。平時(shí)我們用的 Terminal 大多都是這種。
Node.js 里面可以通過 process.stdout.isTTY 來(lái)查看是否是 TTY 類型的標(biāo)準(zhǔn)輸出流,然后提供了 readline 這個(gè)包來(lái)操作它。
比如用 readline.cursorTo(stream, x, y) 來(lái)移動(dòng)光標(biāo)位置, readline.clearLine(stream) 來(lái)清除某行的內(nèi)容,用 readline.clearScreenDown(stream)來(lái)清除某個(gè)位置之后的所有內(nèi)容。
能夠移動(dòng)光標(biāo)位置,能夠清除內(nèi)容,也就能夠刷新、能夠做任意的繪制,這是控制臺(tái)動(dòng)畫的基礎(chǔ)。
繪制用 readline.wrtie(data) 來(lái)輸出字符,可以指定字符的顏色(用 chalk 這個(gè)包)。
只是輸出帶顏色的字符么?那張圖片和那個(gè)藝術(shù)字呢?
其實(shí)那也是字符來(lái)做的,只不過給上了不同的顏色而已,控制臺(tái)只能顯示字符。
左邊的這張圖片的顯示原理是拿到圖片的像素信息,然后轉(zhuǎn)成不同顏色的字符??梢杂?console-png 這個(gè)包。
右邊的藝術(shù)字的顯示原理是固定的一些字符信息,設(shè)置上顏色。用的是 cfonts 這個(gè)包。
小結(jié)一下:
TTY 類型的控制臺(tái)可以設(shè)置顏色、可以在任意位置清除和修改內(nèi)容,這是控制臺(tái)動(dòng)畫能一幀幀刷新的基礎(chǔ),Node.js 提供了 readline 模塊來(lái)做這些。
控制臺(tái)只能顯示字符,圖片可以拿到像素信息然后用帶顏色的字符來(lái)顯示,藝術(shù)字是提前準(zhǔn)備好字符數(shù)組來(lái)繪制,綜合把這些內(nèi)容繪制在不同的位置,然后定時(shí)一幀幀刷新就構(gòu)成了控制臺(tái)動(dòng)畫。
思路通了之后,我們來(lái)寫代碼實(shí)現(xiàn)一下。
代碼實(shí)現(xiàn)
首先,我們會(huì)用到 readline 這個(gè)內(nèi)置模塊,用它來(lái)做一幀幀的刷新。
調(diào)用 readline.createInterface 來(lái)創(chuàng)建一個(gè)實(shí)例,指定輸入輸出流為 stdin、stdout。
stdin 是標(biāo)準(zhǔn)輸入流,是指鍵盤。
stdout 是標(biāo)準(zhǔn)輸出流,是指顯示器。
- const readline = require('readline');
- const outStream = process.stdout;
- const rl = readline.createInterface({
- input: process.stdin,
- output: outStream
- });
然后清除整個(gè)控制臺(tái)的內(nèi)容,把光標(biāo)移動(dòng)到開始,然后 clear:
- readline.cursorTo(outStream, 0, 0);
- readline.clearScreenDown(outStream);
然后開始繪制文字:
準(zhǔn)備一個(gè)數(shù)組放要繪制的文字,然后定時(shí)在不同的位置顯示這些文字
- const textArr = ['2021', '感謝', '大家的', '支持','2022', '我們','一起','加油!'];
- (async function () {
- for(let i = 0; i< textArr.length; i++) {
- readline.cursorTo(outStream, ...randomPos());
- rl.write(randomStyle(textArr[i]));
- await delay(1000);
- readline.cursorTo(outStream, 0, 0);
- readline.clearScreenDown(outStream);
- }
- })();
- function delay(time) {
- return new Promise((resolve) => setTimeout(resolve, time));
- }
我用了 async await 的方式來(lái)組織代碼,基于封裝了一個(gè) delay 方法。
其中位置、樣式都是隨機(jī)的:
- const chalk = require('chalk');
- function randomPos() {
- const x = Math.floor(30 * Math.random());
- const y = Math.floor(10 * Math.random());
- return [x, y];
- }
- function randomStyle(text) {
- const styles = ['redBright','yellowBright', 'blueBright', 'cyanBright','greenBright', 'magentaBright', 'whiteBright'];
- const color = styles[Math.floor(Math.random() * styles.length)];
- return chalk[color](text);
- }
前面的文字動(dòng)畫就做完了:
然后是圖片和藝術(shù)字的顯示:
圖片需要用 console-png 來(lái)把圖片像素信息取出來(lái)轉(zhuǎn)成字符的形式:
- const consolePng = require('console-png');
- consolePng.attachTo(console);
- const image = fs.readFileSync(__dirname + '/headpic.png');
- console.png(image);
藝術(shù)字用 cfonts 來(lái)繪制,拿到字符之后顯示在右邊的位置:
- const CFonts = require('cfonts');
- const prettyFont = CFonts.render('|HAPPY|NEW YEAR', {
- font:'block',
- colors: ['blue', 'yellow']
- });
- let startX = 60;
- let startY = 0;
- prettyFont.array.forEach((line, index) => {
- readline.cursorTo(outStream, startX + index, startY + index);
- rl.write(line);
- });
cfont.render 的第一個(gè)參數(shù)里的豎線是指換行,第二個(gè)參數(shù)的 font 是指定樣式,colors 指定顏色。
然后對(duì)返回的字符數(shù)組做光標(biāo)的偏移之后再顯示。
最后,在右下方顯示公眾號(hào)的標(biāo)記:
- readline.cursorTo(outStream, 120, 25);
- rl.write(chalk.yellowBright('---神光的編程秘籍'));
這樣,最后這一幀就繪制完了:
大功告成!我們?cè)賮?lái)看下整體的效果:
代碼上傳到了 github:https://github.com/QuarkGluonPlasma/cli-exercise
也在這里貼一份:
- const readline = require('readline');
- const chalk = require('chalk');
- const CFonts = require('cfonts');
- const consolePng = require('console-png');
- const fs = require('fs');
- consolePng.attachTo(console);
- const outStream = process.stdout;
- const rl = readline.createInterface({
- input: process.stdin,
- output: outStream
- });
- function delay(time) {
- return new Promise((resolve) => setTimeout(resolve, time));
- }
- function randomStyle(text) {
- const styles = ['redBright','yellowBright', 'blueBright', 'cyanBright','greenBright', 'magentaBright', 'whiteBright'];
- const color = styles[Math.floor(Math.random() * styles.length)];
- return chalk[color](text);
- }
- function randomPos() {
- const x = Math.floor(30 * Math.random());
- const y = Math.floor(10 * Math.random());
- return [x, y];
- }
- readline.cursorTo(outStream, 0, 0);
- readline.clearScreenDown(outStream);
- const image = fs.readFileSync(__dirname + '/headpic.png');
- const textArr = ['2021', '感謝', '大家的', '支持','2022', '我們','一起','加油!'];
- (async function () {
- for(let i = 0; i< textArr.length; i++) {
- readline.cursorTo(outStream, ...randomPos());
- rl.write(randomStyle(textArr[i]));
- await delay(1000);
- readline.cursorTo(outStream, 0, 0);
- readline.clearScreenDown(outStream);
- }
- console.png(image);
- await delay(1000);
- const prettyFont = CFonts.render('|HAPPY|NEW YEAR', {font:'block', colors: ['blue', 'yellow']});
- let startX = 60;
- let startY = 0;
- prettyFont.array.forEach((line, index) => {
- readline.cursorTo(outStream, startX + index, startY + index);
- rl.write(line);
- });
- readline.cursorTo(outStream, 120, 25);
- rl.write(chalk.yellowBright('---神光的編程秘籍'));
- })();
總結(jié)
TTY 類型的終端支持設(shè)置字符顏色和在任意位置清除和修改內(nèi)容,這是控制臺(tái)動(dòng)畫可以刷新的基礎(chǔ)。
我們通過把圖片的像素轉(zhuǎn)為有顏色的字符來(lái)顯示圖片,通過預(yù)置的字符數(shù)組來(lái)顯示藝術(shù)字,在不同的位置繪制這些內(nèi)容就可以達(dá)到豐富的顯示效果。
其中,控制臺(tái)的光標(biāo)位置修改和內(nèi)容的清除使用 Node.js 的 readline 內(nèi)置模塊,其余的是第三方的包。藝術(shù)字使用 cfonts 的包,圖片顯示使用 console-png,字體顏色使用 chalk。
控制臺(tái)是我們每天都用的,前端的大多數(shù)工具都是 cli 的形式。深入學(xué)習(xí) cli 顯示各種內(nèi)容和做動(dòng)畫的知識(shí),有助于更好的理解一些 cli 工具和寫出更好的 cli 工具。
最后,再次感謝大家過去一年的支持,明年一起加油呀~