Skip to content

Commit

Permalink
feat: use esdt to enhance sdf-based text rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Jan 12, 2025
1 parent 4dc4a49 commit f0861af
Show file tree
Hide file tree
Showing 20 changed files with 964 additions and 140 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pnpm run dev
## Lesson 15 - Draw text [🔗](https://infinitecanvas.cc/guide/lesson-015)

- What's TextMetrics and how to get it in server and browser side
- What's shaping
- What's shaping? Implement letterSpacing and kerning
- Paragraph layout, wordbreak, BiDi and cluster
- How to generate SDF atlas and use it to draw
- How to use MSDF to improve text rendering quality
Expand Down
2 changes: 1 addition & 1 deletion README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pnpm run dev
## 课程 15 - 绘制文本 [🔗](https://infinitecanvas.cc/zh/guide/lesson-015)

- 什么是 TextMetrics,如何在服务端和浏览器端获取
- 什么是 Shaping
- 什么是 Shaping?实现 letterSpacing 与 kerning
- 处理段落。分段与自动换行、BiDi 和 cluster
- 如何生成 SDF atlas 并使用它绘制
- 如何使用 MSDF 提升文本渲染质量
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/drawcalls/SDFText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export class SDFText extends Drawcall {
}

createGeometry(): void {
const { metrics, fontFamily, fontWeight, fontStyle, bitmapFont } = this
.shapes[0] as Text;
const { metrics, fontFamily, fontWeight, fontStyle, bitmapFont, esdt } =
this.shapes[0] as Text;

const indices: number[] = [];
const positions: number[] = [];
Expand All @@ -85,6 +85,7 @@ export class SDFText extends Drawcall {
fontStyle,
allText,
this.device,
esdt,
);
}

Expand Down
10 changes: 4 additions & 6 deletions packages/core/src/shaders/sdf_text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,21 @@ void main() {
outputColor = texture(SAMPLER_2D(u_Texture), v_Uv);
#else
float dist;
lowp float buff;
#ifdef USE_SDF
// fillColor = texture(SAMPLER_2D(u_Texture), v_Uv);
dist = texture(SAMPLER_2D(u_Texture), v_Uv).a;
buff = (256.0 - 64.0) / 256.0;
#endif
#ifdef USE_MSDF
vec3 s = texture(SAMPLER_2D(u_Texture), v_Uv).rgb;
dist = median(s.r, s.g, s.b);
buff = 0.5;
#endif
float fontSize = u_ZIndexStrokeWidth.z;
lowp float buff = (256.0 - 64.0) / 256.0;
#ifdef USE_MSDF
buff = 0.5;
#endif
// float opacity = u_FillOpacity;
// if (u_HasStroke > 0.5 && u_StrokeWidth > 0.0) {
// color = u_StrokeColor;
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/shapes/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ export interface TextAttributes extends ShapeAttributes {
* Whether to use kerning in bitmap font. Default is `true`.
*/
bitmapFontKerning: boolean;

/**
* Whether to use esdt SDF generation. Default is `true`.
*/
esdt: boolean;
}

// @ts-ignore
Expand Down Expand Up @@ -157,6 +162,7 @@ export function TextWrapper<TBase extends GConstructor>(Base: TBase) {
#textBaseline: CanvasTextBaseline;
bitmapFont: BitmapFont;
bitmapFontKerning: boolean;
esdt: boolean;
static getGeometryBounds(
attributes: Partial<TextAttributes> & { metrics: TextMetrics },
) {
Expand Down Expand Up @@ -214,6 +220,7 @@ export function TextWrapper<TBase extends GConstructor>(Base: TBase) {
leading,
bitmapFont,
bitmapFontKerning,
esdt,
} = attributes;

this.#x = x ?? 0;
Expand All @@ -236,6 +243,7 @@ export function TextWrapper<TBase extends GConstructor>(Base: TBase) {
this.leading = leading ?? 0;
this.bitmapFont = bitmapFont ?? null;
this.bitmapFontKerning = bitmapFontKerning ?? true;
this.esdt = esdt ?? true;
}

containsPoint(x: number, y: number) {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/utils/glyph/glyph-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export class GlyphManager {
fontStyle = '',
text: string,
device: Device,
esdt: boolean,
) {
let newChars: string[] = [];
if (!this.glyphMap[fontStack]) {
Expand All @@ -183,6 +184,7 @@ export class GlyphManager {
fontWeight,
fontStyle,
char,
esdt,
);
})
.reduce((prev, cur) => {
Expand Down Expand Up @@ -228,6 +230,7 @@ export class GlyphManager {
fontWeight: string,
fontStyle: string,
char: string,
esdt: boolean,
): StyleGlyph {
let sdfGenerator = this.sdfGeneratorCache[fontStack];
if (!sdfGenerator) {
Expand Down Expand Up @@ -263,7 +266,7 @@ export class GlyphManager {
glyphLeft,
glyphTop,
glyphAdvance,
} = sdfGenerator.draw(char);
} = sdfGenerator.draw(char, esdt);

return {
id: char,
Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/utils/glyph/sdf-edt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { edt, getSDFStage, glyphToRGBA, INF, SDFStage } from './tiny-sdf';

// Paint glyph data into stage
export const paintIntoStage = (
stage: SDFStage,
data: Uint8ClampedArray,
w: number,
h: number,
pad: number,
) => {
const wp = w + pad * 2;
const hp = h + pad * 2;
const np = wp * hp;

const { outer, inner } = stage;

outer.fill(INF, 0, np);
inner.fill(0, 0, np);

const getData = (x: number, y: number) =>
(data[4 * (y * w + x) + 3] ?? 0) / 255;
// const getData = (x: number, y: number) => (data[y * w + x] ?? 0) / 255;

for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const a = getData(x, y);
const i = (y + pad) * wp + x + pad;

if (a >= 254 / 255) {
// Fix for bad rasterizer rounding
data[4 * (y * w + x) + 3] = 255;

outer[i] = 0;
inner[i] = INF;
} else if (a > 0) {
const d = 0.5 - a;
outer[i] = d > 0 ? d * d : 0;
inner[i] = d < 0 ? d * d : 0;
}
}
}
};

// Convert grayscale glyph to SDF using pixel-based distance transform
export const glyphToEDT = (
data: Uint8ClampedArray,
w: number,
h: number,
pad: number = 4,
radius: number = 3,
cutoff: number = 0.25,
) => {
const wp = w + pad * 2;
const hp = h + pad * 2;
const np = wp * hp;
const sp = Math.max(wp, hp);

const out = new Uint8Array(np);

const stage = getSDFStage(sp);
paintIntoStage(stage, data, w, h, pad);

const { outer, inner, f, z, v } = stage;

edt(outer, 0, 0, wp, hp, wp, f, z, v);
edt(inner, pad, pad, w, h, wp, f, z, v);

for (let i = 0; i < np; i++) {
const d = Math.sqrt(outer[i]) - Math.sqrt(inner[i]);
out[i] = Math.max(
0,
Math.min(255, Math.round(255 - 255 * (d / radius + cutoff))),
);
}

return glyphToRGBA(out, wp, hp);
};
Loading

0 comments on commit f0861af

Please sign in to comment.