Skip to content

Commit

Permalink
feat: support msdf in text rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Jan 10, 2025
1 parent 0653e23 commit 4dc4a49
Show file tree
Hide file tree
Showing 27 changed files with 3,154 additions and 437 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ pnpm run dev
- What's shaping
- 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
- How to handle emoji

<img src="./screenshots/lesson15.png" width="300" alt="Lesson 15 - text">
Expand Down
1 change: 1 addition & 0 deletions README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ pnpm run dev
- 什么是 Shaping
- 处理段落。分段与自动换行、BiDi 和 cluster
- 如何生成 SDF atlas 并使用它绘制
- 如何使用 MSDF 提升文本渲染质量
- 如何处理 emoji

<img src="./screenshots/lesson15.png" width="300" alt="Lesson 15 - text">
Expand Down
30 changes: 19 additions & 11 deletions packages/core/src/drawcalls/SDFText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,21 @@ export class SDFText extends Drawcall {
}

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

const indices: number[] = [];
const positions: number[] = [];
const uvOffsets: number[] = [];
let indicesOff = 0;
let fontScale = 1;
let fontScale: number;

if (!this.useBitmapFont) {
if (this.useBitmapFont) {
fontScale =
metrics.fontMetrics.fontSize / bitmapFont.baseMeasurementFontSize;
} else {
// scale current font size to base(24)
fontScale = BASE_FONT_WIDTH / metrics.fontMetrics.fontSize;
fontScale = metrics.fontMetrics.fontSize / BASE_FONT_WIDTH;
const allText = this.shapes.map((text: Text) => text.content).join('');
this.#glyphManager.generateAtlas(
metrics.font,
Expand Down Expand Up @@ -110,10 +113,11 @@ export class SDFText extends Drawcall {
object,
lines,
fontStack: font,
lineHeight: (fontScale * lineHeight) / SDF_SCALE,
letterSpacing: fontScale * letterSpacing,
lineHeight: lineHeight / SDF_SCALE,
letterSpacing: letterSpacing,
indicesOffset: indicesOff,
bitmapFont,
fontScale,
});
indicesOff = indicesOffset;

Expand Down Expand Up @@ -402,6 +406,7 @@ export class SDFText extends Drawcall {
letterSpacing,
indicesOffset,
bitmapFont,
fontScale,
}: {
object: Text;
lines: string[];
Expand All @@ -410,8 +415,9 @@ export class SDFText extends Drawcall {
letterSpacing: number;
indicesOffset: number;
bitmapFont: BitmapFont;
fontScale: number;
}) {
const { textAlign = 'start', x = 0, y = 0 } = object;
const { textAlign = 'start', x = 0, y = 0, bitmapFontKerning } = object;

const charUVOffsetBuffer: number[] = [];
const charPositionsBuffer: number[] = [];
Expand All @@ -426,20 +432,22 @@ export class SDFText extends Drawcall {
textAlign,
letterSpacing,
bitmapFont,
fontScale,
bitmapFontKerning,
);

let positions: GlyphPositions;
if (bitmapFont) {
positions = {
[fontStack]: Object.keys(bitmapFont.chars).reduce((acc, char) => {
const { xAdvance } = bitmapFont.chars[char];
const { xAdvance, xOffset, yOffset, rect } = bitmapFont.chars[char];
acc[char] = {
rect: bitmapFont.chars[char].rect,
rect,
metrics: {
width: xAdvance,
height: bitmapFont.lineHeight,
left: 0,
top: 0,
left: xOffset,
top: -yOffset,
advance: xAdvance,
},
};
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/shaders/sdf_text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ void main() {
}
v_Uv = a_UvOffset.xy / u_AtlasSize;
float fontScale = fontSize / 24.;
vec2 offset = a_UvOffset.zw * fontScale;
vec2 offset = a_UvOffset.zw;
gl_Position = vec4((u_ProjectionMatrix
* u_ViewMatrix
Expand Down Expand Up @@ -120,8 +119,12 @@ void main() {
float fontSize = u_ZIndexStrokeWidth.z;
float fontScale = fontSize / 24.0;
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 @@ -123,6 +123,11 @@ export interface TextAttributes extends ShapeAttributes {
* @see https://pixijs.com/8.x/examples/text/bitmap-text
*/
bitmapFont: BitmapFont;

/**
* Whether to use kerning in bitmap font. Default is `true`.
*/
bitmapFontKerning: boolean;
}

// @ts-ignore
Expand Down Expand Up @@ -151,6 +156,7 @@ export function TextWrapper<TBase extends GConstructor>(Base: TBase) {
#textAlign: CanvasTextAlign;
#textBaseline: CanvasTextBaseline;
bitmapFont: BitmapFont;
bitmapFontKerning: boolean;
static getGeometryBounds(
attributes: Partial<TextAttributes> & { metrics: TextMetrics },
) {
Expand Down Expand Up @@ -207,6 +213,7 @@ export function TextWrapper<TBase extends GConstructor>(Base: TBase) {
lineHeight,
leading,
bitmapFont,
bitmapFontKerning,
} = attributes;

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

containsPoint(x: number, y: number) {
Expand Down
35 changes: 28 additions & 7 deletions packages/core/src/utils/font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class CanvasTextMetrics {

private measureBitmapFont(bitmapFont: BitmapFont, fontSize: number) {
const { fontMetrics, lineHeight } = bitmapFont;
const scale = fontSize / fontMetrics.fontSize;
const scale = fontSize / bitmapFont.baseMeasurementFontSize;
return {
scale,
lineHeight: lineHeight * scale,
Expand All @@ -123,11 +123,13 @@ export class CanvasTextMetrics {
strokeWidth,
leading,
bitmapFont,
bitmapFontKerning,
} = style;

let lineHeight = style.lineHeight;
let font: string;
const font = fontStringFromTextStyle(style);
let fontMetrics: globalThis.TextMetrics & { fontSize: number };
let scale = 1;

if (bitmapFont) {
const textMetrics = this.measureBitmapFont(
Expand All @@ -136,18 +138,20 @@ export class CanvasTextMetrics {
);
lineHeight = textMetrics.lineHeight;
fontMetrics = textMetrics.fontMetrics;
scale = textMetrics.scale;
} else {
font = fontStringFromTextStyle(bitmapFont ?? style);
fontMetrics = this.measureFont(font);
this.#context.font = font;
}

lineHeight *= scale;

// fallback in case UA disallow canvas data extraction
if (fontMetrics.fontSize === 0) {
fontMetrics.fontSize = style.fontSize as number;
}

const outputText = wordWrap ? this.wordWrap(text, style) : text;
const outputText = wordWrap ? this.wordWrap(text, style, scale) : text;
const lines = outputText.split(/(?:\r\n|\r|\n)/);
const lineWidths = new Array<number>(lines.length);
let maxLineWidth = 0;
Expand All @@ -156,6 +160,8 @@ export class CanvasTextMetrics {
lines[i],
letterSpacing,
bitmapFont,
bitmapFontKerning,
scale,
);
lineWidths[i] = lineWidth;
maxLineWidth = Math.max(maxLineWidth, lineWidth);
Expand Down Expand Up @@ -305,7 +311,7 @@ export class CanvasTextMetrics {
/**
* @see https://github.com/pixijs/pixijs/blob/dev/src/scene/text/canvas/CanvasTextMetrics.ts#L369
*/
wordWrap(text: string, style: Partial<TextAttributes>) {
wordWrap(text: string, style: Partial<TextAttributes>, scale: number) {
const context = this.#canvas.getContext('2d', {
willReadFrequently: true,
});
Expand Down Expand Up @@ -346,6 +352,7 @@ export class CanvasTextMetrics {
cache,
context as CanvasRenderingContext2D,
bitmapFont,
scale,
);
};
const ellipsisWidth = Array.from(ellipsis).reduce((prev, cur) => {
Expand Down Expand Up @@ -533,14 +540,17 @@ export class CanvasTextMetrics {
cache: CharacterWidthCache,
context: CanvasRenderingContext2D,
bitmapFont: BitmapFont,
scale: number,
): number {
let width = cache[key];
if (typeof width !== 'number') {
const spacing = key.length * letterSpacing;
width =
(bitmapFont
? bitmapFont.chars[key]?.xAdvance || 0
: context.measureText(key).width) + spacing;
: context.measureText(key).width) *
scale +
spacing;
cache[key] = width;
}
return width;
Expand All @@ -550,14 +560,25 @@ export class CanvasTextMetrics {
text: string,
letterSpacing: number,
bitmapFont: BitmapFont,
bitmapFontKerning: boolean,
scale: number,
) {
const segments = this.#graphemeSegmenter(text);

let metricWidth: number;
let boundsWidth: number;
let previousChar: string;
if (bitmapFont) {
metricWidth = segments.reduce((sum, char) => {
return sum + (bitmapFont.chars[char]?.xAdvance || 0);
const advance = bitmapFont.chars[char]?.xAdvance;
const kerning =
(bitmapFontKerning &&
previousChar &&
bitmapFont.chars[char]?.kerning[previousChar]) ||
0;

previousChar = char;
return sum + ((advance + kerning) * scale || 0);
}, 0);
boundsWidth = metricWidth;
} else {
Expand Down
20 changes: 14 additions & 6 deletions packages/core/src/utils/glyph/glyph-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export class GlyphManager {
textAlign: CanvasTextAlign,
letterSpacing: number,
bitmapFont?: BitmapFont,
scale?: number,
bitmapFontKerning?: boolean,
): PositionedGlyph[] {
const positionedGlyphs: PositionedGlyph[] = [];

Expand All @@ -108,14 +110,18 @@ export class GlyphManager {
lines.forEach((line) => {
const lineStartIndex = positionedGlyphs.length;

let previousChar: string;
canvasTextMetrics.graphemeSegmenter(line).forEach((char) => {
let advance: number;
let glyphOffset = 0;
const scale = 1;
let kerning = 0;
if (bitmapFont) {
const charData = bitmapFont.chars[char];
advance = charData.xAdvance;
glyphOffset = charData.yOffset;
kerning =
(bitmapFontKerning &&
previousChar &&
charData.kerning[previousChar]) ||
0;
} else {
const positions = this.glyphMap[fontStack];
const glyph = positions && positions[char];
Expand All @@ -124,12 +130,14 @@ export class GlyphManager {

positionedGlyphs.push({
glyph: char,
x,
y: y + glyphOffset,
x: x,
y: y,
scale,
fontStack,
});
x += advance + letterSpacing;
x += (advance + kerning) * scale + letterSpacing;

previousChar = char;
});

const lineWidth = x - letterSpacing;
Expand Down
8 changes: 6 additions & 2 deletions packages/site/docs/.vitepress/config/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,17 @@ export const en = defineConfig({
link: 'wireframe',
},
{
text: 'Text',
link: 'text',
text: 'Use SDF to draw text',
link: 'sdf-text',
},
{
text: 'Use Bitmap Font to draw text',
link: 'bitmap-font',
},
{
text: 'Use MSDF to draw text',
link: 'msdf-text',
},
],
},
],
Expand Down
8 changes: 6 additions & 2 deletions packages/site/docs/.vitepress/config/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,17 @@ export const zh = defineConfig({
link: 'wireframe',
},
{
text: '绘制文本',
link: 'text',
text: '使用 SDF 绘制文本',
link: 'sdf-text',
},
{
text: '使用 Bitmap Font 绘制文本',
link: 'bitmap-font',
},
{
text: '使用 MSDF 绘制文本',
link: 'msdf-text',
},
],
},
],
Expand Down
Loading

0 comments on commit 4dc4a49

Please sign in to comment.