Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/debug-layer/asset-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class AssetBrowserScene extends Scene {
txtNoAssets = new Text('globalThis.assets is not available.\nRun this example in the ExoJS playground.', { fillColor: C.dim, fontSize: 16 });
txtEmptyCat = new Text('(empty)', { fillColor: C.dimDark, fontSize: 13 });
txtNoSel = new Text('Select an asset from the list.', { fillColor: C.dimDark, fontSize: 14 });
txtMeta = new Text('', { fillColor: C.white, fontSize: 13 }, { maxWidth: PREVIEW_W - 80 });
txtMeta = new Text('', { fillColor: C.white, fontSize: 13, maxWidth: PREVIEW_W - 80 });
txtAudioIcon = new Text('', { fillColor: C.white, fontSize: 28 });
txtAnimPlay = new Text('', { fillColor: C.white, fontSize: 12 });
txtAnimFrame = new Text('', { fillColor: C.dim, fontSize: 11 });
Expand Down
2 changes: 1 addition & 1 deletion examples/debug-layer/asset-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class AssetBrowserScene extends Scene {
);
txtEmptyCat = new Text('(empty)', { fillColor: C.dimDark, fontSize: 13 });
txtNoSel = new Text('Select an asset from the list.', { fillColor: C.dimDark, fontSize: 14 });
txtMeta = new Text('', { fillColor: C.white, fontSize: 13 }, { maxWidth: PREVIEW_W - 80 });
txtMeta = new Text('', { fillColor: C.white, fontSize: 13, maxWidth: PREVIEW_W - 80 });
txtAudioIcon = new Text('', { fillColor: C.white, fontSize: 28 });
txtAnimPlay = new Text('', { fillColor: C.white, fontSize: 12 });
txtAnimFrame = new Text('', { fillColor: C.dim, fontSize: 11 });
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/dialog-system.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class DialogSystemScene extends Scene {
// Name plate sits just above the dialog body, like a classic VN UI.
this.namePlate = new Text(lines[0].speaker, { fillColor: new Color(255, 214, 120), fontSize: 26, fontWeight: 'bold' });
this.namePlate.setPosition(textX, height * 0.5);
this.box = new Text('', { fillColor: Color.white, fontSize: 32, lineHeight: 1.3 }, { maxWidth: width * 0.55 });
this.box = new Text('', { fillColor: Color.white, fontSize: 32, lineHeight: 1.3, maxWidth: width * 0.55 });
this.box.setPosition(textX, height * 0.56);
this.choicePrompt = new Text('', { fillColor: new Color(150, 220, 255), fontSize: 20 });
this.choicePrompt.setPosition(textX, height * 0.78);
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/dialog-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class DialogSystemScene extends Scene {
this.namePlate = new Text(lines[0].speaker, { fillColor: new Color(255, 214, 120), fontSize: 26, fontWeight: 'bold' });
this.namePlate.setPosition(textX, height * 0.5);

this.box = new Text('', { fillColor: Color.white, fontSize: 32, lineHeight: 1.3 }, { maxWidth: width * 0.55 });
this.box = new Text('', { fillColor: Color.white, fontSize: 32, lineHeight: 1.3, maxWidth: width * 0.55 });
this.box.setPosition(textX, height * 0.56);

this.choicePrompt = new Text('', { fillColor: new Color(150, 220, 255), fontSize: 20 });
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/typewriter-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class TypewriterTextScene extends Scene {
init(loader) {
const { width, height } = this.app.canvas;
this.sound = loader.get(Sound, 'tick');
this.text = new Text('', { fillColor: Color.white, fontSize: 40, lineHeight: 56 }, { maxWidth: 900 });
this.text = new Text('', { fillColor: Color.white, fontSize: 40, lineHeight: 56, maxWidth: 900 });
this.text.setAnchor(0, 0.5).setPosition(width * 0.12, height / 2);
this.state = { count: 0 };
// Shown while the browser still blocks audio (`app.audio.locked`); the
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/typewriter-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class TypewriterTextScene extends Scene {
const { width, height } = this.app.canvas;

this.sound = loader.get(Sound, 'tick');
this.text = new Text('', { fillColor: Color.white, fontSize: 40, lineHeight: 56 }, { maxWidth: 900 });
this.text = new Text('', { fillColor: Color.white, fontSize: 40, lineHeight: 56, maxWidth: 900 });
this.text.setAnchor(0, 0.5).setPosition(width * 0.12, height / 2);
this.state = { count: 0 };

Expand Down
4 changes: 2 additions & 2 deletions examples/text-fonts/multiline-and-wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class MultilineAndWrapScene extends Scene {
this.textA.setPosition(colX(0), bodyY);
this.titleB = new Text('Word wrap @ 360px — at word boundaries', { fillColor: titleColor, fontSize: 16 });
this.titleB.setPosition(colX(1), titleY);
this.textB = new Text(paragraph, { fillColor: Color.white, fontSize: 22 }, { maxWidth: 360 });
this.textB = new Text(paragraph, { fillColor: Color.white, fontSize: 22, maxWidth: 360 });
this.textB.setPosition(colX(1), bodyY);
this.titleC = new Text('Break words @ 280px — splits a token', { fillColor: titleColor, fontSize: 16 });
this.titleC.setPosition(colX(2), titleY);
this.textC = new Text(longToken, { fillColor: Color.white, fontSize: 22 }, { maxWidth: 280, breakWords: true });
this.textC = new Text(longToken, { fillColor: Color.white, fontSize: 22, maxWidth: 280, breakWords: true });
this.textC.setPosition(colX(2), bodyY);
}
draw(context) {
Expand Down
4 changes: 2 additions & 2 deletions examples/text-fonts/multiline-and-wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ class MultilineAndWrapScene extends Scene {

this.titleB = new Text('Word wrap @ 360px — at word boundaries', { fillColor: titleColor, fontSize: 16 });
this.titleB.setPosition(colX(1), titleY);
this.textB = new Text(paragraph, { fillColor: Color.white, fontSize: 22 }, { maxWidth: 360 });
this.textB = new Text(paragraph, { fillColor: Color.white, fontSize: 22, maxWidth: 360 });
this.textB.setPosition(colX(1), bodyY);

this.titleC = new Text('Break words @ 280px — splits a token', { fillColor: titleColor, fontSize: 16 });
this.titleC.setPosition(colX(2), titleY);
this.textC = new Text(longToken, { fillColor: Color.white, fontSize: 22 }, { maxWidth: 280, breakWords: true });
this.textC = new Text(longToken, { fillColor: Color.white, fontSize: 22, maxWidth: 280, breakWords: true });
this.textC.setPosition(colX(2), bodyY);
}

Expand Down
6 changes: 3 additions & 3 deletions site/src/content/api/text.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ memberCount: 91
tier: "stable"
sections: ["Import", "Constructors", "Methods", "Properties", "Events", "Source"]
sourcePath: "src/rendering/text/Text.ts"
sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/rendering/text/Text.ts#L46"
sourceUrl: "https://github.com/Exoridus/ExoJS/blob/main/src/rendering/text/Text.ts#L63"
---
## Import

Expand Down Expand Up @@ -47,7 +47,7 @@ options. Colour-glyph nodes use the `text-color` shader instead of `text-sdf`.

## Constructors

- `new(text: string, style?: TextStyleOptions | TextStyle, layout?: LayoutOptions, options: object): Text`
- `new(text: string, options: TextOptions): Text`

## Methods

Expand Down Expand Up @@ -150,4 +150,4 @@ options. Colour-glyph nodes use the `text-color` shader instead of `text-sdf`.

## Source

[src/rendering/text/Text.ts](https://github.com/Exoridus/ExoJS/blob/main/src/rendering/text/Text.ts#L46)
[src/rendering/text/Text.ts](https://github.com/Exoridus/ExoJS/blob/main/src/rendering/text/Text.ts#L63)
4 changes: 3 additions & 1 deletion src/core/serialization/coreSerializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ const textSerializer: NodeSerializer<Text> = {
read(data) {
const layout = typeof data.layout === 'object' && data.layout !== null ? (data.layout as LayoutOptions) : undefined;

return new Text(typeof data.text === 'string' ? data.text : '', deserializeStyleOptions(data.style), layout, {
return new Text(typeof data.text === 'string' ? data.text : '', {
...deserializeStyleOptions(data.style),
...layout,
colorGlyphs: data.colorGlyphs === true,
sdfRadius: typeof data.sdfRadius === 'number' ? data.sdfRadius : undefined,
});
Expand Down
14 changes: 7 additions & 7 deletions src/debug/PerformanceLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Graphics } from '#rendering/primitives/Graphics';
import type { RenderBackend } from '#rendering/RenderBackend';
import type { RenderNode } from '#rendering/RenderNode';
import { Text as Text } from '#rendering/text/Text';
import { TextStyle } from '#rendering/text/TextStyle';
import type { TextStyleOptions } from '#rendering/text/TextStyle';

import { DebugLayer, type DebugLayerViewMode } from './DebugLayer';

Expand Down Expand Up @@ -196,22 +196,22 @@ export class PerformanceLayer extends DebugLayer {
// -----------------------------------------------------------------------

private _build(): void {
const style = new TextStyle({
const style: TextStyleOptions = {
fontSize: textSize,
fontFamily: 'Arial',
fontWeight: 'normal',
fillColor: textColor,
});
};

const bg = new Graphics();

bg.fillColor = bgColor;
bg.drawRectangle(panelX, panelY, panelW, panelH);

this._textFps = new Text('FPS: -', style.clone());
this._textFrame = new Text('Frame: -', style.clone());
this._textDraws = new Text('Draws: -', style.clone());
this._textNodes = new Text('Nodes: -', style.clone());
this._textFps = new Text('FPS: -', style);
this._textFrame = new Text('Frame: -', style);
this._textDraws = new Text('Draws: -', style);
this._textNodes = new Text('Nodes: -', style);

this._textFps.x = panelX + 8;
this._textFps.y = panelY + 8;
Expand Down
8 changes: 4 additions & 4 deletions src/debug/PointerStackLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Graphics } from '#rendering/primitives/Graphics';
import type { RenderBackend } from '#rendering/RenderBackend';
import type { RenderNode } from '#rendering/RenderNode';
import { Text as Text } from '#rendering/text/Text';
import { TextStyle } from '#rendering/text/TextStyle';
import type { TextStyleOptions } from '#rendering/text/TextStyle';

import type { DebugLayerViewMode } from './DebugLayer';
import { DebugLayer } from './DebugLayer';
Expand Down Expand Up @@ -103,12 +103,12 @@ export class PointerStackLayer extends DebugLayer {
// -----------------------------------------------------------------------

private _build(): void {
const style = new TextStyle({
const style: TextStyleOptions = {
fontSize: textSize,
fontFamily: 'Arial',
fontWeight: 'normal',
fillColor: textColor,
});
};

this._bg = new Graphics();
this._bg.fillColor = bgColor;
Expand All @@ -122,7 +122,7 @@ export class PointerStackLayer extends DebugLayer {
const totalLines = maxEntries + 2; // header row + cursor row + entries

for (let i = 0; i < totalLines; i++) {
const t = new Text('', style.clone());
const t = new Text('', style);

t.x = panelPad;
t.y = panelPad + i * lineH;
Expand Down
11 changes: 5 additions & 6 deletions src/debug/RenderPassInspectorLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { RenderBackend } from '#rendering/RenderBackend';
import type { RenderNode } from '#rendering/RenderNode';
import { RenderPipeline } from '#rendering/RenderPipeline';
import { Text as Text } from '#rendering/text/Text';
import { TextStyle } from '#rendering/text/TextStyle';
import type { TextStyleOptions } from '#rendering/text/TextStyle';

import { DebugLayer, type DebugLayerViewMode } from './DebugLayer';

Expand Down Expand Up @@ -212,23 +212,22 @@ export class RenderPassInspectorLayer extends DebugLayer {
}

private _build(): void {
const style = new TextStyle({
const style: TextStyleOptions = {
fontSize: textSize,
fontFamily: 'Arial',
fontWeight: 'normal',
fillColor: textColor,
});
};

this._bg = new Graphics();
const headerStyle = style.clone();
headerStyle.fillColor = headerColor;
const headerStyle: TextStyleOptions = { ...style, fillColor: headerColor };
this._header = new Text('', headerStyle);
this._header.x = panelX + panelPadding;
this._header.y = panelY + panelPadding;

this._lines = [];
for (let i = 0; i < panelMaxLines; i++) {
const line = new Text('', style.clone());
const line = new Text('', style);
line.x = panelX + panelPadding;
line.y = panelY + panelPadding + panelLineH + i * panelLineH;
this._lines.push(line);
Expand Down
1 change: 1 addition & 0 deletions src/rendering/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export { GlyphAtlas } from '#rendering/text/GlyphAtlas';
export type { FontFormat, HTMLTextOptions } from '#rendering/text/HTMLText';
export { HTMLText } from '#rendering/text/HTMLText';
export type { LayoutOptions } from '#rendering/text/LayoutOptions';
export type { TextOptions } from '#rendering/text/Text';
export { Text } from '#rendering/text/Text';
export type { FontFamily, FontRegistry, FontWeight, GradientAxis, StyleChangeHint, TextStyleOptions } from '#rendering/text/TextStyle';
export { TextStyle } from '#rendering/text/TextStyle';
Expand Down
29 changes: 22 additions & 7 deletions src/rendering/text/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ import type { TextPageQuads, TextSize } from './types';

export type { TextPageQuads };

/**
* Construction options for a {@link Text} node — a flat merge of visual
* {@link TextStyleOptions} (appearance) and {@link LayoutOptions} (flow /
* overflow), plus two construction-only flags. The two source interfaces share
* no keys, so the flat shape is unambiguous.
*
* ```ts
* const label = new Text('Hello', { fillColor, fontSize: 24, maxWidth: 360 });
* ```
*/
export interface TextOptions extends TextStyleOptions, LayoutOptions {
/** Use a colour-glyph (emoji) atlas + the `text-color` shader. Construction-only. */
colorGlyphs?: boolean;
/** SDF buffer radius in pixels. Construction-only. */
sdfRadius?: number;
}

/**
* GPU-accelerated text node that rasterizes individual glyphs into a shared
* per-font-variant {@link GlyphAtlas} using the SDF (Signed Distance Field)
Expand Down Expand Up @@ -59,17 +76,15 @@ export class Text extends AbstractText {
private _pageQuads: TextPageQuads[] = [];
private _textBounds: TextSize = { width: 0, height: 0 };

public constructor(text: string, style?: TextStyle | TextStyleOptions, layout?: LayoutOptions, options: { colorGlyphs?: boolean; sdfRadius?: number } = {}) {
public constructor(text: string, options: TextOptions = {}) {
super(text);
this._style = style instanceof TextStyle ? style : new TextStyle(style);
this._layout = layout ?? {};
this._style = new TextStyle(options);
this._layout = options;
this._colorGlyphs = options.colorGlyphs ?? false;
this._sdfRadius = options.sdfRadius ?? SDF_RADIUS;

if (!(style instanceof TextStyle) && style !== undefined) {
const face = this._extractFace(style);
if (face !== null) this._faceLoadPromise = this._loadFace(face);
}
const face = this._extractFace(options);
if (face !== null) this._faceLoadPromise = this._loadFace(face);

this._rebuild('font');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ exports[`root index type-level export inventory > all exported symbols with kind
"TextAsset: class",
"TextFactory: class",
"TextLayoutStyle: interface",
"TextOptions: interface",
"TextPageQuads: interface",
"TextSize: interface",
"TextStyle: class",
Expand Down
4 changes: 2 additions & 2 deletions test/rendering/browser/webgl2-text-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ describe('Text layout WebGL2 browser', () => {
test('"ABC" lays out as horizontally separated, non-overlapping glyphs', async () => {
const backend = await createBackend(192, 64);
// Wide letterSpacing guarantees a visible gap between glyphs regardless of the font.
const text = new Text('ABC', { fillColor: Color.white, fontSize: 30 }, { letterSpacing: 18 });
const text = new Text('ABC', { fillColor: Color.white, fontSize: 30, letterSpacing: 18 });

try {
// Drives the real in-browser GPU path (real atlas texture, real shaders);
Expand Down Expand Up @@ -322,7 +322,7 @@ describe('Text layout WebGL2 browser', () => {
test('wrapped text splits across at least two vertical line bands', async () => {
const backend = await createBackend(128, 128);
// "AAA BBB" cannot fit both words within 56px → wraps to two lines.
const text = new Text('AAA BBB', { fillColor: Color.white, fontSize: 24, lineHeight: 1.6 }, { maxWidth: 56 });
const text = new Text('AAA BBB', { fillColor: Color.white, fontSize: 24, lineHeight: 1.6, maxWidth: 56 });

try {
render(backend, text);
Expand Down
17 changes: 9 additions & 8 deletions test/rendering/text/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ describe('Text', () => {

test('style getter returns the current TextStyle', () => {
const style = new TextStyle({ fontSize: 20 });
const text = new Text('Hi', style);
const text = new Text('Hi');
text.style = style;

expect(text.style).toBe(style);
});
Expand Down Expand Up @@ -179,8 +180,8 @@ describe('Text', () => {
});

test('update() with tint-only hint does not rebuild geometry', () => {
const style = new TextStyle({ fontSize: 16 });
const text = new Text('Hi', style);
const text = new Text('Hi', { fontSize: 16 });
const style = text.style;
const quadsBefore = text.pageQuads[0];

// Consume initial dirty from constructor
Expand All @@ -195,8 +196,8 @@ describe('Text', () => {
});

test('update() triggers rebuild for layout hint', () => {
const style = new TextStyle({ fontSize: 16 });
const text = new Text('Hi', style);
const text = new Text('Hi', { fontSize: 16 });
const style = text.style;
const quadsBefore = text.pageQuads[0];

style.fontSize = 32; // layout hint
Expand All @@ -207,8 +208,8 @@ describe('Text', () => {
});

test('style property mutations are deferred to update()', () => {
const style = new TextStyle({ fontSize: 16 });
const text = new Text('Hi', style);
const text = new Text('Hi', { fontSize: 16 });
const style = text.style;
const quadsBefore = text.pageQuads[0];

style.fontFamily = 'Georgia'; // font hint — must NOT rebuild immediately
Expand All @@ -221,7 +222,7 @@ describe('Text', () => {

test('colorGlyphs flag is accessible', () => {
const normal = new Text('Hi');
const emoji = new Text('👋', undefined, undefined, { colorGlyphs: true });
const emoji = new Text('👋', { colorGlyphs: true });

expect(normal.colorGlyphs).toBe(false);
expect(emoji.colorGlyphs).toBe(true);
Expand Down
6 changes: 3 additions & 3 deletions test/rendering/webgpu-backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ describe('WebGpuBackend', () => {
installCoreRenderers(manager);

// "Hi" → 2 glyphs → 2 quads → 12 indices, batched into 1 drawIndexed
const text = new Text('Hi', new TextStyle({ fontSize: 16 }));
const text = new Text('Hi', { fontSize: 16 });

await manager.initialize();

Expand Down Expand Up @@ -1278,7 +1278,7 @@ describe('WebGpuBackend', () => {
} as unknown as Application;
const manager = new WebGpuBackend(app);
installCoreRenderers(manager);
const text = new Text('Hi', new TextStyle({ fontSize: 16 }));
const text = new Text('Hi', { fontSize: 16 });
const firstQuads = text.pageQuads[0];

await manager.initialize();
Expand Down Expand Up @@ -1340,7 +1340,7 @@ describe('WebGpuBackend', () => {
} as unknown as Application;
const manager = new WebGpuBackend(app);
installCoreRenderers(manager);
const text = new Text('Hi', new TextStyle({ fontSize: 16 }));
const text = new Text('Hi', { fontSize: 16 });
const firstQuads = text.pageQuads[0];

await manager.initialize();
Expand Down
Loading