以前在使用一些開源項目時,常常會看到在控制檯輸出項目大大的 LOGO。例如:前端
添加這種大號「藝術字」能夠達到「品牌露出」的效果,固然,也是程序員特有「情趣」的體現。 😄git
但它們的實現方式無外乎把編排好的 Logo 經過 console.log
輸出。這種方式問題在於它幾乎沒有任何複用能力,並且一些須要轉義的狀況還會致使字符串的可維護性極差。所以,我花了一個週末的時候,實現了一個易用的、可複用的控制檯「藝術字」lib。這樣,下次有新的需求,只須要把正常的文本傳給它,它就能夠幫你自動編排與打印。程序員
正如上節所說,目前通常項目的作法都是自定寫一串特定的文本,例如 minos:github
logger.info(`======================================= ███╗ ███╗ ██╗ ███╗ ██╗ ██████╗ ███████╗ ████╗ ████║ ██║ ████╗ ██║ ██╔═══██╗ ██╔════╝ ██╔████╔██║ ██║ ██╔██╗ ██║ ██║ ██║ ███████╗ ██║╚██╔╝██║ ██║ ██║╚██╗██║ ██║ ██║ ╚════██║ ██║ ╚═╝ ██║ ██║ ██║ ╚████║ ╚██████╔╝ ███████║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ =============================================`);
複製代碼
還有 fis3 這種因爲須要添加轉義因此顯得凌亂很差維護的typescript
logo = [
' /\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\\\\\ /\\\\\\\\\\\\\\\\\\\\\\ ',
' \\/\\\\\\/////////// \\/////\\\\\\/// /\\\\\\/////////\\\\\\ ',
' \\/\\\\\\ \\/\\\\\\ \\//\\\\\\ \\/// ',
' \\/\\\\\\\\\\\\\\\\\\\\\\ \\/\\\\\\ \\////\\\\\\ ',
' \\/\\\\\\/////// \\/\\\\\\ \\////\\\\\\ ',
' \\/\\\\\\ \\/\\\\\\ \\////\\\\\\ ',
' \\/\\\\\\ \\/\\\\\\ /\\\\\\ \\//\\\\\\ ',
' \\/\\\\\\ /\\\\\\\\\\\\\\\\\\\\\\ \\///\\\\\\\\\\\\\\\\\\\\\\/ ',
' \\/// \\/////////// \\/////////// ',
''
].join('\n');
複製代碼
這種些方式都是經過「硬編碼」來實現的,若是有了新項目或需求變更還得從新編排調整。npm
所以,準備實現一種可以根據輸入的字符串進行自動排版展現的控制檯「藝術字」打印庫,例如經過 yo('yoo-hoo')
就會輸出:數組
/\\\ /\\\ /\\\\\\\\ /\\\\\\\\ /\\\ /\\\ /\\\\\\\\ /\\\\\\\\
\/\\\ /\\\ /\\\_____/\\\ /\\\_____/\\\ \/\\\ \/\\\ /\\\_____/\\\ /\\\_____/\\\
\/_\\\/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\
\/_\\\\ \/\\\ \/\\\ \/\\\ \/\\\ /\\\\\\\\\ \/\\\\\\\\\\\ \/\\\ \/\\\ \/\\\ \/\\\
\/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/_______/ \/\\\____/\\\ \/\\\ \/\\\ \/\\\ \/\\\
\/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\ \/\\\
\/\\\ \/_/\\\\\\\\\ \/_/\\\\\\\\\ \/\\\ \/\\\ \/_/\\\\\\\\\ \/_/\\\\\\\\\
\/_/ \/_______/ \/_______/ \/_/ \/_/ \/_______/ \/_______/
複製代碼
下次若是文案改了,直接換下字符串參數就行 —— yo('new-one')
:瀏覽器
/\\\\\ /\\\ /\\\\\\\\\\ /\\\ \\\ \\\ /\\\\\\\\ /\\\\\ /\\\ /\\\\\\\\\\
\/\\\ \\\ \/\\\ \/\\\_____/ \/\\\ \\\ \\\ /\\\_____/\\\ \/\\\ \\\ \/\\\ \/\\\_____/
\/\\\ /\\\ \/\\\ \/\\\ \/\\\ \\\ \\\ \/\\\ \/\\\ \/\\\ /\\\ \/\\\ \/\\\
\/\\\ /\\\ /\\\ \/\\\\\\\\\\ \/\\\ \\\ \\\ /\\\\\\\\\ \/\\\ \/\\\ \/\\\ /\\\ /\\\ \/\\\\\\\\\\
\/\\\ \/\\\ /\\\ \/\\\_____/ \/\\\ \\\ \\\ \/_______/ \/\\\ \/\\\ \/\\\ \/\\\ /\\\ \/\\\_____/
\/\\\ \ /\\\ \\\ \/\\\ \/\\\ \\\\\ \\\ \/\\\ \/\\\ \/\\\ \ /\\\ \\\ \/\\\
\/\\\ \/_\\\\\\ \/\\\\\\\\\\ \/\\\\\__/\\\\\ \/_/\\\\\\\\\ \/\\\ \/_\\\\\\ \/\\\\\\\\\\
\/_/ \/____/ \/________/ \/_/ \/_/ \/_______/ \/_/ \/____/ \/________/
複製代碼
總結來講,就是實現一個通用的、可複用的控制檯「藝術字」打印功能。基於這個目標開發了 yoo-hoo 這個庫。bash
下面來講說大體怎麼實現。markdown
和其餘字體顯示的需求相似,咱們能夠將功能抽象爲三個部分:
這裏咱們先說一下字體的渲染。
之因此先說這部分,是由於它會影響排版信息的輸出格式。
其實字體渲染這部分並無什麼特別的,咱們在控制檯這個環境,受限於 API,基本就是使用 console.log
來將內容「渲染」到屏幕上。不過,正是這裏的「渲染」形式的限制,會倒推咱們的排版方式。
咱們知道,控制檯基本都是單行順序渲染的,大體就是「Z」字型。同時,因爲咱們的「藝術字」會佔據多行,因此最終的渲染不是按單個字順序渲染的,須要先排好版,而後按行來逐步渲染到屏幕上。
這有點像是我們常見的打印機。若是你要打印一個蘋果,它會從上往下逐步打印出這個蘋果,而不是直接像蓋章那樣直接印刷一個蘋果。
下面咱們會先介紹字體庫的生成,而不是緊接挨着的字體排版。由於排版是一個承上啓下的過程,當咱們肯定了上下游環節,這塊的邏輯天然也就肯定了。
當咱們想要實現可複用能力時,所以咱們須要找到或者抽象出系統內邏輯上的最小可複用單元 —— 在這裏顯然就是字符。簡單來講,對於輸入字符串 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 運行時有依賴,因此保證了其依然能夠運行在瀏覽器中。
咱們的字體格式肯定了,目標的渲染方式也肯定了。最後就能夠填充這部分的邏輯實現了。
具體排版上會遇到一些細節點,例如不等高字體的空行填充、最大行寬的換行判斷(須要用戶執行行寬),不過這些都是小點,處理也不太複雜。這裏可能介紹一下稍有特殊的一塊 —— 字間距調整。
咱們知道,一些藝術字的傾斜程度可能很大,例如這個字符「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
完用戶傳入的字符串後,更簡單的索引到對應的字型和字體信息。
固然,其餘還會有一些工做,包括
這些目前實現上遇到的問題不大,篇幅緣由也就不說了。具體的代碼能夠在 Github 上看到。
實現可複用的控制檯「藝術字」功能,總的來講並無太多複雜的點,總體的流程模型就是
生成字體庫 --> 字體排版 --> 渲染文本
這對於前端來講應該是很是好理解的。
作這個項目也確實是本身在工做中但願給一些庫加上這種 logo 或者 banner 展現,但每次重複枯燥的工做確實使人反感。因此想了下可行性以後就搞了 yoo-hoo 這麼個小玩意兒,若是你們也遇到相似的問題,但願能有所幫助。
npm i yoo-hoo
複製代碼
目前 yoo-hoo@1.0.x 內置了一套 26 個字母(A-Z)、10 個數字(0-9)、·
*
-
|
這些字符的字體庫。
考慮到單一的字型和有限的字體量確定不能知足全部需求,因此開發時代碼結構就留下了支持外部擴展的模式。
後續能夠把 2.2 節中的字體源文件解析工具獨立出來,支持用戶「手繪」本身的字型,用工具生成對應格式後,將字體的 JS 模塊傳入 yo
方法中做爲擴展字體加載。
字體源文件的「手繪」雖有成本,但所見即所得,編寫難度不大 🐶 同時也算是一勞永逸。