课程 16 - 文本的高级特性
在上一节课中,我们介绍了基于 SDF 的文本渲染的原理,也尝试使用了 ESDT 和 MSDF 提升渲染质量,另外也提到过 CanvasKit 相比 Canvas 提供的文本高级绘制特性。
在本节课中,我们首先会来看看 SDF 之外的绘制方式,然后将讨论并尝试实现这些特性:装饰线、阴影、文本跟随路径,最后文本不光要能渲染,也要有良好的交互,我们讲讨论输入框、文本选中以及 A11y 这些话题。
首先我们来看看除了 SDF 之外,还有哪些文本渲染方式。
使用贝塞尔曲线渲染文本
使用 Figma 的导出 SVG 功能可以发现,它的文本也是使用 Path 渲染的。如果不考虑渲染性能和 CJK 字符,使用贝塞尔曲线渲染文本确实是不错的选择。为了得到字符的矢量信息,在浏览器环境可以使用:
- opentype.js
- use-gpu 使用的是基于 ab-glyph 封装的 use-gpu-text
- 越来越多的应用使用 harfbuzzjs,详见:State of Text Rendering 2024。font-mesh-pipeline 是一个简单的示例
opentype.js
ts
opentype.load('fonts/Roboto-Black.ttf', function (err, font) {
const path = font.getPath('Hello, World!', 0, 0, 32); // x, y, fontSize
});
harfbuzzjs
ts
import init from 'harfbuzzjs/hb.wasm?init';
import hbjs, { HBBlob, HBFace, HBFont, HBHandle } from 'harfbuzzjs/hbjs.js';
init().then((instance) => {
const hb = hbjs(instance);
});
装饰线
阴影
Pixi.js 提供了 [DropShadowFilter] 来实现阴影效果。
glsl
// @see https://github.com/soimy/pixi-msdf-text/blob/master/src/msdf.frag#L49
vec3 shadowSample = texture2D(uSampler, vTextureCoord - shadowOffset).rgb;
float shadowDist = median(shadowSample.r, shadowSample.g, shadowSample.b);
float distAlpha = smoothstep(0.5 - shadowSmoothing, 0.5 + shadowSmoothing, shadowDist);
vec4 shadow = vec4(shadowColor, shadowAlpha * distAlpha);
gl_FragColor = mix(shadow, text, text.a);
文本跟随路径
在 Figma 社区中,很多用户都在期待这个特性,例如:Make text follow a path or a circle
在 SVG 中可以通过 textPath 实现,详见:Curved Text Along a Path
html
<path
id="curve"
d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97"
/>
<text width="500">
<textPath xlink:href="#curve"> Dangerous Curves Ahead </textPath>
</text>
Skia 提供了 MakeOnPath
方法,详见 Draw text along a path:
ts
const textblob = CanvasKit.TextBlob.MakeOnPath(text, skPath, skFont);
canvas.drawTextBlob(textblob, 0, 0, textPaint);
在 Mapbox 中沿道路河流放置 label 是很常见的场景,详见 Map Label Placement in Mapbox GL
TeX math rendering
更友好的交互方式
输入框
目前我们只实现了文本的绘制,实际在应用中,文本输入框是必不可少的。下图来自 Figma
文本选中
特殊效果
加载 Web 字体
ts
import WebFont from 'webfontloader';
WebFont.load({
google: {
families: ['Gaegu'],
},
active: () => {
const text = new Text({
x: 150,
y: 150,
content: 'Hello, world',
fontFamily: 'Gaegu',
fontSize: 55,
fill: '#F67676',
});
},
});