本文作者:ziven27
原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。
背景简介
Inkstone 是一个面向海外原创作家和翻译的创作平台。大量的作家和作品,意味着大量的书封制作需求。这之前是需要设计师投入精力,或者花钱购买来解决的,淘宝的 5 元书封,说便宜也不便宜,和用户自己上传的书封一样,不保证版权方面没有问题,存在着各种风险。而此时浏览器端书封制作系统成为了我们解决这个需求的一大利器。作家只需要登录我们 Inkstone「 海外作家创作平台 」,短短几步就能制作出一个精美的有版权书封。
海外书封系统主要分为纯色背景和图片背景两条线。纯色背景需要四步,图片背景需要五步就可以完成我们的书封制作。
这一切也早已在去年国内起点作家助手,张鑫旭老师带领下实现了中文版的书封制作系统。而我们海外测这边思路和国内是一样的,但是还是有很多非中文体系下的难点需要做额外的努力。
对于中文版书封系统,张鑫旭老师在他的博客上已经有很多相关内容给大家介绍。本文着重会在区别于国内书封的几个点地方给大家介绍:
- 书封分层逻辑
- 图片滤镜缓存机制
- 英文字体
- 英文分行
- 英文行高
- 英文对齐方式
- 文本颜色
一、书封分层逻辑
最开始拿到这个需求,第一反应就是将不同的图层对应成我们 DOM 的分层,然后直接将处理好的 DOM 导出为图片即可。虽然用 DOM 操作虽然对文本的处理会比 Canvas 优秀很多,但是在图像处理这一块还是 Canvas 可能性更高。比如要实现一个文字需要同时叠加图片纹理和背景颜色的这件事,基于 DOM 目前我好像也没有找到合理的解决方案。
所以选择了如上图所示的层级模式。背景层就是 Canvas 叠加滤镜之后的 Base64 图片,中间层是用 Canvas 直接绘制的文字「 书名 + 作者名 」,最上面一层是 LOGO 和水印的图片。 当用户点击上传按钮的时候,再用 Canvas 将这个三个层级叠加成我们想要的书封,提交到我们的服务器。
二、图片滤镜缓存机制
对于图片滤镜处理的相关技术方案大家可以去张鑫旭老师博客上搜索关键词:滤镜。这边会着重在滤镜缓存优化机制上给大家介绍。
为了体现优质的滤镜效果,和国内书封一样选择的是电影级别的 3D LUT 滤镜。可是这种级别的滤镜,大部分文件大小都接近 1M,如果在用户每次点击按钮之后,才去加载对应文件再渲染,这整个等待时间是很长的。并且更让我吃惊的是,渲染滤镜是一个可能比加载还要花时间的过程。
海外前端需要生成的书封的尺寸是 8101080,而这也就对应着 874, 800 个像素点,而这种 3D LUT 滤镜几乎是需要去处理每一个像素点,可想而知这整个计算量是非常大的。再看看我们在预览状态下的书封尺寸其实是只有 240320 的,用户在点击切换滤镜的时候,并不表示他一定会用这张图,有可能真的只是看看。
单位: ms
所以这边的处理方案是在预览状态下,只会去处理 480*640「 2倍图 」这个尺寸的书封。从上图可以看到我们整体的平均加载时间,减少了近 63% 的时间。
当用户真正点击上传的时候,才去对原始尺寸的图片做滤镜处理。而此时,因为滤镜文件已经加载过了,相当于只需要滤镜渲染的时间,一举两得。
然后我们再来看看预加载逻辑,当用户进入页面的时候默认是没有应用滤镜的。在这个时候会预先加载第一个滤镜,加载完成渲染,然后加载第二个滤镜,加载完成渲染。渲染好的图片会转成 Base64 的 URL,放到右侧 IMG 列表里「 这个列表在生产环境会隐藏 」。当用户点击切换滤镜按钮的之后,只需要从右侧列表中拿出对应序号的 URL,替换左侧的展示区域中的背景图片的 URL 即可。
这样做相当于我们总是先于用户两步,去尝试处理图片的滤镜效果。最大限度的减少了用户等待的时间。甚至当用户走完一圈之后,切换滤镜就等于交换两张 Base64 图片的 URL,没有加载,也没有滤镜处理,完美。
三、英文字体
3.1 字体加载方案
国内对于中文字体的处理我们用自建字体服务 Y-font 「 阅文中文字体接口服务 」支撑,可以按需加载书封用到的几个文字的字体。但是海外和国内是不共享服务器的,导致于我们直接没法使用这套服务。不过好在和中文字体比起来英文字体本身就比较小,再加上 Google Font
的加持,即使全量加载也不会有太大的问题。
海外书封的字体列表本身还和用户选择的作品分类有关系。一开始我是给每个分类都准备了一个存放字体列表的 CSS 文件,当用户切换作品分类的时候,切换对应的 CSS 文件。然而后面发现这个是我想多了。
对于字体浏览器是有自己的处理逻辑了。简单的说就是,页面中如果没有任何一个文字设置了对应的字体,即使你引入了字体的 @font-face
这个字体也不会被加载。当你给某个文字设定了font-family
之后这个字体文件才会加载。当这个字体文件加载成功之后,这个设置了对应font-family
的文字会重新进行绘制。
所以最后我将不同分类的字体文件都合并成了一个,因为不同分类下的字体是有重叠的,反而让整体的字体 CSS 文件变小了。后续通过给文字设置 font-family
来控制字体的加载。
如图所示,我会在页面中直接输出用户选择分类下的字体列表,并只给前五个的文字设置font-family
也就是说我预加载了这五个字体。因为用户选择分类是在第一步,切换字体是在第最后一步,所以当用户到达第五步的时候,这五个字体可能早就已经预加载好了。
当用户切换字体的时候,我会从用户选择的字体序号开始,给后五个文字设置font-family
,相当于用户真正切换字体的时候只加载一个字体「有四个已经预加载好了」。然后用户在这切换字体的整个过程中,也几乎不会感受到字体的加载。这个预加载逻辑和之前处理滤镜的方式如出一辙。
更值得一提的是,这种方式还解决了无法知道字体文件什么时候加载成功的逻辑。
3.2 字体渲染优化
我们都知道存在 DOM 标签中的文字,如果是动态加载的字体,当字体文件加载好之后,设定了相关 font-family
的字体会自动重新渲染成对应的字体。
但是绘制在 Canvas 中的文字,在字体文件加载后,需要手动触发渲染。可是犯难的问题在于,我们如何才能知道一个字体文件是否加载成功?对于这个问题是有一个 「 Web Font Loader 」的库可以解决的。然而当我们采用了预加载五个字体的逻辑之后,这个库的意义就不大了。因为用户在切换字体的时候,下一个字体可能早就已经加载完成,也就不会出现当前字体动态加载的问题。
当然有一些极端的情况就是用户在切换字体的时候按钮多次点击,并且这个速度超过了我们预加载 5 个字体文件的速度。或者在某些网络相对没有那么好的地区,Google Font
加载比较慢。这个时候会出现当前字体对应不上「 会显示默认字体 」。对于这个问题我们的处理的方式是不处理。
因为用户其实并不知道哪个编号对应哪个字体,即使出现了字体编号和字体不匹配的问题,用户也是很难发现的,可能会以为这个编号的字体就是和默认字体长得很像。当用户点完一圈再次回来的时候,这个字体就很有可能加载好了。
四、英文分行
4.1 canvas 文字绘制方式
context.fillText(text, x, y [, maxWidth]);
canvas
文本绘制API
text
:需要绘制的文本x
: 开始绘制的横坐标y
: 开始绘制的纵坐标maxWidth
:文本显示宽度(文本放不下会水平方向压缩)
用 Canvas 绘制的文本,并不会像 DOM 标签那样超出之后会自动换行。想要实现自动换行,需要手动计算,然后追行绘制。比如你有一行文字 你是我的小苹果
, 文字大小是 16px
,行高是24px
,Canvas 画布的宽度是 4*16px
。就需要将按照每行最大 4 个字为一组逐行绘制。
context.font = '16px STheiti, SimHei';
context.fillText('你是我的', 0, 0);
context.fillText('小苹果', 0, 24);
4.2 文字换行方案
图1 / 图2 / 图3
国内和海外书封,最大的不同就是对于文字的处理。而这不同的原因来自于中文和英文本身的差异。 简单的说就是中文可能一个成语搞定的事,英文需要一整句话。
所以多数情况下,作者的书名一行是放不下的。于是我们只能反向思考,基于画布的宽度,和一行文本的个数动态去计算文字的大小「 图1 」。
字号 = 取整(画布宽度/一行单词个数);
当用户点击底部的空格区域添加换行符的时候,选取字数最多那一行作为我们上述公式的被除数,用来计算我们的基础字号,其它行也基于这个字号进行绘制。
当然我们计算的字号不可能会无限大,所以我们会给基础的字号一个最大值,当计算出来的基础字号超过这个最大值的时候,我们会以这个最大字号作为我们的基础字号「 图3 」。
然而这就会出现另外一个问题了,就是大多数的英文书名一行显示的时候,这个动态计算的文字的字号都偏小。这就直接导致,大多数用户在刚进入这个页面的时候,都会看到这个并不优雅的状态「 图1 」。
为了解决这个问题,在用户首次进入文字排版的页面时,会自动在文本正中间的最近的空格处,默认帮用户添加一个换行符。虽然并不是完美的解决,但至少能保证大多数的用户首次进入的文本可读性「 图2 」。也能告知用户,我们的交互形式是利用底部空格进行换行,是一个一举两得的办法。
五、英文行高
单行文字起始点纵坐标 = 基准点起点纵坐标 + 行号 * 行高;
前面有提到,我们在绘制 Canvas 文字的时候,还需要提供起始点的横纵坐标。在字号相等的情况下我们的起始点纵坐标只需要通过上述公式就可以实现。
行高 = 字号 * 1.5;
本来一开始想通过这个公式来处理文字的行高「 这是网页中最常用对于行高的处理方式 」。然而在英文这个错综复杂的字体系列下,这个公式显得是那么的苍白。
因为英文不像中文那样是四四方方的,英文中有视觉上偏上的字母 「 l 」 , 也有视觉上偏下的「 g 」 。即使是同一个字体,同样的字号行高下,你也可以看到,第一列的文字发生了重叠,第二列显示还好。所以在大小写混排的状态下,我们是很难给到某一个具体到行高来规避这样的问题。
此时细心的同学可能发现,后面的两列因为都是大写好像这样的问题就好了很多。所以我们和设计师约定书封标题默认都是大写,然而问题还没有结束。
因为我们的字体用户是可以自由替换的。即使其它条件都一致的情况下,用户看到的效果仍然可能会是千差万别。
行高 = 字号 * 1;
在经过多次的调整和视觉对比之后,我们最终选择了这个公式来作为我们行高的计算方式。当然这中间也麻烦设计师舍弃了一些特别违和的字体。
六、英文对齐方式
本来这个对齐方式如果只有左中右三种方式的话是没有什么好讲。因为 Canvas text-align
API 自带这三种对齐方式。
难就难在第三个 AUTO 的这种对齐方式,这其实是一种类似海报中常用的艺术表现手法,上面的四个图都是在 AUTO 模式下,只是调整了文字的换行实现的。可以看到这个效果是比左中右这三种常规方式更加生动的「 为优秀的设计师点赞 」。
相信大家通过上面的示意图也可以看出其中的逻辑。就是我们每一行的字号是基于上一节提到的公式单独计算的。简单的说就是每行字数越少字号越大「 当然会有一个最大值 」。
单行文字起始点纵坐标 = 基准点起点纵坐标 + 之前每一行的行高;
然而现在因为每一行都是单独计算,所以每一行的字号都不一样,对应的行高也不一样,就得把所有行高都纪录下来,在逐行绘制。所以在 AUTO 模式下文字起始点的纵坐标就变成了上述公式。
七、文本颜色
7.1 取色方案
图1 / 图2
对于文本颜色,一开始我以为,只需要设计师给我一个文字颜色列表就可以了。然而事实还是想得太简单了。
比如设计师给我的颜色列表第一个颜色是白色,当用户选择了如图2那种偏向纯白的背景,如果还是用白色作为我们文字的颜色,就几乎看不清的。所以我们需要有一个逻辑去针对不同的背景切换默认的文字颜色。
这边给大家推荐的一个库是 Color Thief。如上图所示,就是给这个库的 API 提供一张图片,它就会返回给你这张图的配色表。并且这个配色表的长度你是可以定制的。
const isWhite= (r + g + b) / 3 < 128*1.3? false: true;
我们怎么基于这个库去判断一个背景图是偏深还是偏浅呢?很简单,我们取出这个配色表的第一个颜色,也就是这张图片中的主色。让后将这个颜色的 rgb 的色值取一个平均值,如果这个平均值小于 128*1.3 我们就近似认为这个图是偏深。大家可能会问,128 不才是256 的一半吗 ?为啥我们这里还要再乘以 1.3 。
其实这个很简单,在 rgb 平均值在 128 附近的时候,白色和黑色文字其实都是可以看得清的。但是在这个边缘,我们更希望用户是看到的是白色文字。至于为啥是 1.3,其实就是我们自己一张图一张图去试,大概觉得在这个阈值下,比较符合我们期望的效果。
这里 Color thief 对于图片配色表的计算和之前滤镜逻辑有着类似计算逻辑,就是我们提供的图片尺寸越大,这个计算的时间就会越长。本来我们这里要的就是一个模糊的值,所以我们提供给 Color thief 的也是一个缩略图。
7.2 颜色列表
到了这里,我们只是粗略的解决了用户字体设置页面的默认字体颜色问题。对于整个颜色列表要怎么设置也是一个问题。
文字颜色列表 = 黑+白+8个配色+4个百搭颜色
我们用 Color thief 取整个图片的 9 个配色,然后用第一个颜色作为我们评判默认文字颜色是黑是白的标准。然后加上剩于的 8 个配色和设计师给到的 4 个百搭的颜色,组成我们的文字颜色列表。
大家可能会好奇,这里为啥我们直接用背景的配色作为了我们文字的颜色?这个方案是,张鑫旭老师在国内书封项目中提出的。
不难理解,就是图片配色里面的颜色和图片本身搭配才不会有太大的违和感。其它的颜色,有可能单独看会很好看,但是放到一个不搭的背景里面反而会显得奇怪。所以后面设计师给到的颜色也是选的比较百搭的颜色。
总结
篇幅关系,这边着重挑出了几个我认为值得给大家分享的技术难点。另外还想给大家分享的是一点点项目心得。
刚拿到项目的时候,其实我最担心的是图片的滤镜和图片纹理的叠加不知道要怎么实现。因为这部分在我之前的项目经验里面是空白。然而在我实际使用的时候,这部分其实基本上就等同于调用一下 API 而已,当然这个和因为有张鑫旭老师的加持也有很大的关系,几乎这方面的问题都能在他的博客中找到。真正比较难的还是在想要提升用户体验这个点上。
一开始因为自己的一些既定思维,误以为在浏览器端处理英文应该会比中文容易很多。毕竟中文字体动辄几兆的文件大小在那儿摆着。然而在实际开发中,才知道因为英文语言特性,对于这种需要精细化处理的地方会有很多的坑。简单的说,在某种字体下,你这整个逻辑都可能被推翻。在这一点上,可能还是要略微舍弃技术追求,和设计师商量,看看是否可以舍弃掉这样的字体。
技术永远只是你从 A 点到 B 的的工具。到达 B 点才是你核心的目的。