目前国内设计师生成图标字体的方法,通常是在设计软件中导出 SVG,再将其上传至 iconfont.cn 网站上,由网站提供的服务生成一套各种格式的字体以及使用文档。这个方式虽然操作简单,但管理太大图标时并不高效,还有无法将图标指定具体某个 unicode,无法修改字体的参数设置,导致该工具并不能用于生成简单的数字、字母或特殊字符等字体。若用 Glyphs 此类的专业字体软件,设计师则需要额外学习很多软件操作和字体知识。在前端开发中使用的一些字体生成工具,对于设计师来说则过于复杂。
这篇文章中将介绍一种使用 opentype.js 来创建可以直接从 Sketch 中导出 OpenType (.otf) 格式字体的插件,适用于图标及简单的数字、字母或特殊字符等字体。插件中并未考虑例如连体等高级功能,仅是将 Sketch 中的元素转为字体文件中的字形。
相较于 iconfont.cn 网站,这种方式可以不需要离开 Sketch,不需要导出 SVG 文件,用来设计图标或字体的 Sketch 文件可以直接被当作库分享给其他设计师。然而目前插件只提供 OpenType (.otf) 一种格式,目前此中格式可以被网页端、Android 和 iOS 等平台支持,如果需要其他格式可以使用类似 convertio.co 之类的在线转换工具。
工程化思考
设计师在开始进入某个项目的工作之前,应该对比不同实施方案带来的协作和维护成本,思考整个项目该如何管理,如何协作,是否需要引入某些流程自动化工具,以及后期如何交付和维护。特别是大型的图标字体,并且可能还会将图标生成其他平台的特定格式,这种工程化的思考就很重要,可以带来很多的效率提高。
整个项目使用如下的目录结构。
|- dist/
|- script/
|- main.js
|- mainfest.json
|- opentype.js
|- sketch/
|- README.md
|- install.sh
“sketch” 目录保存 Sketch 源文件;“script” 目录是用来保存导出字体的代码,这个结构并非严格意义上的 Sketch 插件,而是以脚本文件形式附在项目中,这样可以更加定制化,不必考虑插件的通用性;通过运行 “install.sh” 创建一个 Sketch 插件,让导出字体功能出现在 Sketch 插件菜单上,但仍映射到 “script” 目录下的文件,所以每次执行插件都是运行修改之后的最新代码;“dist” 目录则用来保存最终导出的字体文件。整个项目用到的工具,甚至 Sketch 文件的图册结构都比较特殊,有必要将所有的操作细节文档化,方法协作和交付,“README.md” 就起到这个作用。
源文件规范
命名规范
规定好的 Sketch 文档结构,可以更方便脚本获取到字体所需要的数据,这里将所有字符或图标都是一个组件,是因为考虑到文件可以被用做库,在其他文件中引用。在如图的数字字体中组件画板的高宽即为字符的高宽,不一定要像字体软件那样把高都定为 1ooo 或 1024 等等,过大的尺寸会给设计过程带来很多不便,图标字体也是这样,按照常规的 16x16 或 24x24 即可,然后通过脚本来处理这个问题。
为了版本迭代,增加一个文本图层来记录版本号,文字内容格式为 “Version: 1.0”,脚本会处理这个文本图册,把版本信息写入字体。字体的版本号有严格的要求,它看起来更像真实的小数,例如 1.0、1.001,而不能像某些软件那样使用 1.0.1、1.0.001。
将 Sketch 文件的文件名作为字体的名称,字重或样式也写在文件名上,例如 “Myfont_Regular.sketch” 、“Myfont_Light.sketch”。图标字体可以直接用图标的名称,例如 “Myicon.sketch”。最后字体需要的其他信息都写入在脚本中。
设计文字字体时需要在组件的名称上标记字符的 Unicode 编码,如上图将字符的名称和编码用空格隔开。图标则可以在脚本中按顺序分配编码,也可强行指定。
图层规范
在设计文字和图标字体时,都需要将内容的图层合并一个图层,图中给图标加图层样式,不仅是出于库使用时的考虑。一个重要的问题是,由于要将 Sketch 中的路径准确转为字体中字形的路径时,需要将 Sketch 中的图层填充设置为 Non-Zero。而刚好这个设置也保护在样式中,所以通过为图层添加样式的操作,会比给每个图层单独设置,来得更方便。至于图层上是否包含样式和使用何种颜色填充,都与字体生成无关,字体只需要路径的数据,所以另一个问题是要将描边扩展转为填充。
目前在 Sketch 上没有自动检测是否符合某些规范的插件,为了保证最终字体输出无误,需要人工仔细检验是否符合这些规定。
字体生成
下载 opentype.js
字体生成使用 opentype.js 库,从 https://github.com/opentypejs/opentype.js/blob/master/dist 下载 opentype.js,保存到 “script/opentype.js”。
插件安装
创建 “script/mainfest.json”,这是 Sketch 插件必不可少的清单文件,将会在 Sketch 的 Plugins 菜单下增加一个名为 “Export Font” 的菜单,内容如下。
{
"commands": [
{
"name": "Export Font",
"identifier": "export-font",
"script": "main.js"
}
],
"menu": {
"title": "Export Font",
"isRoot": true,
"items": ["export-font"]
},
"identifier" : "com.xxx.export-opentype-font",
"version" : "1.0",
"description" : "Export opentype font from Sketch.",
"author": "...",
"name" : "Export Font"
}
创建 “script/main.js” ,这是 Sketch 插件主文件,临时写一段验证是否安装成功的示例代码,它会在 Sketch 底部弹出一段 “Hello Sketch” 信息。
var onRun = function(context) {
var sketch = require("sketch");
sketch.UI.message("Hello Sketch");
};
创建 “install.sh” 用来将 script 目录下的文件打包成 Sketch 插件。
#!/usr/bin/env bash
pluginFolder="/Users/$(whoami)/Library/Application Support/com.bohemiancoding.sketch3/Plugins/create-opentype-font.sketchplugin"
if [ -d "${pluginFolder}" ]; then
rm -rf "${pluginFolder}"
fi
projectPath="`dirname \"$0\"`"
projectPath="`( cd \"${projectPath}\" && pwd )`" # absolutized and normalized
if [ -z "${projectPath}" ]; then
exit 1
fi
mkdir -p "${pluginFolder}/Contents"
ln -s "${projectPath}/script" "${pluginFolder}/Contents/Sketch"
ln -s "${projectPath}/dist" "${pluginFolder}/Contents/Resources"
打开 “终端” 应用,输入 sh
(带有空格),然后将 “install.sh” 拖到窗口上,按回车运行代码。运行成功会在 Sketch 的 Plugins 菜单下找到 “Export Font” 菜单项。运行 “Export Font” 将会看到 “Hello Sketch” 提示信息。
编写字体生成代码
打开 “script/main.js” 删除原本的测试代码,开始编写字体生成代码。
var onRun = function(context) {
var opentype = require("./opentype");
var sketch = require("sketch");
var util = require("util");
var path = require("path");
var document = sketch.getSelectedDocument();
};
查找为记录版本号增加的格式为 “Version: 1.0” 的文本图层,然后将其内容专为字体版本号,默认版本号为 “1.000”。
var onRun = function(context) {
// 以上省略...
// Font Version
var version = "1.000";
document.pages.forEach(function(page) {
var _version = page.layers.filter(function(layer) {
return layer.type == "Text"
}).map(function(layer) {
return layer.text;
}).find(function(text) {
return /Version:/i.test(text)
});
if (_version) {
_version = parseFloat(_version.replace(/Version:\s?/i, ""))
}
if (!Number.isNaN(Number(_version))) {
version = _version.toFixed(3).toString();
}
});
};
从当前文档名获取字体名称和样式。
var onRun = function(context) {
// 以上省略...
// Font info
var fontName = path.basename(document.path, ".sketch");
var familyName = fontName.split("_")[0];
var styleName = fontName.split("_")[1] || "Regular";
var unitsPerEm = 1000;
var ascender = 800;
var descender = -200;
};
获取当前文档内的所有本地组件,组件名作为字符名称,如果组件名指定了 Unicode 编码,则将其作为字符编码。如果是图标字体没有指定编码则从 Unicode 私人使用区 U+E000–U+F8FF(6400个码位)按顺序指定给图标。接着解析组件内图形的路径信息,转为字符所需要的路径信息,Sketch 的路径数值是一个非常多位的小数,在字体中并不需要那么精确,这里把所有数值都保留 6 位精度。
var onRun = function(context) {
// 以上省略...
// Glyphs
var glyphs = [];
var notdefGlyph = new opentype.Glyph({
name: ".notdef",
unicode: 0,
advanceWidth: unitsPerEm,
path: new opentype.Path()
});
glyphs.push(notdefGlyph);
// Get all local symbols
var symbols = util.toArray(document._getMSDocumentData().localSymbols());
// Private Use Areas U+E000
var pua = 57344;
symbols.forEach(function(symbol, index) {
var [name, unicode] = symbol.name().split(/\s+/);
if (unicode) {
unicode = Number("0x" + unicode);
} else {
unicode = pua;
pua ++;
}
var scale = unitsPerEm / symbol.frame().height();
var advanceWidth = parseInt(symbol.frame().width() * scale);
// Path
var _path = new opentype.Path();
if (symbol.pathInBounds()) {
util.toArray(symbol.pathInBounds().contours()).forEach(function(bezierContour) {
util.toArray(bezierContour.segments()).forEach(function(point, index) {
if (index == 0) {
_path.moveTo(
f(point.endPoint1().x * scale),
f(ascender - point.endPoint1().y * scale)
);
}
if (point.segmentType() == 0) {
_path.lineTo(
f(point.endPoint2().x * scale),
f(ascender - point.endPoint2().y * scale)
);
}
if (point.segmentType() == 2) {
_path.curveTo(
f(point.controlPoint1().x * scale),
f(ascender - point.controlPoint1().y * scale),
f(point.controlPoint2().x * scale),
f(ascender - point.controlPoint2().y * scale),
f(point.endPoint2().x * scale),
f(ascender - point.endPoint2().y * scale)
)
}
});
if (bezierContour.isClosed()) {
_path.close();
}
});
}
// Glyph
var _glyph = new opentype.Glyph({
name: name,
unicode: unicode,
advanceWidth: advanceWidth,
path: _path
});
glyphs.push(_glyph);
});
};
function f(n) {
return parseFloat(Number(n).toFixed(6));
}
创建字体对象。
var onRun = function(context) {
// 以上省略...
// Font
var font = new opentype.Font({
familyName: familyName,
styleName: styleName,
unitsPerEm: unitsPerEm,
ascender: ascender,
descender: descender,
version: "Version: " + version,
glyphs: glyphs
});
};
opentype.js 可以直接用在在浏览器和 node.js 环境下,但将其用在 Sketch 环境中,需要重写一个用于保存字体文件的方法 exportFont
。完成脚本之后,运行 Sketch 的 Plugins 菜单下的 “Export Font”,字体文件自动保存到 “dist” 目录下。
var onRun = function(context) {
// 以上省略...
// Export
var filePath = __command.pluginBundle().url().path() + "/Contents/Resources/" + fontName + ".otf";
exportFont(font, filePath);
sketch.UI.message(fontName + '.otf export done.');
};
// 省略 function f(n) {...}
function exportFont(font, filePath) {
var arrayBuffer = font.toArrayBuffer();
var buffer = new Buffer(arrayBuffer.byteLength);
var view = new Uint8Array(arrayBuffer);
for (var i = 0; i < buffer.length; ++i) {
buffer[i] = view[i];
}
var nsData = buffer.toNSData();
nsData.writeToFile_atomically(filePath, true);
}
字体预览
通过网页的 Web Font 来做字体预览是比较方便的方式,不需要反复安装字体。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Font Preview</title>
<style>
@font-face {
font-family: "MyFont";
font-weight: 400;
src: url("./dist/MyFont_Regular.otf") format("opentype");
}
body {
font-size: 48px;
font-family: MyFont, sans-serif;
}
</style>
</head>
<body>
0123456789
</body>
</html>
图标字体则将 body 内的内容改用 javascript 生成字符。
<body>
<script>
const start = Number('0x' + 'E000');
for (let i = 0; i < 200; i++) {
const text = document.createElement('span');
text.innerHTML = `&#x${(i + start).toString(16)}; `;
document.body.appendChild(text);
}
</script>
</body>