How to realize the reusable console "WordArt" printing function

Before, when using some open source projects, you often see the large LOGO of the project output on the console. For example:

  • When the hexo minos theme is started, the "MINOS" copy will be displayed in the console
  • "FIS" will also be displayed when fis3 is started

Adding this large "art word" can achieve the effect of "brand exposure". Of course, it is also the embodiment of the programmer's unique "interest". 😄

However, their implementation method is nothing more than passing the arranged Logo through the console Log output. The problem with this approach is that it has almost no reusability, and some situations that need escape will lead to poor maintainability of strings. Therefore, I spent a weekend to implement an easy-to-use and reusable console "WordArt" lib. In this way, the next time there is a new demand, just pass the normal text to it, and it can help you automatically arrange and print.

1. Objectives

As mentioned in the previous section, the current practice of general projects is to write a specific string of text by yourself, such as minos:

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

And fis3, which is messy and hard to maintain because it needs to add escape

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

These methods are implemented through "hard coding". If there are new projects or demand changes, they have to be rearranged and adjusted.

Therefore, it is planned to implement a console "WordArt" printing library that can automatically typeset and display according to the input string. For example, it will output through yo ('yoo Hoo '):

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

Next time, if the copy is changed, just replace the string parameter - yo ('New one '):

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

To sum up, it is to realize a general and reusable console "WordArt" printing function. Based on this goal yoo-hoo This library.

Let's talk about how to achieve it.

2. How to achieve

Similar to other font display requirements, we can abstract the function into three parts:

  1. Generation of font library
  2. Typesetting of fonts
  3. Font rendering

Let's talk about font rendering first.

2.1. Font rendering

The reason to say this part first is that it will affect the output format of typesetting information.

In fact, there is nothing special about font rendering. In the console environment, limited by the API, we basically use console Log to "render" the content to the screen. However, it is the limitation of the "rendering" form here that will push back our typesetting method.

We know that the console is basically rendered in a single line order, which is roughly the "Z" font. At the same time, because our "WordArt" will occupy multiple lines, the final rendering is not rendered in the order of a single word. We need to arrange the version first, and then gradually render it to the screen according to the lines.

It's a bit like our common printer. If you want to print an apple, it will print the apple step by step from top to bottom instead of directly printing an apple like a seal.

Next, we will first introduce the generation of font library, rather than the next font layout. Because typesetting is a connecting process, when we determine the upstream and downstream links, the logic of this part will be determined naturally.

2.2. Font library generation

When we want to achieve reusability, we need to find or abstract the smallest logical reusable unit in the system - which is obviously a character here. In short, for the input string JS, if we can find the corresponding character representation of J and S, supplemented by typesetting, we can theoretically achieve our goal. It'S a bit like our ancestors' movable type printing.

Therefore, in the font library, we will have a mapping between word meaning and font type. In fact, this is the same as the idea of the format in the font file commonly used in our front-end. There needs to be such a mapping relationship.

Where does the font come from? Well, I also used a stupid method - painting by myself 😂. For example, here is my "hand drawn" 1:

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

The process of drawing is boring. Many font parts are reused to a certain extent, which simplifies this cumbersome work. Of course, this is only a one-time work. Once a type of "font" is created, there is no need to repeat this work in the future.

I save the above content in a separate file. For now, it is directly in the form of txt is the suffix, which is our original font format. The reason why I didn't put it in js, because \ in JavaScript wants to escape, so the visual effect of the text is inconsistent with the final rendering effect, which is not conducive to debugging and maintenance.

The original font file is divided into two parts:

  • The first line corresponds to one word meaning. For example, · and * I use the same graph. Multiple meanings are separated by spaces without line breaks.
  • Except the first line, the rest is the font.

In theory, we can use this original font file as the font library. The mapping relationship can be obtained by reading and parsing the file content through the fs module in NodeJS.

But I hope it can also be used in non NodeJS environments (such as browsers), so I can't rely on fs module. Here is a script to parse the original file and generate the corresponding JS module. Since we do not directly maintain these generated JS modules, its readability is not important. When designing the data format, it can be completely oriented to the subsequent typesetting process.

First, implement a simple parser to parse the meaning of the first line. This is also similar to a lexical parser, but because the grammar rules are extremely retarded (simple), there is no need to say more. It is roughly as follows:

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

The following is the part dealing with font. The reason why we need to deal with font is because of the escape problem mentioned above. Because we used \ in the original format for font display, and put it directly into the generated JS file, this \ becomes an escape character. To display normally, it needs to be changed to \. One way is regular matching, which replaces \ in all source text with \ \ and then writes it. But I chose another way.

Pass characters through The charCodeAt method is converted to char code storage, and the font information is read through string Turn back from charcode. The original string becomes an array of numbers, so there is no problem with special characters. Finally, by splicing text and generating JS file, the original font file which is convenient for human maintenance is transformed into a module for compiling 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 is the word meaning list corresponding to the font, codes is the char code array of the font, and all fonts are placed in a JS file.

It is mentioned here that parsedFonts in line 3 is to traverse the content parsed by all the original font files. Therefore, this part also needs to recursively read the font files in the source file directory through the fs module of NodeJS. It's a basic exercise, so you don't have to start it.

Because this part can be parsed and compiled in advance, once the JS module is generated, it will not depend on the NodeJS runtime, so it can still run in the browser.

2.3. Typesetting of fonts

Our font format is determined, and the rendering method of the target is also determined. Finally, you can fill in the logical implementation of this part.

Some fine nodes will be encountered in the specific typesetting, such as the filling of empty lines with unequal height fonts and the judgment of the maximum line width (the user needs to execute the line width), but these are small points and the processing is not too complicated. Here may introduce a slightly special piece - word spacing adjustment.

As we know, some artistic characters may be highly inclined, such as the character "1":

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

If the space is allocated according to a simple rectangular bounding box, it will probably be as follows:

Even if the front and back fonts are set to the minimum spacing (0), they will still be far away, which destroys a certain display effect. For example, in the figure above, the distance between my two bounding boxes is actually only 1, but it looks very large. What we actually hope is as follows:

When the spacing is 1, the two characters "1" are adjusted to be 1 in the nearest place. If you want a wider effect, you can set more spacing. This process mainly needs to calculate the maximum "squeeze space" (that is, the intersection space supported by the two boxes). At the beginning of rendering, we said that we store and print according to the lines out of the console. For example, the height of "1" is 8, so when rendering, it is an 8-element string array:

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

Direct lines when rendering Foreach (L = > console. Log (L)) is enough.

💣 Note that in order to facilitate readers' reading, I did not add escape to the string in the lines array above, which is illegal! Just to make it easier to read and understand, it can't be written in practice.

The calculation of maximum indentation (the word indentation is not accurate, but I hope you can understand that meaning) only needs to know how many spaces there are at the end of each line before, and how many spaces there are in front of each line after adding new characters. Combine the two, and then traverse all lines to take a minimum value:

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

Finally, the calcIndent method returns the value that the new character needs to be indented (or tightened) forward. Finally, when rendering, adjust the number of spaces added when connecting each line according to this value.

Incidentally, the previous font format will be converted into a dictionary like format - the meaning of the word is used as the key, and a series of attributes such as the font type are used as the value:

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

In this way, after split ting the string passed in by the user, it is easier to index the corresponding font and font information.

2.4. other

Of course, there will be some other work, including

  • Support color
  • Support the return of lines after typesetting, so that users can render by themselves
  • Support user-defined adjustment of word spacing

These problems encountered in the current implementation are not large, and the reason for the length will not be mentioned. Specific codes can be found in Github I saw it on the.

3. Summary

To realize the reusable console "WordArt" function, generally speaking, there are not many complex points. The overall process model is

Generate font library -- > font layout -- > render text

This should be very easy to understand for the front end.

It's true that I want to add this logo or banner display to some libraries in my work, but it's really disgusting to repeat the boring work every time. So I thought about the feasibility and did it yoo-hoo Such a gadget, if you also encounter similar problems, I hope it can be helpful.

npm i yoo-hoo

4. Finally

Current yoo-hoo@1.0.x Built in a set of fonts with 26 letters (A-Z), 10 numbers (0-9), · * - | these characters.

Considering that a single font and a limited number of fonts can not meet all requirements, the code structure leaves a mode supporting external expansion during development.

Later, the font source file analysis tool in Section 2.2 can be independent to support users to "draw" their own font. After generating the corresponding format with the tool, the JS module of the font is passed into the yo method to load as an extended font.

Although the "hand drawing" of font source files has cost, what you see is what you get, so it is not difficult to write 🐶 At the same time, it is once and for all.

Tags: Javascript Front-end

Posted by bucfan99 on Sat, 07 May 2022 01:48:37 +0300