Node.js 控制台动画,绘制跨年祝福

3,147 阅读5分钟

今天是 2021 年的最后一天了,明天就是 2022 年。回顾过去一年,要特别感谢大家对我的支持。

人生不过几十年,每一年都值得纪念和祝福,所以我想用 Node.js 控制台动画送上一份我的新年祝福:

2021 年的最后一天,我们来学点 cli 的技术吧。

实现原理

动画都需要一帧帧的刷新,控制台动画也不例外。

那控制台是怎么刷新的呢?

控制台中有一种叫做 TTY,特点是可以设置颜色,可以清除或修改某个位置的内容。平时我们用的 Terminal 大多都是这种。

Node.js 里面可以通过 process.stdout.isTTY 来查看是否是 TTY 类型的标准输出流,然后提供了 readline 这个包来操作它。

比如用 readline.cursorTo(stream, x, y) 来移动光标位置, readline.clearLine(stream) 来清除某行的内容,用 readline.clearScreenDown(stream)来清除某个位置之后的所有内容。

能够移动光标位置,能够清除内容,也就能够刷新、能够做任意的绘制,这是控制台动画的基础。

绘制用 readline.wrtie(data) 来输出字符,可以指定字符的颜色(用 chalk 这个包)。

只是输出带颜色的字符么?那张图片和那个艺术字呢?

其实那也是字符来做的,只不过给上了不同的颜色而已,控制台只能显示字符。

左边的这张图片的显示原理是拿到图片的像素信息,然后转成不同颜色的字符。可以用 console-png 这个包。

右边的艺术字的显示原理是固定的一些字符信息,设置上颜色。用的是 cfonts 这个包。

小结一下:

TTY 类型的控制台可以设置颜色、可以在任意位置清除和修改内容,这是控制台动画能一帧帧刷新的基础,Node.js 提供了 readline 模块来做这些。

控制台只能显示字符,图片可以拿到像素信息然后用带颜色的字符来显示,艺术字是提前准备好字符数组来绘制,综合把这些内容绘制在不同的位置,然后定时一帧帧刷新就构成了控制台动画。

思路通了之后,我们来写代码实现一下。

代码实现

首先,我们会用到 readline 这个内置模块,用它来做一帧帧的刷新。

调用 readline.createInterface 来创建一个实例,指定输入输出流为 stdin、stdout。

stdin 是标准输入流,是指键盘。

stdout 是标准输出流,是指显示器。

const readline = require('readline');

const outStream = process.stdout;

const rl = readline.createInterface({
    input: process.stdin,
    output: outStream
});

然后清除整个控制台的内容,把光标移动到开始,然后 clear:

readline.cursorTo(outStream, 0, 0);
readline.clearScreenDown(outStream);

然后开始绘制文字:

准备一个数组放要绘制的文字,然后定时在不同的位置显示这些文字

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 的方式来组织代码,基于封装了一个 delay 方法。

其中位置、样式都是随机的:

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);
}

前面的文字动画就做完了:

然后是图片和艺术字的显示:

图片需要用 console-png 来把图片像素信息取出来转成字符的形式:

const consolePng = require('console-png');
consolePng.attachTo(console);

const image = fs.readFileSync(__dirname + '/headpic.png');
console.png(image);

艺术字用 cfonts 来绘制,拿到字符之后显示在右边的位置:

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 的第一个参数里的竖线是指换行,第二个参数的 font 是指定样式,colors 指定颜色。

然后对返回的字符数组做光标的偏移之后再显示。

最后,在右下方显示公众号的标记:

readline.cursorTo(outStream, 120, 25);
rl.write(chalk.yellowBright('---神光的编程秘籍'));

这样,最后这一帧就绘制完了:

大功告成!我们再来看下整体的效果:

代码上传到了 github:github.com/QuarkGluonP…

也在这里贴一份:

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('---神光的编程秘籍'));
})();

总结

TTY 类型的终端支持设置字符颜色和在任意位置清除和修改内容,这是控制台动画可以刷新的基础。

我们通过把图片的像素转为有颜色的字符来显示图片,通过预置的字符数组来显示艺术字,在不同的位置绘制这些内容就可以达到丰富的显示效果。

其中,控制台的光标位置修改和内容的清除使用 Node.js 的 readline 内置模块,其余的是第三方的包。艺术字使用 cfonts 的包,图片显示使用 console-png,字体颜色使用 chalk。

控制台是我们每天都用的,前端的大多数工具都是 cli 的形式。深入学习 cli 显示各种内容和做动画的知识,有助于更好的理解一些 cli 工具和写出更好的 cli 工具。

最后,再次感谢大家过去一年的支持,明年一起加油呀~