Skip to content

Commit

Permalink
chore: add test case for bitmap font loader
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Jan 8, 2025
1 parent ec105f1 commit 0653e23
Show file tree
Hide file tree
Showing 17 changed files with 4,215 additions and 65 deletions.
112 changes: 112 additions & 0 deletions __tests__/unit/bitmap-font.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Device, Format, TextureUsage } from '@antv/g-device-api';
import { bitmapFontJSONParser } from '../../packages/core/src/utils/bitmap-font/bitmap-font-json-parser';
import { BitmapFont } from '../../packages/core/src/utils/bitmap-font';

describe('BitmapFont', () => {
const mockFontData = {
info: {
face: 'Arial',
size: 32,
},
common: {
lineHeight: 36,
base: 29,
},
pages: [{ id: 0, file: 'arial.png' }],
chars: [
{
id: 65, // 'A'
x: 0,
y: 0,
width: 24,
height: 28,
xoffset: 0,
yoffset: 0,
xadvance: 26,
page: 0,
},
{
id: 66, // 'B'
x: 0,
y: 0,
width: 24,
height: 28,
xoffset: 0,
yoffset: 0,
xadvance: 26,
page: 0,
},
],
kernings: [
{
first: 65,
second: 66,
amount: -2,
},
],
};

// 模拟图片数据
const mockImage = {
width: 256,
height: 256,
} as ImageBitmap;

// 模拟 Device
const mockDevice = {
createTexture: jest.fn().mockReturnValue({
setImageData: jest.fn(),
destroy: jest.fn(),
}),
} as unknown as Device;

it('应该正确解析 JSON 格式的字体数据', () => {
const jsonStr = JSON.stringify(mockFontData);
const parsedData = bitmapFontJSONParser.parse(jsonStr);

expect(parsedData.fontFamily).toBe('Arial');
expect(parsedData.fontSize).toBe(32);
expect(parsedData.lineHeight).toBe(36);
});

it('应该正确创建 BitmapFont 实例', () => {
const font = new BitmapFont({
data: bitmapFontJSONParser.parse(JSON.stringify(mockFontData)),
images: [mockImage],
});

expect(font.fontFamily).toBe('Arial');
expect(font.lineHeight).toBe(36);
expect(font.chars['A']).toBeDefined();
});

it('应该正确创建纹理', () => {
const font = new BitmapFont({
data: bitmapFontJSONParser.parse(JSON.stringify(mockFontData)),
images: [mockImage],
});

font.createTexture(mockDevice);

expect(mockDevice.createTexture).toHaveBeenCalledWith({
format: Format.U8_RGBA_NORM,
width: mockImage.width,
height: mockImage.height,
usage: TextureUsage.SAMPLED,
});
});

it('应该正确处理销毁', () => {
const font = new BitmapFont({
data: bitmapFontJSONParser.parse(JSON.stringify(mockFontData)),
images: [mockImage],
});

font.createTexture(mockDevice);
font.destroy();

// 验证所有纹理都被销毁
expect(font.pages).toBeNull();
expect(font.chars?.['A']?.texture).toBeUndefined();
});
});
Binary file added packages/core/examples/desyrel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,922 changes: 1,922 additions & 0 deletions packages/core/examples/desyrel.xml

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions packages/core/examples/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const canvas = await new Canvas({

(async () => {
// const res = await fetch('./DimboR.fnt');
const res = await fetch('./msdf-sans-serif.json');
// const res = await fetch('./msdf-sans-serif.json');
const res = await fetch('./desyrel.xml');
const font = await loadBitmapFont.parse(await res.text());
console.log(font);
const text = new Text({
Expand All @@ -47,7 +48,7 @@ const canvas = await new Canvas({
content: 'Hello, world',
fontSize: 48,
fill: '#F67676',
fontFamily: 'sans-serif',
fontFamily: 'Desyrel',
bitmapFont: font,
// wireframe: true,
// textAlign: 'right',
Expand Down
60 changes: 41 additions & 19 deletions packages/core/src/drawcalls/SDF.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class SDF extends Drawcall {
}

this.vertexBufferDatas[1] = new Float32Array(
new Array(28 * this.shapes.length).fill(0),
new Array(32 * this.shapes.length).fill(0),
);
this.vertexBuffers[1] = this.device.createBuffer({
viewOrSize: this.vertexBufferDatas[1],
Expand Down Expand Up @@ -133,44 +133,49 @@ export class SDF extends Drawcall {
if (this.instanced) {
this.vertexBufferDescriptors.push(
{
arrayStride: 4 * 28,
arrayStride: 4 * 32,
stepMode: VertexStepMode.INSTANCE,
attributes: [
{
shaderLocation: Location.POSITION_SIZE, // a_PositionSize
shaderLocation: Location.POSITION, // a_Position
offset: 0,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.FILL_COLOR, // a_FillColor
shaderLocation: Location.SIZE, // a_Size
offset: 4 * 4,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.STROKE_COLOR, // a_StrokeColor
shaderLocation: Location.FILL_COLOR, // a_FillColor
offset: 4 * 8,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.ZINDEX_STROKE_WIDTH, // a_ZIndexStrokeWidth
shaderLocation: Location.STROKE_COLOR, // a_StrokeColor
offset: 4 * 12,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.OPACITY, // a_Opacity
shaderLocation: Location.ZINDEX_STROKE_WIDTH, // a_ZIndexStrokeWidth
offset: 4 * 16,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.INNER_SHADOW_COLOR, // a_InnerShadowColor
shaderLocation: Location.OPACITY, // a_Opacity
offset: 4 * 20,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.INNER_SHADOW, // a_InnerShadow
shaderLocation: Location.INNER_SHADOW_COLOR, // a_InnerShadowColor
offset: 4 * 24,
format: Format.F32_RGBA,
},
{
shaderLocation: Location.INNER_SHADOW, // a_InnerShadow
offset: 4 * 28,
format: Format.F32_RGBA,
},
],
},
{
Expand Down Expand Up @@ -298,8 +303,8 @@ export class SDF extends Drawcall {
) {
if (this.instanced) {
const instancedData: number[] = [];
this.shapes.forEach((shape) => {
const [buffer] = this.generateBuffer(shape);
this.shapes.forEach((shape, i, total) => {
const [buffer] = this.generateBuffer(shape, i, total.length);
instancedData.push(...buffer);
});
this.vertexBufferDatas[1] = new Float32Array(instancedData);
Expand All @@ -326,7 +331,11 @@ export class SDF extends Drawcall {
);
} else {
const { worldTransform } = this.shapes[0];
const [buffer, legacyObject] = this.generateBuffer(this.shapes[0]);
const [buffer, legacyObject] = this.generateBuffer(
this.shapes[0],
0,
1,
);
const u_ModelMatrix = worldTransform.toArray(true);
this.#uniformBuffer.setSubData(
0,
Expand Down Expand Up @@ -382,7 +391,11 @@ export class SDF extends Drawcall {
}
}

private generateBuffer(shape: Shape): [number[], Record<string, unknown>] {
private generateBuffer(
shape: Shape,
index: number,
total: number,
): [number[], Record<string, unknown>] {
const {
fillRGB,
strokeRGB: { r: sr, g: sg, b: sb, opacity: so },
Expand All @@ -398,27 +411,34 @@ export class SDF extends Drawcall {
sizeAttenuation,
} = shape;

let position: [number, number, number, number] = [0, 0, 0, 0];
let size: [number, number, number, number] = [0, 0, 0, 0];
let type: number = 0;
let cornerRadius = 0;
const zIndex =
(shape.globalRenderOrder + (1 / total) * index) / ZINDEX_FACTOR;
if (shape instanceof Circle) {
const { cx, cy, r } = shape;
size = [cx, cy, r, r];
position = [cx, cy, zIndex, 0];
size = [r, r, 0, 0];
type = 0;
} else if (shape instanceof Ellipse) {
const { cx, cy, rx, ry } = shape;
size = [cx, cy, rx, ry];
position = [cx, cy, zIndex, 0];
size = [rx, ry, 0, 0];
type = 1;
} else if (shape instanceof Rect) {
const { x, y, width, height, cornerRadius: r } = shape;
size = [x + width / 2, y + height / 2, width / 2, height / 2];
position = [x + width / 2, y + height / 2, zIndex, 0];
size = [width / 2, height / 2, 0, 0];
type = 2;
cornerRadius = r;
}

const { r: fr, g: fg, b: fb, opacity: fo } = fillRGB || {};

const u_PositionSize = size;
const u_Position = position;
const u_Size = size;
const u_FillColor = [fr / 255, fg / 255, fb / 255, fo];
const u_StrokeColor = [sr / 255, sg / 255, sb / 255, so];
const u_ZIndexStrokeWidth = [
Expand All @@ -441,7 +461,8 @@ export class SDF extends Drawcall {

return [
[
...u_PositionSize,
...u_Position,
...u_Size,
...u_FillColor,
...u_StrokeColor,
...u_ZIndexStrokeWidth,
Expand All @@ -450,7 +471,8 @@ export class SDF extends Drawcall {
...u_InnerShadow,
],
{
u_PositionSize,
u_Position,
u_Size,
u_FillColor,
u_StrokeColor,
u_ZIndexStrokeWidth,
Expand Down
36 changes: 31 additions & 5 deletions packages/core/src/drawcalls/SDFText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ export class SDFText extends Drawcall {

this.vertexBufferDescriptors = [
{
arrayStride: 4 * 2,
arrayStride: 4 * 3,
stepMode: VertexStepMode.VERTEX,
attributes: [
{
shaderLocation: Location.POSITION, // a_Position
offset: 0,
format: Format.F32_RG,
format: Format.F32_RGB,
},
],
},
Expand All @@ -186,8 +186,16 @@ export class SDFText extends Drawcall {
bitmapFont.createTexture(this.device);
glyphAtlasTexture = bitmapFont.pages[0].texture;

defines += '#define USE_MSDF\n';
const { distanceField } = bitmapFont;
if (distanceField.type === 'msdf') {
defines += '#define USE_MSDF\n';
} else if (distanceField.type === 'sdf') {
defines += '#define USE_SDF\n';
} else {
defines += '#define USE_SDF_NONE\n';
}
} else {
defines += '#define USE_SDF\n';
glyphAtlasTexture = this.#glyphManager.getAtlasTexture();
}

Expand Down Expand Up @@ -351,6 +359,7 @@ export class SDFText extends Drawcall {

const u_FillColor = [fr / 255, fg / 255, fb / 255, fo];
const u_StrokeColor = [sr / 255, sg / 255, sb / 255, so];

const u_ZIndexStrokeWidth = [
shape.globalRenderOrder / ZINDEX_FACTOR,
strokeWidth,
Expand Down Expand Up @@ -443,7 +452,7 @@ export class SDFText extends Drawcall {
}

getGlyphQuads(positionedGlyphs, positions, this.useBitmapFont).forEach(
(quad) => {
(quad, index, total) => {
// interleaved uv & offsets
charUVOffsetBuffer.push(quad.tex.x, quad.tex.y, quad.tl.x, quad.tl.y);
charUVOffsetBuffer.push(
Expand All @@ -464,7 +473,24 @@ export class SDFText extends Drawcall {
quad.bl.x,
quad.bl.y,
);
charPositionsBuffer.push(x, y, x, y, x, y, x, y);

const zIndex =
(object.globalRenderOrder + (1 / total.length) * index) /
ZINDEX_FACTOR;
charPositionsBuffer.push(
x,
y,
zIndex,
x,
y,
zIndex,
x,
y,
zIndex,
x,
y,
zIndex,
);

indexBuffer.push(0 + i, 2 + i, 1 + i);
indexBuffer.push(2 + i, 0 + i, 3 + i);
Expand Down
Loading

0 comments on commit 0653e23

Please sign in to comment.