
本文記錄一下如何基於字體實現漢字svg描點的過程
參考項目:
1、makemeahanzi https://github.com/skishore/makemeahanzi
2、hanzi-writer-data https://github.com/chanind/hanzi-writer-data
3、hanzi-writer https://github.com/chanind/hanzi-writer
生成dictionary.txt和graphics.txt文件
生成這兩個文件我們需要用到makemeahanzi
運行Makemeahanzi-tools
首先下載makemeahanzi項目的tools分支
該項目是用meteor框架寫的,那么我們想要運行該項目,需要安裝Node環境和Meteor。
安裝Node
直接上官網懟吧 傳送門:https://nodejs.org/en/download/,安裝完成后記得配置一下環境變量,cmd輸入node -v 顯示版本號即為安裝成功。
安裝Meteor
說到安裝Meteor,那么Meteor是什么?以下內容來自 https://www.w3cschool.cn/discovermeteor/rbj81jjm.html
Meteor 是什么?
Meteor 是一個構建在 Node.js 之上的平台,用來開發實時網頁程序。Meteor 位於程序數據庫和用戶界面之間,保持二者之間的數據同步更新。
因為 Meteor 是基於 Node.js 開發的,所以在客戶端和服務器端都使用 JavaScript 作為開發語言。而且,Meteor 程序的代碼還能在前后兩端共用。
Meteor 這個平台很強大,網頁程序開發過程中的很多復雜、容易出錯的功能都能抽象出來,實現起來很簡單。
為什么使用 Meteor?
那么,你為什么要花時間學習 Meteor,而不去學其他框架呢?撥開 Meteor 的各種功能,我們認為原因只有一個:因為 Meteor 易於學習。
而且,和其他框架不同,使用 Meteor,幾小時之內就能開發出一個正常運行的實時網頁程序。如果之前做過前端開發,對 JavaScript 已經有所了解,甚至都不用再學習一門新的編程語言。
Meteor 可能就是你要找的理想框架,當然,也可能不是。既然只要幾晚或一個周末就能上手,為什么不試試呢?
因為丫自帶一個MongoDB,可以前端后端一把梭,再也不用和后端攻城獅因為接口文檔撕逼了。
安裝環境參考Meteor官網 傳送門:https://www.meteor.com/install,
linux下使用命令
curl https://install.meteor.com/ | sh
windows環境下使用命令
choco install meteor
PS:WTF???choco是什么鬼?
choco其實是Chocolatey的命令,Chocolatey是windows的包管理工具,類似yam和apt-get
如果沒有安裝Chocolatey,那么需要先安裝Chocolatey。傳送門:https://chocolatey.org/install
依照文檔有兩種安裝方式,cmd和powershell
cmd安裝:
@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
powershell安裝:
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
PS:如果安裝失敗,可能是網絡問題,需要梯子
安裝成功后再次執行choco install meteor,等待安裝完成即可
至於Meteor如何使用,可以閱讀官方文檔,這里不細說了,因為我也得看官方文檔。傳送門:https://guide.meteor.com/index.html
運行項目
Node和Meteor安裝完成后,我們開始運行項目
項目目錄:

運行項目之前先把public文件夾拷貝到.meteor文件夾里面,VSCODE打開項目,輸入meteor,等待項目跑起來,然后瀏覽器輸入 http://localhost:3000#, 進入到server文件夾下輸入meteor mongo可以打開mongo,mongo地址一般是localhost:3001端口

如何使用
你可能在使用的過程中會發現和我截圖的有點不一樣,比如只有AR PL KaitiM GB 和 AR PL UKai 這兩個字體,是因為我在原有代碼上進行了一些修改,后面我會講到如何修改代碼使之兼容任意字體。
在#后面輸入漢字然后回車,例如http://localhost:3000/#貓

這時候點擊一下字體,你可以生成對應字體的文字,下面是我點了FZKTJW的效果

那么現在是時候講講如何真正的使用了,他的使用方式是------->快捷鍵,哈哈哈哈。
快捷鍵a:顯示上一個字
快捷鍵A:顯示上一個未完成的字
快捷鍵q:顯示上一個完成的字
快捷鍵d:顯示下一個字
快捷鍵D:顯示下一個未完成的字
快捷鍵e:顯示下一個完成的字
快捷鍵r:重置當前操作
快捷鍵s:下一步操作
快捷鍵w:上一步操作
完成一個字需要六步操作,分別為:path, bridges, strokes, analysis, order, verified
path:路徑

bridges:標記描點

strokes:筆畫
通過更改描點可以控制筆畫

analysis:分析
分析字的結構,偏旁部首等待,如果結構沒有完成,下一步會先去完成結構

對於“貓”這個字需要我們先完成“犭”和“苗”的構建,按s進入下一步

我們完成了“犭”的部分,接下來按D回到未完成的“貓”繼續下一步

按s進入下一步完成苗的構建

此處跳過N部,總之構建完所有依賴的結構后按D回到未完成的“貓”繼續搞

按s進入下一步
order:順序,筆順
這里可以調整筆畫的順序(直接拖動)和筆順Reverse是調整方向

verified:檢查並且完成

到此我們完成了對於貓這個字的所有構建,已經可以導出路徑文件了,那么如何導出路徑文件呢?
導出路徑文件和生成svg文件
導出路徑文件
打開F12,切換到Console,然后輸入
Meteor.call("backup");
進行一下備份,然后輸入
Meteor.call('export');就可以在項目文件夾里面的.meteor文件夾下生成dictionary.txt和graphics.txt文件,稍后再講這倆文件怎么用
生成svg文件
F12,Console輸入
Meteor.call('exportSVGs');
在項目文件夾.meteor文件夾下會生成.svg文件夾,里面就是字體的svg文件
PS:如果生成的svg文件是空的,那么需要修改一下代碼,將lib/animation.html 文件復制到private文件夾下並且覆蓋原文件
源碼分析
上面講了一大坨,這里講一下具體的實現原理
首先是項目結構:

.meteor:運行時的根目錄
.vscode:vscode的配置文件
client:客戶端文件
dist:項目構建后的目錄
lib:公共庫文件
packages:npm包
private:私有目錄
public:公開目錄,里面存放字體和漢字語言庫,這個文件夾需要拷貝到.meteor里面
server:服務器文件
項目里面的源碼文件有很多,下面說幾個用到的
1、client/editor.js對應的是編輯頁面的邏輯,快捷鍵,操作步驟定義都在這個文件里。
2、client/lib/path.js對應的是生成路徑的邏輯,這里可以自己添加字體。添加字體的方法如下:
Template.path_stage.helpers({
alternative: () => Session.get('stages.path.alternative') || '?',
options: () => [{font: 'arphic/gkai00mp.ttf', label: 'AR PL KaitiM GB'},
{font: 'arphic/UKaiCN.ttf', label: 'AR PL UKai'},
{font: 'arphic/simhei.ttf', label: 'simhei'},
{font: 'arphic/FZKTJW.TTF', label: 'FZKTJW'},
{font: 'arphic/FZLanTingYuanS-B-GB-TEST.ttf', label: 'FZLanTingYuanS-B-GB-TEST'},],
});
在options里面添加好字體后,將字體文件拷貝到.meteor/public/arphic/文件夾下即可
修改path.js使之兼容各種字體
本項目中用到的字體大小是1024的,如果一般字體沒有這么大,比如256的,那么生成出來的字會很小,不能沾滿整個畫布,那么坐標也是無法使用的。我們可以修改path.js中的
// We avoid arrow functions in this map so that this is bound to the template.
Template.path_stage.events({
'blur .value': function(event) {
const text = $(event.target).text();
const value = text.length === 1 && text !== '?' ? text : undefined;
if (value === stage.alternative) {
$(event.target).text(value || '?');
} else {
stage.alternative = value;
stage.forceRefresh();
}
},
'click .option': function(event) {
const label = this.label;
const character = stage.character;
assert(character.length === 1);
Session.set('modal.text', `Loading ${label}...`);
Session.set('modal.value', 0);
opentype.load(this.font, (error, font) => {
stage.alternative = undefined;
if (error) {
stage.onGetPath(`Error loading ${label}: ${error}`);
return;
}
Session.set('modal.text', `Extracting ${character} from ${label}...`);
Session.set('modal.value', 0.5);
const index = font.charToGlyphIndex(character);
const glyph = font.glyphs.get(index);
if (glyph.unicode !== character.codePointAt(0)) {
stage.onGetPath(`${character} is not present in ${label}.`);
return;
}
const commands = font.getPath(character,0,0,1024).commands;
// TODO(skishore): We may want a try/catch around this call.
// const path = svg.convertCommandsToPath(glyph.path.commands);
const path = svg.convertCommandsToPath(commands);
stage.onGetPath(undefined, path);
});
},
});
紅色部分即為修改的部分,這時候字體大小就會變成1024的大小,那么接下來會發現字體是反的,這時候需要修改/client/lib/external/opentype/0.4.10/opentype.js
Glyph.prototype.getPath = function(x, y, fontSize) {
x = x !== undefined ? x : 0;
y = y !== undefined ? y : 0;
fontSize = fontSize !== undefined ? fontSize : 72;
var scale = 1 / this.path.unitsPerEm * fontSize;
var p = new path.Path();
var commands = this.path.commands;
for (var i = 0; i < commands.length; i += 1) {
var cmd = commands[i];
if (cmd.type === 'M') {
p.moveTo(x + (cmd.x * scale), y + (cmd.y * scale));
} else if (cmd.type === 'L') {
p.lineTo(x + (cmd.x * scale), y + (cmd.y * scale));
} else if (cmd.type === 'Q') {
p.quadraticCurveTo(x + (cmd.x1 * scale), y + (cmd.y1 * scale),
x + (cmd.x * scale), y + (cmd.y * scale));
} else if (cmd.type === 'C') {
p.curveTo(x + (cmd.x1 * scale), y + (cmd.y1 * scale),
x + (cmd.x2 * scale), y + (cmd.y2 * scale),
x + (cmd.x * scale), y + (cmd.y * scale));
} else if (cmd.type === 'Z') {
p.closePath();
}
}
return p;
};
修改紅色部分。這時候文字就會顯示正常。
OpenType.js
這里簡單介紹一下opentype.js 傳送門:https://github.com/opentypejs/opentype.js
opentype.js是TrueType和OpenType字體的JavaScript解析器和編寫器。
它可以讓我們從瀏覽器或Node.js 訪問文本的字體,具體使用方式直接看github就可以了,上面的說明還是比較清楚的。
使用dictionary.txt和graphics.txt文件
如何使用這兩個文件,我們需要用到hanzi-writer-data 和hanzi-writer
運行hanzi-writer-data
下載hanzi-writer-data,解壓后的目錄結構:

打開stroke_data_parse.py文件可以看到
dictionary_file = os.path.join(root, 'vendor/makemeahanzi/dictionary.txt')
graphics_file = os.path.join(root, 'vendor/makemeahanzi/graphics.txt')
他是使用dictionary.txt和graphics.txt生成單個字獨立的.json文件,以供hanzi-writer使用
我們將剛才生成的dictionary.txt和graphics.txt拷貝到vendor/makemeahanzi/目錄下
然后打開cmd輸入python stroke_data_parse.py 會看到data文件夾下有生成好的json文件

打開文件的時候需要設置utf8編碼,參考修改:
with open(dictionary_file,'r',encoding='UTF-8') as f:
lines = f.readlines()
for line in lines:
decoded_line = json.loads(line)
dict_data[decoded_line['character']] = decoded_line
with open(graphics_file,'r',encoding='UTF-8') as f:
lines = f.readlines()
for line in lines:
decoded_line = json.loads(line)
char = decoded_line.pop('character')
graphics_data[char] = decoded_line
和
# write out data
for char in graphics_data:
radical = get_radical_strokes(char)
if radical:
graphics_data[char]['radStrokes'] = radical
for char, data in graphics_data.items():
out_file = os.path.join(output_dir, f'{char}.json')
with open(out_file, 'w',encoding="UTF-8") as f:
f.write(json.dumps(data, ensure_ascii=False))
with open(os.path.join(output_dir, 'all.json'), 'w',encoding="UTF-8") as f:
f.write(json.dumps(graphics_data, ensure_ascii=False))
PS:等等 沒有python 命令怎么辦? 簡單啊,裝Python,記得裝3x以上的版本
Python安裝傳送門:https://www.python.org/getit/
hanzi-writer
我們來看看怎么利用上面生成的json文件吧
將剛才生成的json文件拷貝到hanzi-writer項目的demo文件夾,將里面的地址替換成剛才生成的all.json數據文件,然后打開后查找剛才我們自己構建的“貓”

嗯,到這里就全部完成了,開心的擼吧
部署
meteor build dist --architecture=os.linux.x86_64
tar xvf meteor-build-test.tar.gz
export MONGO_URL='mongodb://**.**.**.**:27017/makeahanzi'
export PORT=3000
export ROOT_URL='http://**.**.**.**'
