小而美的字帖应用——《田农汉帖》开发日志

有一天我那个喜欢书法的大舅问我说,怎样在电脑上把印刷体弄成颜体?仔细询问目的后得知他想要一款能在平板电脑上用的能自定义字体、内容并生成字帖的软件。我闲得蛋疼,就自嗨写了一个《田农汉帖》
田农汉帖
源代码: gitee:tiannong/hantie

首先声明我真的不懂编程,甚至不会写javascript,对设计模式、开发之类更是一窍不通,只会复制粘贴,所以下面的内容没什么太大价值。总的来说只有一句话要说,就是软件开发不仅是技术活,也是个艺术活。

那我就啰嗦地讲下这个应用方方面面的摸索开发过程。

前期准备

确定需求

首先从确定需求开始。很简单,就是整一个能生成字帖的工具。大概是田字格那种字帖,上面写一些诗词之类。反正至少得自定义内容。自定义字体。

同类应用对比

然后就想上网看下现成的在线字帖工具是怎样的。哎呀,中文互联网真是乱七八糟,还要微信登录,广告满天飘。于是赶紧去App Store看了一眼,其实上架的字帖app不算少,功能也很完善,能想到的内购内容人家也都想到了。有的甚至还支持临摹。那我做的这玩意儿有什么用,不是重复造轮子吗?

技术选型

考虑到我写的这一坨代码的第一名用户他有一个平板电脑(不清楚具体型号),也不方便问是什么操作系统,并且 我不打算为不同操作系统单独写app,何况万一以后还会在PC上用这个软件,所以还是偷懒用HTML5。HTML5有一个好特性就是canvas。

编程目标是小、简洁美观、轻量化,确保在512MB内存的古董机子上也能跑, 所以找来找去用了MDUI——一个Material Design风格的超轻量级响应式高性能前端界面库。这玩意组件全面,不依赖第三方库而且编码门槛低

俗话说天下代码一大抄(并不),接着去Github上找了一圈,有一个很简陋的字帖生成器原型,很好,就是在canvas上渲染的。单HTML文件,代码量不到两百行,最后效果居然还不错。就拿这个改了。

界面设计和交互逻辑

既然用了Material Design的前端库,那照着Ehviewer抄吧。反正都长差不多。MDUI的组件真的很丰富,就是徒眼看元素缩进有点晕。
第一版的设计是,左边一个抽屉。分成“设置”、“生成字帖”两个按钮。在设置界面调整各种参数,录入文字,然后在”生成字帖“观看效果。但这样的交互其实很麻烦:为什么还要再点回去才能看到最终的字帖生成效果?(还要忍受抽屉的伸缩动画)为什么不能实时地修改?
所以后面改了一版,弄了个Tab,第一个Tab是各种排版微调设置,第二个Tab就是效果预览。虽然做不到实时预览效果,但好歹只要点一下就能看到最终效果。这样抽屉就空下来了,那就放一个“关于”吧,写一些作者信息,开源组件许可证,捐助链接之类的。

对了,因为没有后端,也就没有渲染模板引擎来处理路径,总不可能a href吧?况且也没有后端程序来在页面间传递参数。我们尽量在一个html文件里面用js来引值传值,设置style。所以抽屉标签页的切换就是通过以下代码完成的:

1
2
3
4
5
6
function show_home() {
var home_content = document.getElementById("home_content")
var about_content = document.getElementById("about_content")
about_content.style.display = "none";
home_content.style.display = "block";
}

The hack works。不优雅但是有效够用。

Less is more

一开始不能设计太多功能——比如分享到朋友圈——这些玩意儿要额外引入和native API交互的东西。但目前最应该做的是完成20%的核心需求,循序渐进地把最重要的部分写完,再去考虑剩下的扩展功能。

前期应该做的事情:

  • 熟悉前端框架的组件用法
  • 弄清楚canvas的渲染流程
  • 一点点增添功能,比如格子样式,字号,字体更换

前期阶段不应该做的事情:

  • 忙着添加各种自定义纸张质感而忽略了主要功能的开发
  • 着手对其他CJK语言的支持
  • 整些没用的功能例如用户登录、分享到朋友圈/微信/微博
  • 太早写开发文档自嗨

这些功能要么会引入累赘的模块让代码复杂性成倍上升,要么会引发著作权/法律问题,或者让自己挖太大的坑而失去填坑的热情。

canvas的那些坑

上面说过了核心代码就是往canvas上渲染东西。就是循环读取每一个汉字,然后计算坐标,画很多canvas来绘制底格和汉字(确保缩放时不会错位)

至于字体,可以通过如下方法指定给canvas:

1
2
3
4
5
6
7
8
9
@font-face {
font-family: "myFont";
src: url("./custom.ttf");
}

const cvs = document.querySelector('canvas')
const ctx = cvs.getContext('2d')
context.font = '30px myFont'
ctx.fillText('测试文本', 50, 50)

每次修改排版设置后,通过按钮的onclick事件call函数来销毁掉所有canvas(注意是直接销毁上次绘制字帖时动态生成的element,不是仅仅擦除画布),再重新开始绘制。简单粗暴。

其实自己本来也想做临摹功能,估计就是接收屏幕触摸事件,在canvas绘制轨迹;搞不好还能做成软笔效果(就像华为手机上的手写输入法的那种笔锋),不过心想这不是给自己挖坑吗,我还得写一大套起落笔算法。虽然之前参与过的画板项目myPaint有现成的软笔笔锋算法。但多一事不如少一事……

还有一个坑是在绘制纸张纹理时,发现怎么死活都画不到canvas上。后来把代码改成这样,等image load结束后再绘制就正常了:

1
2
3
4
ground_texture.onload = function() {
const pattern = ctx.createPattern(ground_texture, "repeat");
ctx.fillStyle = pattern;
}

类似地,字体加载也有这个问题,见下。

字体加载和缓存

大部分浏览器,字体是懒加载的,在@font-face中声明了字体并不意味着下载此字体,只有在使用了这个字体,浏览器才会下载。

对于DOM+CSS来说没有问题,因为页面可以先显示默认的字体,当自定义字体加载完成后,页面自动呈现新字体。但是对canvas来说就不行了,因为在绘制的时候自定义字体还未加载完,那么绘制到画布会使用的默认字体,等自定义字体加载完,画布是不会自动更换像素的。

网络上找了一下有一个类似异步的document.fonts.ready写法,然后发现可以用FontFace,等加载完毕后再回调canvas绘制。

1
2
const myFont = new FontFace('myFont', 'url(./custom.ttf)')
myFont.load().then(font => { document.fonts.add(font)}).then(() => {....})

这些接口在不同浏览器引擎支持程度不一样,最差的当然是IE,当Safari也好不到哪里去。Edge也不咋样。不过Android 的Webview倒是对这个特性支持不错。
在等待的时间里还可以通过显示剩余时间不可预测的进度条(就像Windows设置界面里的用户登录迟缓进度条一样)让使用者对字体资源的加载情形有个数。
之后浏览器应该会形成font cache,无需重复加载了。但我试了一下发现有时候过太久还是要重新加载,国内这网络环境少说也得十秒钟,体验不大好(就算有progress bar)。

字体的版权和繁简问题

由于大部分字体是需要授权的商业字体,稍有不慎容易引发律师函。所以在字体的选择上,应该找那些对个人/家庭使用免费授权的,或者是开源字体(注意 开源≠随便用≠免费)。
字体使用上的规矩还很复杂,比如说把微软雅黑打印出来给人看合不合法呢?拿来做企业的宣传广告合不合法呢?预载到打印机上合不合法呢?私下拷贝、流通合不合法呢?草,我为什么要研究这个。根据微软的《字体限制常见问答》和苹果的《macOS许可协议》来看,情况还很多,还涉及字体作为“软件”和“美术作品” 的不同定义的问题,需要自行反复理解。

网络上找到一段解释:

由于许可证主要禁止将字体「嵌入」到当前系统以外的网页或软件中,只需要弄清楚「嵌入」的概念即可。也就是说如果你使用了某种字体的文件在传输时,使得对方无论是否安装了该字体都能显示出相同的效果,那么说明你「嵌入」了这一字体。因此,在网页设计中通过样式表(CSS)的 font-family 字段指定特定字体并不构成「嵌入」,因为这只是告知客户端尝试调用其本机安装的该字体来渲染页面;如果用户没有安装任何指定的字体,则只会显示出浏览器和/或操作系统规定的某种回退(fallback)字体。类似地,在 app 中指定系统内建的某个字体、由操作系统在运行时从系统资源库中调用,也不构成「嵌入」。相反,如果你将字体文件拷贝到自己的网页服务器上或 app 的资源文件中,然后在代码中直接调用这些字体文件本身,哪怕是原封不动地用着系统内建的字体,也是违反协议的。

谁能想到,浓眉大眼的微软雅黑居然也不能乱用……总之一句话,考虑到潜在风险,最好不要用操作系统自带字体。

好在有很多别的选择。目前向个人用户免费开放字体(有条件)的有方正字库、汉仪字体、新蒂字体、文悦。里面印刷体手写体都有。记得阅读许可协议,确定自己的行为没有违反字体授权。不然对方法务部门可不是闹着玩的。
最后选定的是这几款:

  • 方正柳公权楷书
  • 方正字迹-书体坊勤礼碑颜体-简
  • 汉仪新蒂文徵明体
  • 汉仪程行简
  • 汉仪颜楷W

众所周知Google Fonts被墙了很久,国外其他服务在中国大陆也很不稳定。方正、汉仪的云字库还要注册,太麻烦了。一个原则就是:要在断网的时候也能用。

其他小细节和质感提升

首先是配色。MDUI自带了一套主题配色方案,考虑了强调色、对比度。基本不用人操心,只需要用mdui-theme-开头的一系列类就完事了。主题配色的那一段html我直接从MDUI官网抄过来的。默认界面可以古朴端庄一些,考虑到这是一个字帖App,可以用浅灰-鹅黄,宫红-墨黑这种,深灰不可以看起来不吉利。反正明度一定要低,不能有太廉价的塑料味儿。至于字帖的字体颜色和田字格配色就别用默认的工业配色盘了,可以从“中国传统色”里面选。

另外界面字体可以换方正标雅宋:严肃中带着端庄,简洁不失秀丽,一看就有国学味儿——不认识方正标雅宋的请自行打开学〇强国App观摩一下。 宋刻本字体配Google的Material Design有一种中西结合、跨越千年的木刻纹路和冰冷屏幕碰撞的奇妙质感。

另外快捷方式图标可以用GIMP或者Inkscape绘制一下——美工工具链当然也得用开源的。注意icon的字体的版权。放进HTML的head里。这样添加到桌面/主屏幕 就有漂亮的图标了。(咕咕咕中)

1
2
link rel="shortcut icon" href="图标地址" type="image/x-icon">
<link rel="icon" href="图标地址" type="image/x-icon">

打开时应该留着上次的设置。这个可以用localStorage做到(130KB)

此外,有的字体是简体输入显示成繁体,有的必须是繁体才能显示繁体。里面的字符集门道我也不熟,我猜可以用openCC,在检测到使用繁-繁字体时自动把原文的简体字部分转成繁体;但这种情况很少,就不做这功能了。

本来还觉得安卓滑来滑去的动画效果不够庄重,但懒得改了……

内购点假想

虽然这个应用目前是自娱自乐的,不过还是假想了一下它的付费卖点。

App要让用户付费,最关键的就是让人觉得”我需要这个东西“ ”钱值这个功能“。功能不光指是实用性,还可以是品味、或者人为制造的稀缺性。 这些都在营销教程里讲烂的。这个字帖应用可能设计成内购的地方,比如某些高级的配色/纸张纹理质感,其实没什么卵用,但看上去很漂亮,很中国风,很衬托正统中华方块儿字的灵动气韵,很有国学味,用户就想买。

附加值首页可以弄个“每日字帖”,推送诗词,说得好像买了就有毅力每天在手机上一边欣赏书法一边积累背诵诗词似的。

那还有没别的内购点?比如导出成图片格式(便于打印后临摹)的功能也可以收费,当然懂行的用户会按Ctrl+P,或者screenshot。为此有必要提供一个空心字选项,还好canvas直接就有strokeText()函数。一行代码搞定。

版本迭代和后续维护

web页面的更新很简单,本地调试好了之后直接推送到服务器上即可。(什么webpack,没听说过)
整个程序,包括js、img不到100K。但几个字体加起来就有50M了。

要桌面版或者安卓版,也可以用Electron,node-webkit,QtWebEngine之类打一个包。反正就是浏览器壳里面塞一个webview,(直接用HBuilder之类就行,不用下一个安卓开发全家桶)。iOS就算了,没钱交开发者年费……