本文记录一下如何基于字体实现汉字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%System32WindowsPowerShellv1.0powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%chocolateyin"
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://**.**.**.**'