@flighthq/bitmaptext 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitmapText.d.ts +16 -0
- package/dist/bitmapText.d.ts.map +1 -0
- package/dist/bitmapText.js +122 -0
- package/dist/bitmapText.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/updateBitmapText.d.ts +3 -0
- package/dist/updateBitmapText.d.ts.map +1 -0
- package/dist/updateBitmapText.js +238 -0
- package/dist/updateBitmapText.js.map +1 -0
- package/package.json +44 -0
- package/src/bitmapText.test.ts +222 -0
- package/src/updateBitmapText.test.ts +240 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BitmapText, BitmapTextAlign, BitmapTextData, BitmapTextOptions, BitmapTextRuntime, GlyphSource, QuadBatch, Rectangle } from '@flighthq/types';
|
|
2
|
+
export declare function computeBitmapTextLocalBoundsRectangle(out: Rectangle, source: Readonly<BitmapText>): void;
|
|
3
|
+
export declare function createBitmapText(glyphSource: GlyphSource | null, options?: Readonly<BitmapTextOptions>): BitmapText;
|
|
4
|
+
export declare function createBitmapTextData(data?: Readonly<Partial<BitmapTextData>>): BitmapTextData;
|
|
5
|
+
export declare function createBitmapTextRuntime(): BitmapTextRuntime;
|
|
6
|
+
export declare function getBitmapTextBounds(source: Readonly<BitmapText>): Rectangle;
|
|
7
|
+
export declare function getBitmapTextQuadBatches(source: Readonly<BitmapText>): QuadBatch[];
|
|
8
|
+
export declare function reserveBitmapText(target: BitmapText, glyphCapacity: number): void;
|
|
9
|
+
export declare function setBitmapTextAlign(target: BitmapText, align: BitmapTextAlign): void;
|
|
10
|
+
export declare function setBitmapTextColor(target: BitmapText, color: number): void;
|
|
11
|
+
export declare function setBitmapTextGlyphSource(target: BitmapText, glyphSource: GlyphSource | null): void;
|
|
12
|
+
export declare function setBitmapTextLetterSpacing(target: BitmapText, letterSpacing: number): void;
|
|
13
|
+
export declare function setBitmapTextLineHeight(target: BitmapText, lineHeight: number): void;
|
|
14
|
+
export declare function setBitmapTextText(target: BitmapText, text: string): void;
|
|
15
|
+
export declare function setBitmapTextWrapWidth(target: BitmapText, wrapWidth: number | null): void;
|
|
16
|
+
//# sourceMappingURL=bitmapText.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapText.d.ts","sourceRoot":"","sources":["../src/bitmapText.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EAGX,SAAS,EACT,SAAS,EACV,MAAM,iBAAiB,CAAC;AAQzB,wBAAgB,qCAAqC,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAWxG;AAQD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,UAAU,CAenH;AAED,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,GAAG,cAAc,CAU7F;AAED,wBAAgB,uBAAuB,IAAI,iBAAiB,CAK3D;AAID,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,CAI3E;AAID,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,EAAE,CAElF;AAKD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAGjF;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI,CAEnF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE1E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAElG;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAE1F;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAEpF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAExE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEzF"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createDisplayObjectGeneric, createDisplayObjectRuntime, getDisplayObjectRuntime, } from '@flighthq/displayobject';
|
|
2
|
+
import { copyRectangle, createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { addNodeChild } from '@flighthq/node';
|
|
4
|
+
import { createQuadBatch, reserveQuadBatch } from '@flighthq/sprite';
|
|
5
|
+
import { createTextureAtlas } from '@flighthq/textureatlas';
|
|
6
|
+
import { BitmapTextKind } from '@flighthq/types';
|
|
7
|
+
// White (`0xRRGGBBAA` all-ones) — the untinted default; the backing batch stays material-free at this value.
|
|
8
|
+
const BITMAP_TEXT_DEFAULT_COLOR = 0xffffffff;
|
|
9
|
+
// Writes the laid-out text extent (cached by `updateBitmapText`) into `out`, or a zero rectangle when
|
|
10
|
+
// the text has not been laid out yet. Alias-safe: `out` may be the cached rectangle.
|
|
11
|
+
export function computeBitmapTextLocalBoundsRectangle(out, source) {
|
|
12
|
+
const runtime = getDisplayObjectRuntime(source);
|
|
13
|
+
const bounds = runtime.localBoundsRectangle;
|
|
14
|
+
if (bounds === null) {
|
|
15
|
+
out.x = 0;
|
|
16
|
+
out.y = 0;
|
|
17
|
+
out.width = 0;
|
|
18
|
+
out.height = 0;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
copyRectangle(out, bounds);
|
|
22
|
+
}
|
|
23
|
+
// Allocates a BitmapText display node bound to `glyphSource` for both layout AND pixels. The
|
|
24
|
+
// `GlyphSource` seam now pairs its glyph geometry (rects, advances, kerning, metrics) with the
|
|
25
|
+
// backing atlas image(s) those rects sample (`getGlyphAtlasImage(page)`), so no separate image is
|
|
26
|
+
// supplied. The node owns one backing QuadBatch child per glyph-atlas page; a single-page source
|
|
27
|
+
// yields exactly one (page 0), created eagerly here. Call `updateBitmapText` to lay out the current
|
|
28
|
+
// `text`, which sets each page batch's atlas image and grows the set for a multi-page source.
|
|
29
|
+
export function createBitmapText(glyphSource, options) {
|
|
30
|
+
const bitmapText = createDisplayObjectGeneric(BitmapTextKind, undefined, createBitmapTextData, createBitmapTextRuntime);
|
|
31
|
+
const data = bitmapText.data;
|
|
32
|
+
data.glyphSource = glyphSource;
|
|
33
|
+
if (options !== undefined)
|
|
34
|
+
applyBitmapTextOptions(data, options);
|
|
35
|
+
const quadBatch = createQuadBatch({ data: { atlas: createTextureAtlas() } });
|
|
36
|
+
const runtime = getDisplayObjectRuntime(bitmapText);
|
|
37
|
+
runtime.quadBatches.push(quadBatch);
|
|
38
|
+
addNodeChild(bitmapText, quadBatch);
|
|
39
|
+
return bitmapText;
|
|
40
|
+
}
|
|
41
|
+
export function createBitmapTextData(data) {
|
|
42
|
+
return {
|
|
43
|
+
align: data?.align ?? 'left',
|
|
44
|
+
color: data?.color ?? BITMAP_TEXT_DEFAULT_COLOR,
|
|
45
|
+
glyphSource: data?.glyphSource ?? null,
|
|
46
|
+
letterSpacing: data?.letterSpacing ?? 0,
|
|
47
|
+
lineHeight: data?.lineHeight ?? 1,
|
|
48
|
+
text: data?.text ?? '',
|
|
49
|
+
wrapWidth: data?.wrapWidth ?? null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function createBitmapTextRuntime() {
|
|
53
|
+
const runtime = createDisplayObjectRuntime(defaultMethods);
|
|
54
|
+
runtime.localBoundsRectangle = null;
|
|
55
|
+
runtime.quadBatches = [];
|
|
56
|
+
return runtime;
|
|
57
|
+
}
|
|
58
|
+
// Allocates a fresh Rectangle holding the laid-out text extent. Use
|
|
59
|
+
// `computeBitmapTextLocalBoundsRectangle` with an owned `out` in hot paths.
|
|
60
|
+
export function getBitmapTextBounds(source) {
|
|
61
|
+
const out = createRectangle();
|
|
62
|
+
computeBitmapTextLocalBoundsRectangle(out, source);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
// The backing QuadBatches the node lays glyph quads into — the renderable children, one per
|
|
66
|
+
// glyph-atlas page in page order. A single-page source yields exactly one; empty before construction.
|
|
67
|
+
export function getBitmapTextQuadBatches(source) {
|
|
68
|
+
return getDisplayObjectRuntime(source).quadBatches;
|
|
69
|
+
}
|
|
70
|
+
// Grows each backing page QuadBatch to hold at least `glyphCapacity` glyph quads without reallocating
|
|
71
|
+
// during layout. Optional — `updateBitmapText` auto-grows — but avoids incremental reallocation for
|
|
72
|
+
// large strings. Reserving every page's batch over-allocates for multi-page text but never under-sizes.
|
|
73
|
+
export function reserveBitmapText(target, glyphCapacity) {
|
|
74
|
+
const runtime = getDisplayObjectRuntime(target);
|
|
75
|
+
for (const quadBatch of runtime.quadBatches)
|
|
76
|
+
reserveQuadBatch(quadBatch, glyphCapacity);
|
|
77
|
+
}
|
|
78
|
+
// The setters below mutate node data only; call `updateBitmapText` afterward to re-lay-out the batch.
|
|
79
|
+
export function setBitmapTextAlign(target, align) {
|
|
80
|
+
target.data.align = align;
|
|
81
|
+
}
|
|
82
|
+
export function setBitmapTextColor(target, color) {
|
|
83
|
+
target.data.color = color;
|
|
84
|
+
}
|
|
85
|
+
export function setBitmapTextGlyphSource(target, glyphSource) {
|
|
86
|
+
target.data.glyphSource = glyphSource;
|
|
87
|
+
}
|
|
88
|
+
export function setBitmapTextLetterSpacing(target, letterSpacing) {
|
|
89
|
+
target.data.letterSpacing = letterSpacing;
|
|
90
|
+
}
|
|
91
|
+
export function setBitmapTextLineHeight(target, lineHeight) {
|
|
92
|
+
target.data.lineHeight = lineHeight;
|
|
93
|
+
}
|
|
94
|
+
export function setBitmapTextText(target, text) {
|
|
95
|
+
target.data.text = text;
|
|
96
|
+
}
|
|
97
|
+
export function setBitmapTextWrapWidth(target, wrapWidth) {
|
|
98
|
+
target.data.wrapWidth = wrapWidth;
|
|
99
|
+
}
|
|
100
|
+
function applyBitmapTextOptions(data, options) {
|
|
101
|
+
if (options.align !== undefined)
|
|
102
|
+
data.align = options.align;
|
|
103
|
+
if (options.color !== undefined)
|
|
104
|
+
data.color = options.color;
|
|
105
|
+
if (options.letterSpacing !== undefined)
|
|
106
|
+
data.letterSpacing = options.letterSpacing;
|
|
107
|
+
if (options.lineHeight !== undefined)
|
|
108
|
+
data.lineHeight = options.lineHeight;
|
|
109
|
+
if (options.text !== undefined)
|
|
110
|
+
data.text = options.text;
|
|
111
|
+
if (options.wrapWidth !== undefined)
|
|
112
|
+
data.wrapWidth = options.wrapWidth;
|
|
113
|
+
}
|
|
114
|
+
function copyLocalBoundsRectangle(out, source) {
|
|
115
|
+
const runtime = getDisplayObjectRuntime(source);
|
|
116
|
+
if (runtime.localBoundsRectangle !== null)
|
|
117
|
+
copyRectangle(out, runtime.localBoundsRectangle);
|
|
118
|
+
}
|
|
119
|
+
const defaultMethods = {
|
|
120
|
+
computeLocalBoundsRectangle: copyLocalBoundsRectangle,
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=bitmapText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitmapText.js","sourceRoot":"","sources":["../src/bitmapText.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateBitmapText.d.ts","sourceRoot":"","sources":["../src/updateBitmapText.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,UAAU,EAQX,MAAM,iBAAiB,CAAC;AAiBzB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CA4E7D"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createColorTransformAdjustment } from '@flighthq/adjustments';
|
|
2
|
+
import { getDisplayObjectRuntime, setDisplayObjectColorAdjustments } from '@flighthq/displayobject';
|
|
3
|
+
import { createRectangle } from '@flighthq/geometry';
|
|
4
|
+
import { createColorTransform } from '@flighthq/materials';
|
|
5
|
+
import { addNodeChild, invalidateNodeLocalBounds } from '@flighthq/node';
|
|
6
|
+
import { appendQuadBatchInstance, clearQuadBatch, createQuadBatch } from '@flighthq/sprite';
|
|
7
|
+
import { addTextureAtlasRegion, createTextureAtlas } from '@flighthq/textureatlas';
|
|
8
|
+
// White (`0xRRGGBBAA` all-ones) leaves the backing batch material-free — the untinted path.
|
|
9
|
+
const BITMAP_TEXT_DEFAULT_COLOR = 0xffffffff;
|
|
10
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
11
|
+
const SPACE = 0x20;
|
|
12
|
+
// Lays out `bitmapText`'s current string and rewrites its backing QuadBatches: one quad per visible
|
|
13
|
+
// glyph, partitioned by the glyph's atlas page into one QuadBatch per page, positioned by the glyph
|
|
14
|
+
// source's advances and kerning, broken on explicit newlines and (when `wrapWidth` is set) at word
|
|
15
|
+
// boundaries, stacked by the metric line advance, and aligned per line. Each page batch's atlas image
|
|
16
|
+
// is bound from `getGlyphAtlasImage(page)` and its regions rebuilt from that page's encountered glyph
|
|
17
|
+
// rects each call, so a dynamic glyph source whose rects shift between layouts stays correct. A
|
|
18
|
+
// single-page source produces exactly one batch (page 0). A page whose `getGlyphAtlasImage` returns
|
|
19
|
+
// null cannot be sampled, so its glyphs are skipped. Missing glyphs (`getGlyphEntry` → null) are
|
|
20
|
+
// omitted entirely — no quad and no advance — since a null entry carries no advance to honor. Bounds
|
|
21
|
+
// span every drawn glyph across all pages.
|
|
22
|
+
export function updateBitmapText(bitmapText) {
|
|
23
|
+
const data = bitmapText.data;
|
|
24
|
+
const runtime = getDisplayObjectRuntime(bitmapText);
|
|
25
|
+
const bounds = ensureBoundsRectangle(runtime);
|
|
26
|
+
// Clear and recolor every existing page batch; pages with glyphs this layout are refilled below,
|
|
27
|
+
// pages that fall silent stay as empty children drawing nothing.
|
|
28
|
+
for (const quadBatch of runtime.quadBatches) {
|
|
29
|
+
clearQuadBatch(quadBatch);
|
|
30
|
+
if (quadBatch.data.atlas !== null)
|
|
31
|
+
quadBatch.data.atlas.regions.length = 0;
|
|
32
|
+
applyBitmapTextColor(quadBatch, data.color);
|
|
33
|
+
}
|
|
34
|
+
const glyphSource = data.glyphSource;
|
|
35
|
+
if (glyphSource === null || data.text.length === 0) {
|
|
36
|
+
setEmptyRectangle(bounds);
|
|
37
|
+
invalidateNodeLocalBounds(bitmapText);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const metrics = glyphSource.getGlyphMetrics();
|
|
41
|
+
const lineAdvance = (metrics.ascent + metrics.descent + metrics.lineGap) * data.lineHeight;
|
|
42
|
+
const lines = layoutBitmapTextLines(glyphSource, data);
|
|
43
|
+
const refWidth = data.wrapWidth ?? maxLineWidth(lines);
|
|
44
|
+
const pages = new Map();
|
|
45
|
+
let minX = Infinity;
|
|
46
|
+
let minY = Infinity;
|
|
47
|
+
let maxX = -Infinity;
|
|
48
|
+
let maxY = -Infinity;
|
|
49
|
+
for (let li = 0; li < lines.length; li++) {
|
|
50
|
+
const line = lines[li];
|
|
51
|
+
const baselineY = metrics.ascent + li * lineAdvance;
|
|
52
|
+
let startX = 0;
|
|
53
|
+
let gapExtra = 0;
|
|
54
|
+
if (data.align === 'center')
|
|
55
|
+
startX = (refWidth - line.width) / 2;
|
|
56
|
+
else if (data.align === 'right')
|
|
57
|
+
startX = refWidth - line.width;
|
|
58
|
+
else if (data.align === 'justify' && data.wrapWidth !== null && !line.paragraphEnd && line.gaps.length > 0) {
|
|
59
|
+
gapExtra = (data.wrapWidth - line.width) / line.gaps.length;
|
|
60
|
+
}
|
|
61
|
+
let penX = startX;
|
|
62
|
+
for (let wi = 0; wi < line.words.length; wi++) {
|
|
63
|
+
if (wi > 0)
|
|
64
|
+
penX += line.gaps[wi - 1] + gapExtra;
|
|
65
|
+
const word = line.words[wi];
|
|
66
|
+
for (const glyph of word.glyphs) {
|
|
67
|
+
const entry = glyph.entry;
|
|
68
|
+
const page = ensureBitmapTextPageBatch(bitmapText, runtime, glyphSource, data.color, pages, entry.page);
|
|
69
|
+
if (page === null)
|
|
70
|
+
continue;
|
|
71
|
+
const quadX = penX + glyph.penWithinWord + entry.bearingX;
|
|
72
|
+
const quadY = baselineY - entry.bearingY;
|
|
73
|
+
let regionId = page.regionByCodepoint.get(glyph.codepoint);
|
|
74
|
+
if (regionId === undefined) {
|
|
75
|
+
addTextureAtlasRegion(page.atlas, entry.x, entry.y, entry.width, entry.height);
|
|
76
|
+
regionId = page.atlas.regions.length - 1;
|
|
77
|
+
page.regionByCodepoint.set(glyph.codepoint, regionId);
|
|
78
|
+
}
|
|
79
|
+
appendQuadBatchInstance(page.quadBatch, regionId, quadX, quadY);
|
|
80
|
+
if (quadX < minX)
|
|
81
|
+
minX = quadX;
|
|
82
|
+
if (quadY < minY)
|
|
83
|
+
minY = quadY;
|
|
84
|
+
if (quadX + entry.width > maxX)
|
|
85
|
+
maxX = quadX + entry.width;
|
|
86
|
+
if (quadY + entry.height > maxY)
|
|
87
|
+
maxY = quadY + entry.height;
|
|
88
|
+
}
|
|
89
|
+
penX += word.width;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (minX === Infinity) {
|
|
93
|
+
setEmptyRectangle(bounds);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
bounds.x = minX;
|
|
97
|
+
bounds.y = minY;
|
|
98
|
+
bounds.width = maxX - minX;
|
|
99
|
+
bounds.height = maxY - minY;
|
|
100
|
+
}
|
|
101
|
+
invalidateNodeLocalBounds(bitmapText);
|
|
102
|
+
}
|
|
103
|
+
// Sets (or clears) the backing batch's node-level color transform from a packed-RGBA color, as a
|
|
104
|
+
// single-entry color-adjustment stack on the batch's runtime slot. White clears it. Every glyph of the
|
|
105
|
+
// batch shares this one tint (whole-batch, not per-glyph), so it resolves to one value and folds into the
|
|
106
|
+
// glyph draw as a single whole-batch color-transform uniform.
|
|
107
|
+
function applyBitmapTextColor(quadBatch, color) {
|
|
108
|
+
if (color === BITMAP_TEXT_DEFAULT_COLOR) {
|
|
109
|
+
setDisplayObjectColorAdjustments(quadBatch, null);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const colorTransform = createColorTransform({
|
|
113
|
+
redMultiplier: ((color >>> 24) & 0xff) / 255,
|
|
114
|
+
greenMultiplier: ((color >>> 16) & 0xff) / 255,
|
|
115
|
+
blueMultiplier: ((color >>> 8) & 0xff) / 255,
|
|
116
|
+
alphaMultiplier: (color & 0xff) / 255,
|
|
117
|
+
});
|
|
118
|
+
setDisplayObjectColorAdjustments(quadBatch, [createColorTransformAdjustment(colorTransform)]);
|
|
119
|
+
}
|
|
120
|
+
// Measures one paragraph (a newline-free run) into words separated by whitespace gaps. Intra-word
|
|
121
|
+
// kerning and letter spacing are baked into each glyph's `penWithinWord` and the word width; kerning
|
|
122
|
+
// does not cross a space. Zero-size glyphs advance the pen but produce no quad; missing glyphs are skipped.
|
|
123
|
+
function buildBitmapTextWords(glyphSource, paragraph, letterSpacing) {
|
|
124
|
+
const tokens = [];
|
|
125
|
+
let pendingGap = 0;
|
|
126
|
+
let glyphs = [];
|
|
127
|
+
let pen = 0;
|
|
128
|
+
let previousCodepoint = -1;
|
|
129
|
+
let inWord = false;
|
|
130
|
+
const flush = () => {
|
|
131
|
+
if (!inWord)
|
|
132
|
+
return;
|
|
133
|
+
tokens.push({ gap: pendingGap, word: { glyphs, width: pen } });
|
|
134
|
+
pendingGap = 0;
|
|
135
|
+
glyphs = [];
|
|
136
|
+
pen = 0;
|
|
137
|
+
previousCodepoint = -1;
|
|
138
|
+
inWord = false;
|
|
139
|
+
};
|
|
140
|
+
for (const character of paragraph) {
|
|
141
|
+
const codepoint = character.codePointAt(0);
|
|
142
|
+
if (codepoint === undefined || codepoint === CARRIAGE_RETURN)
|
|
143
|
+
continue;
|
|
144
|
+
if (codepoint === SPACE) {
|
|
145
|
+
flush();
|
|
146
|
+
const spaceEntry = glyphSource.getGlyphEntry(SPACE);
|
|
147
|
+
pendingGap += (spaceEntry !== null ? spaceEntry.advance : 0) + letterSpacing;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const entry = glyphSource.getGlyphEntry(codepoint);
|
|
151
|
+
if (entry === null)
|
|
152
|
+
continue;
|
|
153
|
+
if (previousCodepoint >= 0)
|
|
154
|
+
pen += glyphSource.getGlyphKerning(previousCodepoint, codepoint);
|
|
155
|
+
if (entry.width > 0 && entry.height > 0)
|
|
156
|
+
glyphs.push({ codepoint, entry, penWithinWord: pen });
|
|
157
|
+
pen += entry.advance + letterSpacing;
|
|
158
|
+
previousCodepoint = codepoint;
|
|
159
|
+
inWord = true;
|
|
160
|
+
}
|
|
161
|
+
flush();
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
// Ensures a backing QuadBatch exists for glyph-atlas `page` (page-indexed in `runtime.quadBatches`,
|
|
165
|
+
// growing and parenting fresh batches as needed) and binds it to that page's atlas image, returning
|
|
166
|
+
// the per-layout page context (batch + atlas + region cache). Returns null when the page has no atlas
|
|
167
|
+
// image to sample — its glyphs are then skipped. Newly grown gap pages get an empty batch bound to
|
|
168
|
+
// their own image; the common single-page source touches only page 0 (created by `createBitmapText`).
|
|
169
|
+
function ensureBitmapTextPageBatch(bitmapText, runtime, glyphSource, color, pages, page) {
|
|
170
|
+
const cached = pages.get(page);
|
|
171
|
+
if (cached !== undefined)
|
|
172
|
+
return cached;
|
|
173
|
+
const image = glyphSource.getGlyphAtlasImage(page);
|
|
174
|
+
if (image === null)
|
|
175
|
+
return null;
|
|
176
|
+
while (runtime.quadBatches.length <= page) {
|
|
177
|
+
const created = createQuadBatch({ data: { atlas: createTextureAtlas() } });
|
|
178
|
+
applyBitmapTextColor(created, color);
|
|
179
|
+
runtime.quadBatches.push(created);
|
|
180
|
+
addNodeChild(bitmapText, created);
|
|
181
|
+
}
|
|
182
|
+
const quadBatch = runtime.quadBatches[page];
|
|
183
|
+
const atlas = quadBatch.data.atlas;
|
|
184
|
+
atlas.image = image;
|
|
185
|
+
const pageBatch = { atlas, quadBatch, regionByCodepoint: new Map() };
|
|
186
|
+
pages.set(page, pageBatch);
|
|
187
|
+
return pageBatch;
|
|
188
|
+
}
|
|
189
|
+
function ensureBoundsRectangle(runtime) {
|
|
190
|
+
if (runtime.localBoundsRectangle === null)
|
|
191
|
+
runtime.localBoundsRectangle = createRectangle();
|
|
192
|
+
return runtime.localBoundsRectangle;
|
|
193
|
+
}
|
|
194
|
+
// Greedy line fill: split the text on explicit newlines into paragraphs, then within each paragraph
|
|
195
|
+
// pack words onto lines, breaking before a word when `wrapWidth` is set and it would overflow. A word
|
|
196
|
+
// wider than `wrapWidth` occupies its own overflowing line (no mid-word breaking).
|
|
197
|
+
function layoutBitmapTextLines(glyphSource, data) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
const paragraphs = data.text.split('\n');
|
|
200
|
+
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
201
|
+
const tokens = buildBitmapTextWords(glyphSource, paragraphs[pi], data.letterSpacing);
|
|
202
|
+
let current = { words: [], gaps: [], width: 0, paragraphEnd: false };
|
|
203
|
+
for (const token of tokens) {
|
|
204
|
+
const wraps = data.wrapWidth !== null &&
|
|
205
|
+
current.words.length > 0 &&
|
|
206
|
+
current.width + token.gap + token.word.width > data.wrapWidth;
|
|
207
|
+
if (wraps) {
|
|
208
|
+
lines.push(current);
|
|
209
|
+
current = { words: [token.word], gaps: [], width: token.word.width, paragraphEnd: false };
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
if (current.words.length > 0) {
|
|
213
|
+
current.gaps.push(token.gap);
|
|
214
|
+
current.width += token.gap;
|
|
215
|
+
}
|
|
216
|
+
current.words.push(token.word);
|
|
217
|
+
current.width += token.word.width;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
current.paragraphEnd = true;
|
|
221
|
+
lines.push(current);
|
|
222
|
+
}
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
function maxLineWidth(lines) {
|
|
226
|
+
let max = 0;
|
|
227
|
+
for (const line of lines)
|
|
228
|
+
if (line.width > max)
|
|
229
|
+
max = line.width;
|
|
230
|
+
return max;
|
|
231
|
+
}
|
|
232
|
+
function setEmptyRectangle(out) {
|
|
233
|
+
out.x = 0;
|
|
234
|
+
out.y = 0;
|
|
235
|
+
out.width = 0;
|
|
236
|
+
out.height = 0;
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=updateBitmapText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateBitmapText.js","sourceRoot":"","sources":["../src/updateBitmapText.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/bitmaptext",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/adjustments": "0.1.0",
|
|
31
|
+
"@flighthq/displayobject": "0.1.0",
|
|
32
|
+
"@flighthq/geometry": "0.1.0",
|
|
33
|
+
"@flighthq/materials": "0.1.0",
|
|
34
|
+
"@flighthq/node": "0.1.0",
|
|
35
|
+
"@flighthq/sprite": "0.1.0",
|
|
36
|
+
"@flighthq/textureatlas": "0.1.0",
|
|
37
|
+
"@flighthq/types": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "^5.3.0"
|
|
41
|
+
},
|
|
42
|
+
"description": "QuadBatch-backed bitmap text display node laying out glyphs from a GlyphSource",
|
|
43
|
+
"sideEffects": false
|
|
44
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { getDisplayObjectRuntime } from '@flighthq/displayobject';
|
|
2
|
+
import { createRectangle } from '@flighthq/geometry';
|
|
3
|
+
import { getQuadBatchCapacity } from '@flighthq/sprite';
|
|
4
|
+
import type { BitmapTextRuntime, GlyphEntry, GlyphSource, ImageResource } from '@flighthq/types';
|
|
5
|
+
import { BitmapTextKind } from '@flighthq/types';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
computeBitmapTextLocalBoundsRectangle,
|
|
10
|
+
createBitmapText,
|
|
11
|
+
createBitmapTextData,
|
|
12
|
+
createBitmapTextRuntime,
|
|
13
|
+
getBitmapTextBounds,
|
|
14
|
+
getBitmapTextQuadBatches,
|
|
15
|
+
reserveBitmapText,
|
|
16
|
+
setBitmapTextAlign,
|
|
17
|
+
setBitmapTextColor,
|
|
18
|
+
setBitmapTextGlyphSource,
|
|
19
|
+
setBitmapTextLetterSpacing,
|
|
20
|
+
setBitmapTextLineHeight,
|
|
21
|
+
setBitmapTextText,
|
|
22
|
+
setBitmapTextWrapWidth,
|
|
23
|
+
} from './bitmapText';
|
|
24
|
+
import { updateBitmapText } from './updateBitmapText';
|
|
25
|
+
|
|
26
|
+
// A single-page glyph source: A/B are 6x8, a space advances 5, and page 0 is a stub `ImageResource`.
|
|
27
|
+
function createTestGlyphSource(): GlyphSource {
|
|
28
|
+
const entries = new Map<number, GlyphEntry>();
|
|
29
|
+
const add = (cp: number, x: number): void => {
|
|
30
|
+
entries.set(cp, { advance: 10, bearingX: 0, bearingY: 8, height: 8, page: 0, width: 6, x, y: 0 });
|
|
31
|
+
};
|
|
32
|
+
add(0x41, 0); // A
|
|
33
|
+
add(0x42, 6); // B
|
|
34
|
+
entries.set(0x20, { advance: 5, bearingX: 0, bearingY: 0, height: 0, page: 0, width: 0, x: 0, y: 0 }); // space
|
|
35
|
+
const image = {} as ImageResource;
|
|
36
|
+
return {
|
|
37
|
+
getGlyphAtlasImage: (page = 0) => (page === 0 ? image : null),
|
|
38
|
+
getGlyphEntry: (cp) => entries.get(cp) ?? null,
|
|
39
|
+
getGlyphKerning: () => 0,
|
|
40
|
+
getGlyphMetrics: () => ({ ascent: 8, descent: 2, lineGap: 0 }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('computeBitmapTextLocalBoundsRectangle', () => {
|
|
45
|
+
it('writes the laid-out text extent into a distinct out rectangle', () => {
|
|
46
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'A' });
|
|
47
|
+
updateBitmapText(text);
|
|
48
|
+
const out = createRectangle();
|
|
49
|
+
computeBitmapTextLocalBoundsRectangle(out, text);
|
|
50
|
+
expect(out.x).toBe(0);
|
|
51
|
+
expect(out.y).toBe(0);
|
|
52
|
+
expect(out.width).toBe(6);
|
|
53
|
+
expect(out.height).toBe(8);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('is safe when out aliases the cached bounds rectangle', () => {
|
|
57
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'A' });
|
|
58
|
+
updateBitmapText(text);
|
|
59
|
+
const cached = (getDisplayObjectRuntime(text) as BitmapTextRuntime).localBoundsRectangle;
|
|
60
|
+
expect(cached).not.toBeNull();
|
|
61
|
+
computeBitmapTextLocalBoundsRectangle(cached!, text);
|
|
62
|
+
expect(cached!.width).toBe(6);
|
|
63
|
+
expect(cached!.height).toBe(8);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('writes zeros before the first layout', () => {
|
|
67
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
68
|
+
const out = createRectangle();
|
|
69
|
+
out.width = 99;
|
|
70
|
+
computeBitmapTextLocalBoundsRectangle(out, text);
|
|
71
|
+
expect(out.width).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('createBitmapText', () => {
|
|
76
|
+
it('creates a BitmapText node with a backing QuadBatch child', () => {
|
|
77
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
78
|
+
expect(text.kind).toBe(BitmapTextKind);
|
|
79
|
+
const batches = getBitmapTextQuadBatches(text);
|
|
80
|
+
expect(batches).toHaveLength(1);
|
|
81
|
+
expect(getDisplayObjectRuntime(text).children).toContain(batches[0]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('applies construction options to node data', () => {
|
|
85
|
+
const text = createBitmapText(createTestGlyphSource(), {
|
|
86
|
+
align: 'center',
|
|
87
|
+
color: 0xff0000ff,
|
|
88
|
+
letterSpacing: 2,
|
|
89
|
+
lineHeight: 1.5,
|
|
90
|
+
text: 'Hi',
|
|
91
|
+
wrapWidth: 120,
|
|
92
|
+
});
|
|
93
|
+
expect(text.data.align).toBe('center');
|
|
94
|
+
expect(text.data.color).toBe(0xff0000ff);
|
|
95
|
+
expect(text.data.letterSpacing).toBe(2);
|
|
96
|
+
expect(text.data.lineHeight).toBe(1.5);
|
|
97
|
+
expect(text.data.text).toBe('Hi');
|
|
98
|
+
expect(text.data.wrapWidth).toBe(120);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('binds the supplied glyph source', () => {
|
|
102
|
+
const glyphSource = createTestGlyphSource();
|
|
103
|
+
const text = createBitmapText(glyphSource);
|
|
104
|
+
expect(text.data.glyphSource).toBe(glyphSource);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('createBitmapTextData', () => {
|
|
109
|
+
it('defaults to left-aligned white empty unwrapped text', () => {
|
|
110
|
+
const data = createBitmapTextData();
|
|
111
|
+
expect(data.align).toBe('left');
|
|
112
|
+
expect(data.color).toBe(0xffffffff);
|
|
113
|
+
expect(data.glyphSource).toBeNull();
|
|
114
|
+
expect(data.letterSpacing).toBe(0);
|
|
115
|
+
expect(data.lineHeight).toBe(1);
|
|
116
|
+
expect(data.text).toBe('');
|
|
117
|
+
expect(data.wrapWidth).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('honors provided overrides', () => {
|
|
121
|
+
const data = createBitmapTextData({ align: 'right', text: 'x', wrapWidth: 50 });
|
|
122
|
+
expect(data.align).toBe('right');
|
|
123
|
+
expect(data.text).toBe('x');
|
|
124
|
+
expect(data.wrapWidth).toBe(50);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('createBitmapTextRuntime', () => {
|
|
129
|
+
it('starts with null bounds and no page batches', () => {
|
|
130
|
+
const runtime = createBitmapTextRuntime();
|
|
131
|
+
expect(runtime.localBoundsRectangle).toBeNull();
|
|
132
|
+
expect(runtime.quadBatches).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('getBitmapTextBounds', () => {
|
|
137
|
+
it('allocates a rectangle covering the laid-out glyphs', () => {
|
|
138
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB' });
|
|
139
|
+
updateBitmapText(text);
|
|
140
|
+
const bounds = getBitmapTextBounds(text);
|
|
141
|
+
expect(bounds.x).toBe(0);
|
|
142
|
+
expect(bounds.y).toBe(0);
|
|
143
|
+
expect(bounds.width).toBe(16); // B at x=10, region width 6 → right edge 16
|
|
144
|
+
expect(bounds.height).toBe(8);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('getBitmapTextQuadBatches', () => {
|
|
149
|
+
it('returns the backing batches in page order, matching the node children', () => {
|
|
150
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
151
|
+
const batches = getBitmapTextQuadBatches(text);
|
|
152
|
+
expect(batches).toHaveLength(1);
|
|
153
|
+
expect(batches[0]).toBe(getDisplayObjectRuntime(text).children![0]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('reserveBitmapText', () => {
|
|
158
|
+
it('grows the backing quad batch capacity', () => {
|
|
159
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
160
|
+
reserveBitmapText(text, 64);
|
|
161
|
+
expect(getQuadBatchCapacity(getBitmapTextQuadBatches(text)[0]!)).toBeGreaterThanOrEqual(64);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('setBitmapTextAlign', () => {
|
|
166
|
+
it('mutates the align field', () => {
|
|
167
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
168
|
+
setBitmapTextAlign(text, 'justify');
|
|
169
|
+
expect(text.data.align).toBe('justify');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('setBitmapTextColor', () => {
|
|
174
|
+
it('mutates the color field', () => {
|
|
175
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
176
|
+
setBitmapTextColor(text, 0x00ff00ff);
|
|
177
|
+
expect(text.data.color).toBe(0x00ff00ff);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('setBitmapTextGlyphSource', () => {
|
|
182
|
+
it('rebinds the glyph source', () => {
|
|
183
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
184
|
+
const next = createTestGlyphSource();
|
|
185
|
+
setBitmapTextGlyphSource(text, next);
|
|
186
|
+
expect(text.data.glyphSource).toBe(next);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('setBitmapTextLetterSpacing', () => {
|
|
191
|
+
it('mutates the letterSpacing field', () => {
|
|
192
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
193
|
+
setBitmapTextLetterSpacing(text, 3);
|
|
194
|
+
expect(text.data.letterSpacing).toBe(3);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('setBitmapTextLineHeight', () => {
|
|
199
|
+
it('mutates the lineHeight field', () => {
|
|
200
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
201
|
+
setBitmapTextLineHeight(text, 2);
|
|
202
|
+
expect(text.data.lineHeight).toBe(2);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('setBitmapTextText', () => {
|
|
207
|
+
it('mutates the text field', () => {
|
|
208
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
209
|
+
setBitmapTextText(text, 'changed');
|
|
210
|
+
expect(text.data.text).toBe('changed');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('setBitmapTextWrapWidth', () => {
|
|
215
|
+
it('mutates the wrapWidth field', () => {
|
|
216
|
+
const text = createBitmapText(createTestGlyphSource());
|
|
217
|
+
setBitmapTextWrapWidth(text, 200);
|
|
218
|
+
expect(text.data.wrapWidth).toBe(200);
|
|
219
|
+
setBitmapTextWrapWidth(text, null);
|
|
220
|
+
expect(text.data.wrapWidth).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { getDisplayObjectColorAdjustments } from '@flighthq/displayobject';
|
|
2
|
+
import type { ColorTransformAdjustment, GlyphEntry, GlyphSource, ImageResource } from '@flighthq/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createBitmapText, getBitmapTextBounds, getBitmapTextQuadBatches, setBitmapTextColor } from './bitmapText';
|
|
6
|
+
import { updateBitmapText } from './updateBitmapText';
|
|
7
|
+
|
|
8
|
+
// A deterministic single-page glyph source: every visible glyph is 6x8 with advance 10 and bearingY 8
|
|
9
|
+
// (so line tops sit at y=0), a space advances 5 with no pixels, and the pair (A, B) kerns by -2. All
|
|
10
|
+
// glyphs live on page 0, whose atlas image is a stub `ImageResource`.
|
|
11
|
+
function createTestGlyphSource(): GlyphSource {
|
|
12
|
+
const entries = new Map<number, GlyphEntry>();
|
|
13
|
+
const add = (cp: number, x: number): void => {
|
|
14
|
+
entries.set(cp, { advance: 10, bearingX: 0, bearingY: 8, height: 8, page: 0, width: 6, x, y: 0 });
|
|
15
|
+
};
|
|
16
|
+
add(0x41, 0); // A
|
|
17
|
+
add(0x42, 6); // B
|
|
18
|
+
entries.set(0x20, { advance: 5, bearingX: 0, bearingY: 0, height: 0, page: 0, width: 0, x: 0, y: 0 }); // space
|
|
19
|
+
const kerning = new Map<number, number>([[(0x41 << 16) | 0x42, -2]]);
|
|
20
|
+
const image = {} as ImageResource;
|
|
21
|
+
return {
|
|
22
|
+
getGlyphAtlasImage: (page = 0) => (page === 0 ? image : null),
|
|
23
|
+
getGlyphEntry: (cp) => entries.get(cp) ?? null,
|
|
24
|
+
getGlyphKerning: (l, r) => kerning.get((l << 16) | r) ?? 0,
|
|
25
|
+
getGlyphMetrics: () => ({ ascent: 8, descent: 2, lineGap: 0 }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A two-page glyph source: 'A' lives on page 0, 'B' on page 1, each page a DISTINCT stub image.
|
|
30
|
+
function createTwoPageGlyphSource(): { source: GlyphSource; page0Image: ImageResource; page1Image: ImageResource } {
|
|
31
|
+
const page0Image = {} as ImageResource;
|
|
32
|
+
const page1Image = {} as ImageResource;
|
|
33
|
+
const entries = new Map<number, GlyphEntry>([
|
|
34
|
+
[0x41, { advance: 10, bearingX: 0, bearingY: 8, height: 8, page: 0, width: 6, x: 0, y: 0 }], // A → page 0
|
|
35
|
+
[0x42, { advance: 10, bearingX: 0, bearingY: 8, height: 8, page: 1, width: 6, x: 3, y: 0 }], // B → page 1
|
|
36
|
+
]);
|
|
37
|
+
const source: GlyphSource = {
|
|
38
|
+
getGlyphAtlasImage: (page = 0) => (page === 0 ? page0Image : page === 1 ? page1Image : null),
|
|
39
|
+
getGlyphEntry: (cp) => entries.get(cp) ?? null,
|
|
40
|
+
getGlyphKerning: () => 0,
|
|
41
|
+
getGlyphMetrics: () => ({ ascent: 8, descent: 2, lineGap: 0 }),
|
|
42
|
+
};
|
|
43
|
+
return { source, page0Image, page1Image };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('updateBitmapText', () => {
|
|
47
|
+
it('places each glyph at its cumulative advance, applying kerning', () => {
|
|
48
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB' });
|
|
49
|
+
updateBitmapText(text);
|
|
50
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
51
|
+
expect(data.instanceCount).toBe(2);
|
|
52
|
+
expect(data.transforms[0]).toBe(0); // A.x
|
|
53
|
+
expect(data.transforms[1]).toBe(0); // A.y
|
|
54
|
+
expect(data.transforms[2]).toBe(8); // B.x = 10 (advance) - 2 (kerning)
|
|
55
|
+
expect(data.transforms[3]).toBe(0); // B.y
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('builds one atlas region per distinct glyph and references it by id', () => {
|
|
59
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB' });
|
|
60
|
+
updateBitmapText(text);
|
|
61
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
62
|
+
expect(data.atlas!.regions).toHaveLength(2);
|
|
63
|
+
expect(data.ids[0]).toBe(0);
|
|
64
|
+
expect(data.ids[1]).toBe(1);
|
|
65
|
+
expect(data.atlas!.regions[0].x).toBe(0); // A rect
|
|
66
|
+
expect(data.atlas!.regions[1].x).toBe(6); // B rect
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reuses one region for repeated codepoints', () => {
|
|
70
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AA' });
|
|
71
|
+
updateBitmapText(text);
|
|
72
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
73
|
+
expect(data.instanceCount).toBe(2);
|
|
74
|
+
expect(data.atlas!.regions).toHaveLength(1);
|
|
75
|
+
expect(data.ids[0]).toBe(0);
|
|
76
|
+
expect(data.ids[1]).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('starts a new line at the metric line advance on an explicit newline', () => {
|
|
80
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'A\nB' });
|
|
81
|
+
updateBitmapText(text);
|
|
82
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
83
|
+
expect(data.instanceCount).toBe(2);
|
|
84
|
+
expect(data.transforms[0]).toBe(0); // line 0 A.x
|
|
85
|
+
expect(data.transforms[1]).toBe(0); // line 0 A.y
|
|
86
|
+
expect(data.transforms[2]).toBe(0); // line 1 B.x
|
|
87
|
+
expect(data.transforms[3]).toBe(10); // line 1 B.y = 1 * (8 + 2 + 0)
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('scales the line advance by lineHeight', () => {
|
|
91
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'A\nB', lineHeight: 2 });
|
|
92
|
+
updateBitmapText(text);
|
|
93
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
94
|
+
expect(data.transforms[3]).toBe(20); // 2 * (8 + 2 + 0)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('word-wraps at a word boundary when the width is exceeded', () => {
|
|
98
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AA AA', wrapWidth: 30 });
|
|
99
|
+
updateBitmapText(text);
|
|
100
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
101
|
+
expect(data.instanceCount).toBe(4);
|
|
102
|
+
let firstLine = 0;
|
|
103
|
+
let secondLine = 0;
|
|
104
|
+
for (let i = 0; i < data.instanceCount; i++) {
|
|
105
|
+
if (data.transforms[i * 2 + 1] === 0) firstLine++;
|
|
106
|
+
else if (data.transforms[i * 2 + 1] === 10) secondLine++;
|
|
107
|
+
}
|
|
108
|
+
expect(firstLine).toBe(2);
|
|
109
|
+
expect(secondLine).toBe(2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('keeps words on one line and honors space advance when no wrap is set', () => {
|
|
113
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AA AA' });
|
|
114
|
+
updateBitmapText(text);
|
|
115
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
116
|
+
expect(data.instanceCount).toBe(4);
|
|
117
|
+
for (let i = 0; i < data.instanceCount; i++) expect(data.transforms[i * 2 + 1]).toBe(0);
|
|
118
|
+
expect(data.transforms[4]).toBe(25); // third glyph: word1 width 20 + space 5
|
|
119
|
+
expect(data.transforms[6]).toBe(35);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('offsets a line for center alignment', () => {
|
|
123
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB', wrapWidth: 100, align: 'center' });
|
|
124
|
+
updateBitmapText(text);
|
|
125
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
126
|
+
expect(data.transforms[0]).toBe(41); // (100 - 18) / 2
|
|
127
|
+
expect(data.transforms[2]).toBe(49); // 41 + 8
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('offsets a line for right alignment', () => {
|
|
131
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB', wrapWidth: 100, align: 'right' });
|
|
132
|
+
updateBitmapText(text);
|
|
133
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
134
|
+
expect(data.transforms[0]).toBe(82); // 100 - 18
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('stretches inter-word gaps for justify on non-final lines', () => {
|
|
138
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AA AA AA', wrapWidth: 50, align: 'justify' });
|
|
139
|
+
updateBitmapText(text);
|
|
140
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
141
|
+
// Line 0 = "AA AA" justified to 50: gap 5 grows by (50 - 45) / 1 = 5, so word 2 starts at 30.
|
|
142
|
+
expect(data.transforms[4]).toBe(30);
|
|
143
|
+
expect(data.transforms[6]).toBe(40);
|
|
144
|
+
// Line 1 = trailing "AA" (paragraph end) stays left.
|
|
145
|
+
const line1 = [];
|
|
146
|
+
for (let i = 0; i < data.instanceCount; i++)
|
|
147
|
+
if (data.transforms[i * 2 + 1] === 10) line1.push(data.transforms[i * 2]);
|
|
148
|
+
expect(line1).toEqual([0, 10]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('omits a missing glyph with no quad and no advance', () => {
|
|
152
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'A?B' });
|
|
153
|
+
updateBitmapText(text);
|
|
154
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
155
|
+
expect(data.instanceCount).toBe(2);
|
|
156
|
+
expect(data.transforms[0]).toBe(0); // A
|
|
157
|
+
expect(data.transforms[2]).toBe(8); // B placed as if '?' were absent (10 - 2 kerning)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('adds letterSpacing after each glyph advance', () => {
|
|
161
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB', letterSpacing: 1 });
|
|
162
|
+
updateBitmapText(text);
|
|
163
|
+
const data = getBitmapTextQuadBatches(text)[0]!.data;
|
|
164
|
+
expect(data.transforms[2]).toBe(9); // 10 + 1 (spacing) - 2 (kerning)
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('lays out an empty string as an empty batch without throwing', () => {
|
|
168
|
+
const text = createBitmapText(createTestGlyphSource(), { text: '' });
|
|
169
|
+
expect(() => updateBitmapText(text)).not.toThrow();
|
|
170
|
+
expect(getBitmapTextQuadBatches(text)[0]!.data.instanceCount).toBe(0);
|
|
171
|
+
const bounds = getBitmapTextBounds(text);
|
|
172
|
+
expect(bounds.width).toBe(0);
|
|
173
|
+
expect(bounds.height).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('reports laid-out bounds covering the glyph extent', () => {
|
|
177
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB' });
|
|
178
|
+
updateBitmapText(text);
|
|
179
|
+
const bounds = getBitmapTextBounds(text);
|
|
180
|
+
expect(bounds.x).toBe(0);
|
|
181
|
+
expect(bounds.y).toBe(0);
|
|
182
|
+
expect(bounds.width).toBe(14); // B at x=8, region width 6 → right edge 14
|
|
183
|
+
expect(bounds.height).toBe(8);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('sets a whole-batch color-transform tint only for non-white colors', () => {
|
|
187
|
+
const text = createBitmapText(createTestGlyphSource(), { text: 'AB' });
|
|
188
|
+
updateBitmapText(text);
|
|
189
|
+
expect(getDisplayObjectColorAdjustments(getBitmapTextQuadBatches(text)[0]!)).toBeNull();
|
|
190
|
+
setBitmapTextColor(text, 0xff0000ff);
|
|
191
|
+
updateBitmapText(text);
|
|
192
|
+
// One color-transform adjustment on the batch's runtime slot — a single whole-batch tint, not per-glyph.
|
|
193
|
+
const adjustments = getDisplayObjectColorAdjustments(getBitmapTextQuadBatches(text)[0]!);
|
|
194
|
+
expect(adjustments).not.toBeNull();
|
|
195
|
+
expect(adjustments!).toHaveLength(1);
|
|
196
|
+
const colorTransform = (adjustments![0] as ColorTransformAdjustment).colorTransform;
|
|
197
|
+
expect(colorTransform.redMultiplier).toBe(1);
|
|
198
|
+
expect(colorTransform.greenMultiplier).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('produces exactly one batch bound to the page-0 image for a single-page source', () => {
|
|
202
|
+
const source = createTestGlyphSource();
|
|
203
|
+
const text = createBitmapText(source, { text: 'AB' });
|
|
204
|
+
updateBitmapText(text);
|
|
205
|
+
const batches = getBitmapTextQuadBatches(text);
|
|
206
|
+
expect(batches).toHaveLength(1);
|
|
207
|
+
expect(batches[0].data.instanceCount).toBe(2);
|
|
208
|
+
expect(batches[0].data.atlas!.image).toBe(source.getGlyphAtlasImage(0));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('partitions glyphs into one QuadBatch per page, each bound to its own page image', () => {
|
|
212
|
+
const { source, page0Image, page1Image } = createTwoPageGlyphSource();
|
|
213
|
+
const text = createBitmapText(source, { text: 'AB' });
|
|
214
|
+
updateBitmapText(text);
|
|
215
|
+
const batches = getBitmapTextQuadBatches(text);
|
|
216
|
+
expect(batches).toHaveLength(2);
|
|
217
|
+
|
|
218
|
+
// Page 0 holds 'A' only, sampling page0Image; page 1 holds 'B' only, sampling page1Image.
|
|
219
|
+
const page0 = batches[0].data;
|
|
220
|
+
const page1 = batches[1].data;
|
|
221
|
+
expect(page0.instanceCount).toBe(1);
|
|
222
|
+
expect(page1.instanceCount).toBe(1);
|
|
223
|
+
expect(page0.atlas!.image).toBe(page0Image);
|
|
224
|
+
expect(page1.atlas!.image).toBe(page1Image);
|
|
225
|
+
expect(page0.atlas!.regions[0].x).toBe(0); // A's rect on page 0
|
|
226
|
+
expect(page1.atlas!.regions[0].x).toBe(3); // B's rect on page 1
|
|
227
|
+
expect(page0.transforms[0]).toBe(0); // A at pen origin
|
|
228
|
+
expect(page1.transforms[0]).toBe(10); // B after A's advance (no kerning)
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('spans every page when computing bounds', () => {
|
|
232
|
+
const { source } = createTwoPageGlyphSource();
|
|
233
|
+
const text = createBitmapText(source, { text: 'AB' });
|
|
234
|
+
updateBitmapText(text);
|
|
235
|
+
const bounds = getBitmapTextBounds(text);
|
|
236
|
+
expect(bounds.x).toBe(0);
|
|
237
|
+
expect(bounds.width).toBe(16); // B (page 1) at x=10, width 6 → right edge 16
|
|
238
|
+
expect(bounds.height).toBe(8);
|
|
239
|
+
});
|
|
240
|
+
});
|