如何實現可複用的控制檯「藝術字」打印功能

以前在使用一些開源項目時,常常會看到在控制檯輸出項目大大的 LOGO。例如:前端

  • hexo minos 主題啓動時在控制檯裏會顯示「MINOS」文案
  • fis3 啓動時也會有顯示「FIS」

添加這種大號「藝術字」能夠達到「品牌露出」的效果,固然,也是程序員特有「情趣」的體現。 😄git

但它們的實現方式無外乎把編排好的 Logo 經過 console.log 輸出。這種方式問題在於它幾乎沒有任何複用能力,並且一些須要轉義的狀況還會致使字符串的可維護性極差。所以,我花了一個週末的時候,實現了一個易用的、可複用的控制檯「藝術字」lib。這樣,下次有新的需求,只須要把正常的文本傳給它,它就能夠幫你自動編排與打印程序員

1. 目標

正如上節所說,目前通常項目的作法都是自定寫一串特定的文本,例如 minos:github

logger.info(`======================================= ███╗ ███╗ ██╗ ███╗ ██╗ ██████╗ ███████╗ ████╗ ████║ ██║ ████╗ ██║ ██╔═══██╗ ██╔════╝ ██╔████╔██║ ██║ ██╔██╗ ██║ ██║ ██║ ███████╗ ██║╚██╔╝██║ ██║ ██║╚██╗██║ ██║ ██║ ╚════██║ ██║ ╚═╝ ██║ ██║ ██║ ╚████║ ╚██████╔╝ ███████║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ =============================================`);
複製代碼

還有 fis3 這種因爲須要添加轉義因此顯得凌亂很差維護的typescript

logo = [
      ' /\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\\\\\ ',
      ' \\/\\\\\\/////////// \\/////\\\\\\/// /\\\\\\/////////\\\\\\ ',
      ' \\/\\\\\\ \\/\\\\\\ \\//\\\\\\ \\/// ',
      ' \\/\\\\\\\\\\\\\\\\\\\\\\ \\/\\\\\\ \\////\\\\\\ ',
      ' \\/\\\\\\/////// \\/\\\\\\ \\////\\\\\\ ',
      ' \\/\\\\\\ \\/\\\\\\ \\////\\\\\\ ',
      ' \\/\\\\\\ \\/\\\\\\ /\\\\\\ \\//\\\\\\ ',
      ' \\/\\\\\\ /\\\\\\\\\\\\\\\\\\\\\\ \\///\\\\\\\\\\\\\\\\\\\\\\/ ',
      ' \\/// \\/////////// \\/////////// ',
      ''
    ].join('\n');
複製代碼

這種些方式都是經過「硬編碼」來實現的,若是有了新項目或需求變更還得從新編排調整。npm

所以,準備實現一種可以根據輸入的字符串進行自動排版展現的控制檯「藝術字」打印庫,例如經過 yo('yoo-hoo') 就會輸出:數組

/\\\    /\\\  /\\\\\\\\      /\\\\\\\\                /\\\    /\\\    /\\\\\\\\      /\\\\\\\\
 \/\\\   /\\\ /\\\_____/\\\  /\\\_____/\\\             \/\\\   \/\\\  /\\\_____/\\\  /\\\_____/\\\
   \/_\\\/\\\ \/\\\    \/\\\ \/\\\    \/\\\             \/\\\   \/\\\ \/\\\    \/\\\ \/\\\    \/\\\
      \/_\\\\  \/\\\    \/\\\ \/\\\    \/\\\  /\\\\\\\\\ \/\\\\\\\\\\\ \/\\\    \/\\\ \/\\\    \/\\\
         \/\\\  \/\\\    \/\\\ \/\\\    \/\\\ \/_______/  \/\\\____/\\\ \/\\\    \/\\\ \/\\\    \/\\\
          \/\\\  \/\\\    \/\\\ \/\\\    \/\\\             \/\\\   \/\\\ \/\\\    \/\\\ \/\\\    \/\\\
           \/\\\  \/_/\\\\\\\\\  \/_/\\\\\\\\\              \/\\\   \/\\\ \/_/\\\\\\\\\  \/_/\\\\\\\\\
            \/_/     \/_______/     \/_______/               \/_/    \/_/    \/_______/     \/_______/
複製代碼

下次若是文案改了,直接換下字符串參數就行 —— yo('new-one')瀏覽器

/\\\\\     /\\\  /\\\\\\\\\\  /\\\  \\\  \\\                /\\\\\\\\    /\\\\\     /\\\  /\\\\\\\\\\
\/\\\ \\\  \/\\\ \/\\\_____/  \/\\\  \\\  \\\              /\\\_____/\\\ \/\\\ \\\  \/\\\ \/\\\_____/
 \/\\\ /\\\ \/\\\ \/\\\        \/\\\  \\\  \\\             \/\\\    \/\\\ \/\\\ /\\\ \/\\\ \/\\\
  \/\\\  /\\\ /\\\ \/\\\\\\\\\\ \/\\\  \\\  \\\  /\\\\\\\\\ \/\\\    \/\\\ \/\\\  /\\\ /\\\ \/\\\\\\\\\\
   \/\\\ \/\\\ /\\\ \/\\\_____/  \/\\\  \\\  \\\ \/_______/  \/\\\    \/\\\ \/\\\ \/\\\ /\\\ \/\\\_____/
    \/\\\ \ /\\\ \\\ \/\\\        \/\\\ \\\\\ \\\             \/\\\    \/\\\ \/\\\ \ /\\\ \\\ \/\\\
     \/\\\  \/_\\\\\\ \/\\\\\\\\\\ \/\\\\\__/\\\\\             \/_/\\\\\\\\\  \/\\\  \/_\\\\\\ \/\\\\\\\\\\
      \/_/    \/____/  \/________/  \/_/      \/_/                \/_______/   \/_/    \/____/  \/________/
複製代碼

總結來講,就是實現一個通用的、可複用的控制檯「藝術字」打印功能。基於這個目標開發了 yoo-hoo 這個庫。bash

下面來講說大體怎麼實現。markdown

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 方法中做爲擴展字體加載。

字體源文件的「手繪」雖有成本,但所見即所得,編寫難度不大 🐶 同時也算是一勞永逸。

相關文章
相關標籤/搜索