乐趣区

关于javascript:如何实现可复用的控制台艺术字打印功能

之前在应用一些开源我的项目时,常常会看到在控制台输入我的项目大大的 LOGO。例如:

  • hexo minos 主题启动时在控制台里会显示「MINOS」文案
  • fis3 启动时也会有显示「FIS」

增加这种大号「艺术字」能够达到「品牌露出」的成果,当然,也是程序员特有「情趣」的体现。????

但它们的实现形式无外乎把编排好的 Logo 通过 console.log 输入。这种形式问题在于它简直没有任何复用能力,而且一些须要本义的状况还会导致字符串的可维护性极差。因而,我花了一个周末的时候,实现了一个易用的、可复用的控制台「艺术字」lib。这样,下次有新的需要,只须要把失常的文本传给它,它就能够帮你 主动编排与打印

1. 指标

正如上节所说,目前个别我的项目的做法都是自定写一串特定的文本,例如 minos:

logger.info(`=======================================
███╗   ███╗ ██╗ ███╗   ██╗  ██████╗  ███████╗
████╗ ████║ ██║ ████╗  ██║ ██╔═══██╗ ██╔════╝
██╔████╔██║ ██║ ██╔██╗ ██║ ██║   ██║ ███████╗
██║╚██╔╝██║ ██║ ██║╚██╗██║ ██║   ██║ ╚════██║
██║ ╚═╝ ██║ ██║ ██║ ╚████║ ╚██████╔╝ ███████║
╚═╝     ╚═╝ ╚═╝ ╚═╝  ╚═══╝  ╚═════╝  ╚══════╝
=============================================`);

还有 fis3 这种因为须要增加本义所以显得凌乱不好保护的

logo = [
      '/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\  /\\\\\\\\\\\\\\\\\\\\\\     /\\\\\\\\\\\\\\\\\\\\\\',
      '\\/\\\\\\///////////  \\/////\\\\\\///    /\\\\\\/////////\\\\\\',
      '\\/\\\\\\                 \\/\\\\\\      \\//\\\\\\      \\///',
      '\\/\\\\\\\\\\\\\\\\\\\\\\         \\/\\\\\\       \\////\\\\\\',
      '\\/\\\\\\///////          \\/\\\\\\          \\////\\\\\\',
      '\\/\\\\\\                 \\/\\\\\\             \\////\\\\\\',
      '\\/\\\\\\                 \\/\\\\\\      /\\\\\\      \\//\\\\\\',
      '\\/\\\\\\              /\\\\\\\\\\\\\\\\\\\\\\ \\///\\\\\\\\\\\\\\\\\\\\\\/',
      '\\///              \\///////////    \\///////////',
      ''].join('\n');

这种些形式都是通过「硬编码」来实现的,如果有了新我的项目或需要变动还得从新编排调整。

因而,筹备实现一种可能依据输出的字符串进行主动排版展现的控制台「艺术字」打印库,例如通过 yo('yoo-hoo') 就会输入:

 /\\\    /\\\  /\\\\\\\\      /\\\\\\\\                /\\\    /\\\    /\\\\\\\\      /\\\\\\\\
 \/\\\   /\\\ /\\\_____/\\\  /\\\_____/\\\             \/\\\   \/\\\  /\\\_____/\\\  /\\\_____/\\\
   \/_\\\/\\\ \/\\\    \/\\\ \/\\\    \/\\\             \/\\\   \/\\\ \/\\\    \/\\\ \/\\\    \/\\\
      \/_\\\\  \/\\\    \/\\\ \/\\\    \/\\\  /\\\\\\\\\ \/\\\\\\\\\\\ \/\\\    \/\\\ \/\\\    \/\\\
         \/\\\  \/\\\    \/\\\ \/\\\    \/\\\ \/_______/  \/\\\____/\\\ \/\\\    \/\\\ \/\\\    \/\\\
          \/\\\  \/\\\    \/\\\ \/\\\    \/\\\             \/\\\   \/\\\ \/\\\    \/\\\ \/\\\    \/\\\
           \/\\\  \/_/\\\\\\\\\  \/_/\\\\\\\\\              \/\\\   \/\\\ \/_/\\\\\\\\\  \/_/\\\\\\\\\
            \/_/     \/_______/     \/_______/               \/_/    \/_/    \/_______/     \/_______/

下次如果文案改了,间接换下字符串参数就行 —— yo('new-one')

/\\\\\     /\\\  /\\\\\\\\\\  /\\\  \\\  \\\                /\\\\\\\\    /\\\\\     /\\\  /\\\\\\\\\\
\/\\\ \\\  \/\\\ \/\\\_____/  \/\\\  \\\  \\\              /\\\_____/\\\ \/\\\ \\\  \/\\\ \/\\\_____/
 \/\\\ /\\\ \/\\\ \/\\\        \/\\\  \\\  \\\             \/\\\    \/\\\ \/\\\ /\\\ \/\\\ \/\\\
  \/\\\  /\\\ /\\\ \/\\\\\\\\\\ \/\\\  \\\  \\\  /\\\\\\\\\ \/\\\    \/\\\ \/\\\  /\\\ /\\\ \/\\\\\\\\\\
   \/\\\ \/\\\ /\\\ \/\\\_____/  \/\\\  \\\  \\\ \/_______/  \/\\\    \/\\\ \/\\\ \/\\\ /\\\ \/\\\_____/
    \/\\\ \ /\\\ \\\ \/\\\        \/\\\ \\\\\ \\\             \/\\\    \/\\\ \/\\\ \ /\\\ \\\ \/\\\
     \/\\\  \/_\\\\\\ \/\\\\\\\\\\ \/\\\\\__/\\\\\             \/_/\\\\\\\\\  \/\\\  \/_\\\\\\ \/\\\\\\\\\\
      \/_/    \/____/  \/________/  \/_/      \/_/                \/_______/   \/_/    \/____/  \/________/

总结来说,就是实现一个通用的、可复用的控制台「艺术字」打印性能。基于这个指标开发了 yoo-hoo 这个库。

上面来说说大抵怎么实现。

2. 如何实现

和其余字体显示的需要相似,咱们能够将性能形象为三个局部:

  1. 字体库的生成
  2. 字体的排版
  3. 字体的渲染

这里咱们先说一下字体的渲染。

2.1. 字体渲染

之所以先说这部分,是因为它会影响排版信息的输入格局。

其实字体渲染这部分并没有什么特地的,咱们在控制台这个环境,受限于 API,根本就是应用 console.log 来将内容「渲染」到屏幕上。不过,正是这里的「渲染」模式的限度,会倒推咱们的排版形式。

咱们晓得,控制台根本都是单行程序渲染的,大抵就是「Z」字型。同时,因为咱们的「艺术字」会占据多行,所以最终的渲染不是按单个字程序渲染的,须要先排好版,而后按行来逐渐渲染到屏幕上。

这有点像是咱们常见的打印机。如果你要打印一个苹果,它会从上往下逐渐打印出这个苹果,而不是间接像盖章那样间接印刷一个苹果。

上面咱们会先介绍字体库的生成,而不是紧接挨着的字体排版。因为排版是一个承前启后的过程,当咱们确定了上下游环节,这块的逻辑天然也就确定了。

2.2. 字体库生成

当咱们想要实现可复用能力时,因而咱们须要找到或者形象出零碎内逻辑上的最小可复用单元 —— 在这里显然就是字符。简略来说,对于输出字符串 JS 时,如果咱们能找到对应的 J 和 S 的字符示意模式,辅以排版,实践上就有能力实现咱们的指标。这有点像是咱们老祖宗的活字印刷术。

所以在字体库这里,咱们会有一个字义与字型的映射。这个其实和咱们前端常见的字体文件内格局的思维一样,都须要有这么一个映射关系。

字型哪里来呢?好吧,我也是用了一个笨办法 —— 本人「手绘」????。举个例子,上面就是我「手绘」的 1:

1
  /\\\
/\\\\\\
\/__/\\\
    \/\\\
     \/\\\
      \/\\\
      /\\\\\\\
      \/_____/

绘制的过程是干燥的,好再很多字型的部分是有肯定复用的,简化了这项繁琐的工作。当然,这只是一次性的工作,一旦创立好一类「字体」,当前就不须要再反复这项工作了。

我把下面这个内容存在一个独自的文件中,目前间接以 .txt 为后缀,这就是咱们的字体原始格局。之所以不放在 .js 中,是因为 JavaScript 中 \ 是想要本义的,这样文本的视觉和最初的出现成果就不统一了,不利于调试和保护。

原始字体文件分为两局部:

  • 下面第一行是字义,反对一个多个字义对应一个图形。例如 ·* 我应用了同一个图形。多个字义间空格宰割,不换行。
  • 除去第一行,剩下的内容就是字型。

实践上,咱们能够以这个原始字体文件来作为字体库了,通过 NodeJS 中的 fs 模块读取并解析文件内容即可失去映射关系。

但我心愿它也能在非 NodeJS 环境(例如浏览器)中应用,所以不能依赖 fs 模块。这里做了一个原始文件的解析脚本,生成对应的 JS 模块。因为咱们并不间接保护这些生成的 JS 模块,所以它的可读性不重要,能够设计数据格式的时候能够齐全面向后续的排版流程。

首先实现一个简略的解析器来解析第一行的字义。这也相似一个词法解析器,但因为语法规定极其弱智(简略),所以也就不必多说了,大抵如下:

const parseDefinition = function (line: string) {
    let token = '';
    const defs: string[] = [];
    for (const char of line) {if (char === ' ' && token) {defs.push(token);
            token = '';
        }
        if (char !== ' ') {token += char;}
    }
    if (token) {defs.push(token);
    }
    return defs;
}

上面就是解决字型局部。之所以须要解决字型,是因为下面提到的本义问题。因为咱们在原始格局中应用了 \ 来进行字型展现,而将其间接放入生成的 JS 文件中这个 \ 就变为了本义符,要想失常展现须要变为 \\。一种形式是正则匹配,将所有源文本中的 \ 替换为 \\ 再写入。但我抉择了另一种形式。

将字符通过 .charCodeAt 办法转为 char code 存储,读取字体信息时再通过 String.fromCharCode 转回来。原来的字符串变成了数字类型的数组,这样就没有特殊字符的问题了。最初,通过拼接文本并生成 JS 文件来将原始的、利于人保护的字体文件,转成了编译 JS 工作的模块。

const arrayToString = <T>(arr: T[]) => '[' + arr.map(d => `'${d}'`).join(',') + ']';

const text = parsedFonts.reduce((t, f, idx) => {
    return t + (
        '\n/**\n'
        + f.content
        + '\n*/\n'
        + `fonts[${idx}] = {\n`
        + `  defs: ${arrayToString(f.defs)},\n`
        + `  codes: ${arrayToString(f.codes)}\n`
        + '};\n'
    );
}, '');
const moduleText = ('const fonts = [];\n'
    + text
    + 'module.exports.fonts = fonts;\n'
);

fs.writeFileSync(fontFilepath, moduleText, 'utf-8');

其中 defs 就是这个字型对应的字义列表,codes 则是字型的 char code 数组,所有的字体都被放在一个 JS 文件中。

这里提一下,第 3 行的 parsedFonts 就是遍历所有原始字体文件解析到的内容,因而失去这部分也是须要通过 NodeJS 的 fs 模块来递归读取源文件目录下的字体文件的。算是基操,就不必开展了。

因为这部分是能够提前解析编译的,一旦生成了 JS 模块后就不会对 NodeJS 运行时有依赖,所以保障了其仍然能够运行在浏览器中。

2.3. 字体的排版

咱们的字体格局确定了,指标的渲染形式也确定了。最初就能够填充这部分的逻辑实现了。

具体排版上会遇到一些细节点,例如不等高字体的空行填充、最大行宽的换行判断(须要用户执行行宽),不过这些都是小点,解决也不太简单。这里可能介绍一下稍有非凡的一块 —— 字间距调整。

咱们晓得,一些艺术字的歪斜水平可能很大,例如这个字符「1」:

  /\\\
/\\\\\\
\/__/\\\
    \/\\\
     \/\\\
      \/\\\
      /\\\\\\\
      \/_____/

如果按简略的矩形型突围盒来调配空间,大略会是上面这样:

前后两个字体,即便设置为最小间距(0),依然会间隔很远,这样就毁坏了肯定的显示成果。例如上图中我两个突围盒间距其实只有 1,但看起来就很大。咱们理论心愿的可能是上面这样:

间距为 1 时,两个字符「1」调整为在最近的中央间距为 1。如果要更宽的成果能够设置更多间距。这个解决起来次要就是须要算出最大的「挤压空间」(即两个盒子最大反对的穿插空间)。最开始渲染的时候说了,咱们是按 console 出的行来存储的与打印的,举个例子,这个「1」高度为 8,所以渲染的时候就是一个 8 个元素的字符串数组:

const lines = [
    '/\\\',
    '/\\\\\\',
    '\/__/\\\',
    '\/\\\',
    '\/\\\',
    '\/\\\',
    '/\\\\\\\',
    '\/_____/',
];

渲染的时候间接 lines.forEach(l => console.log(l)) 即可。

???? 留神,为了便于读者浏览,下面的 lines 数组内的字符串我没有加上本义,它是不非法的!只是为了展现起来更便于浏览了解,理论中不能这么写。

最大缩进(缩进这个词不精确,但心愿大家可能了解那个意思)的计算只须要晓得之前的每个 line 尾部对应有多少空格,同时须要再其后新增加字符每个 line 后面又别离有多少空格,综合两者,再遍历所有的 line 取一个最小值即可:

// calc the prefix space
const prefixSpace = function (str: string) {const matched = /^\s+/gu.exec(str);

    return matched ? matched[0].length : 0;
};

// calc the tail space
const tailSpace = function (str: string) {const matched = /\s+$/gu.exec(str);

    return matched ? matched[0].length : 0;
};

// calc how many spaces need for indent for layout
// overwise the gap between two characters will be different
const calcIndent = function (lines: string[], charLines: string[]): number {
    // maximum indent that won't break the layout
    let maxPossible = Infinity;

    for (let i = 1; i < lines.length; i++) {const formerTailNum = tailSpace(lines[i]);
        const latterPrefixNum = prefixSpace(charLines[i]);

        maxPossible = Math.min(maxPossible, formerTailNum + latterPrefixNum);
    }

    return maxPossible;
};

最初 calcIndent 办法返回的就是新字符须要向前缩进(或者说缩紧)的值。最初渲染的时候依据这个值来调整每行连贯时增加的空格数即可。

捎带一提,之前的字体格局 load 进来会被转换为相似字典的格局 —— 字义作为 key,字型等一系列属性作为 value:

const dictionary = {
    'a': {lines: [...],
        width: ...,
        height: ...,
    },
    'b': {...},
    ...
}

这样遍于 split 完用户传入的字符串后,更简略的索引到对应的字型和字体信息。

2.4. 其余

当然,其余还会有一些工作,包含

  • 反对色彩
  • 反对返回排版完的 lines 让用户本人渲染
  • 反对用户自定义调整字间距

这些目前实现上遇到的问题不大,篇幅起因也就不说了。具体的代码能够在 Github 上看到。

3. 总结

实现可复用的控制台“艺术字”性能,总的来说并没有太多简单的点,整体的流程模型就是

生成字体库 --> 字体排版 --> 渲染文本

这对于前端来说应该是十分好了解的。

做这个我的项目也的确是本人在工作中心愿给一些库加上这种 logo 或者 banner 展现,但每次反复干燥的工作的确令人恶感。所以想了下可行性之后就搞了 yoo-hoo 这么个小玩意儿,如果大家也遇到相似的问题,心愿能有所帮忙。

npm i yoo-hoo

4. 最初

目前 yoo-hoo@1.0.x 内置了一套 26 个字母(A-Z)、10 个数字(0-9)、· * - | 这些字符的字体库。

思考到繁多的字型和无限的字体量必定不能满足所有需要,所以开发时代码构造就留下了反对内部扩大的模式。

后续能够把 2.2 节中的字体源文件解析工具独立进去,反对用户「手绘」本人的字型,用工具生成对应格局后,将字体的 JS 模块传入 yo 办法中作为扩大字体加载。

字体源文件的「手绘」虽有老本,但所见即所得,编写难度不大 ???? 同时也算是一劳永逸。

退出移动版