Skip to content

Commit 8784ae1

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): migrate text
1 parent 3cb1ed0 commit 8784ae1

File tree

5 files changed

+572
-97
lines changed

5 files changed

+572
-97
lines changed

libs/soba/abstractions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './billboard/billboard';
2+
export * from './text/text';
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import {
2+
CUSTOM_ELEMENTS_SCHEMA,
3+
Component,
4+
DestroyRef,
5+
EventEmitter,
6+
Injector,
7+
Input,
8+
NgZone,
9+
Output,
10+
computed,
11+
effect,
12+
inject,
13+
} from '@angular/core';
14+
import { NgtArgs, NgtSignalStore, NgtStore, injectNgtRef, type NgtMesh } from 'angular-three';
15+
16+
// @ts-expect-error: no type def for troika-three-text
17+
import { Text, preloadFont } from 'troika-three-text';
18+
19+
export type NgtsTextState = {
20+
text: string;
21+
/** Font size, default: 1 */
22+
fontSize: number;
23+
anchorX: number | 'left' | 'center' | 'right';
24+
anchorY: number | 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom';
25+
sdfGlyphSize: number;
26+
font?: string;
27+
characters?: string;
28+
color?: THREE.ColorRepresentation;
29+
maxWidth?: number;
30+
lineHeight?: number;
31+
letterSpacing?: number;
32+
textAlign?: 'left' | 'right' | 'center' | 'justify';
33+
clipRect?: [number, number, number, number];
34+
depthOffset?: number;
35+
direction?: 'auto' | 'ltr' | 'rtl';
36+
overflowWrap?: 'normal' | 'break-word';
37+
whiteSpace?: 'normal' | 'overflowWrap' | 'nowrap';
38+
outlineWidth?: number | string;
39+
outlineOffsetX?: number | string;
40+
outlineOffsetY?: number | string;
41+
outlineBlur?: number | string;
42+
outlineColor?: THREE.ColorRepresentation;
43+
outlineOpacity?: number;
44+
strokeWidth?: number | string;
45+
strokeColor?: THREE.ColorRepresentation;
46+
strokeOpacity?: number;
47+
fillOpacity?: number;
48+
debugSDF?: boolean;
49+
};
50+
51+
declare global {
52+
interface HTMLElementTagNameMap {
53+
'ngts-text': NgtsTextState & NgtMesh;
54+
}
55+
}
56+
57+
@Component({
58+
selector: 'ngts-text',
59+
standalone: true,
60+
template: `
61+
<ngt-primitive
62+
ngtCompound
63+
*args="[troikaText]"
64+
[ref]="textRef"
65+
[text]="state().text"
66+
[anchorX]="state().anchorX"
67+
[anchorY]="state().anchorY"
68+
[font]="state().font"
69+
[fontSize]="state().fontSize"
70+
[sdfGlyphSize]="state().sdfGlyphSize"
71+
[characters]="state().characters"
72+
[color]="state().color"
73+
[maxWidth]="state().maxWidth"
74+
[lineHeight]="state().lineHeight"
75+
[letterSpacing]="state().letterSpacing"
76+
[textAlign]="state().textAlign"
77+
[clipRect]="state().clipRect"
78+
[depthOffset]="state().depthOffset"
79+
[direction]="state().direction"
80+
[overflowWrap]="state().overflowWrap"
81+
[whiteSpace]="state().whiteSpace"
82+
[outlineWidth]="state().outlineWidth"
83+
[outlineOffsetX]="state().outlineOffsetX"
84+
[outlineOffsetY]="state().outlineOffsetY"
85+
[outlineBlur]="state().outlineBlur"
86+
[outlineColor]="state().outlineColor"
87+
[outlineOpacity]="state().outlineOpacity"
88+
[strokeWidth]="state().strokeWidth"
89+
[strokeColor]="state().strokeColor"
90+
[strokeOpacity]="state().strokeOpacity"
91+
[fillOpacity]="state().fillOpacity"
92+
[debugSDF]="state().debugSDF"
93+
>
94+
<ng-content />
95+
</ngt-primitive>
96+
`,
97+
imports: [NgtArgs],
98+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
99+
})
100+
export class NgtsText extends NgtSignalStore<NgtsTextState> {
101+
@Input() textRef = injectNgtRef<Text>();
102+
103+
@Input({ required: true }) set text(text: string) {
104+
this.set({ text });
105+
}
106+
107+
@Input() set font(font: string) {
108+
this.set({ font });
109+
}
110+
111+
@Input() set fontSize(fontSize: number) {
112+
this.set({ fontSize });
113+
}
114+
115+
@Input() set anchorX(anchorX: number | 'left' | 'center' | 'right') {
116+
this.set({ anchorX });
117+
}
118+
119+
@Input() set anchorY(anchorY: number | 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom') {
120+
this.set({ anchorY });
121+
}
122+
123+
@Input() set sdfGlyphSize(sdfGlyphSize: number) {
124+
this.set({ sdfGlyphSize });
125+
}
126+
127+
@Input() set characters(characters: string) {
128+
this.set({ characters });
129+
}
130+
131+
@Input() set color(color: THREE.ColorRepresentation) {
132+
this.set({ color });
133+
}
134+
135+
@Input() set maxWidth(maxWidth: number) {
136+
this.set({ maxWidth });
137+
}
138+
139+
@Input() set lineHeight(lineHeight: number) {
140+
this.set({ lineHeight });
141+
}
142+
143+
@Input() set letterSpacing(letterSpacing: number) {
144+
this.set({ letterSpacing });
145+
}
146+
147+
@Input() set textAlign(textAlign: 'left' | 'right' | 'center' | 'justify') {
148+
this.set({ textAlign });
149+
}
150+
151+
@Input() set clipRect(clipRect: [number, number, number, number]) {
152+
this.set({ clipRect });
153+
}
154+
155+
@Input() set depthOffset(depthOffset: number) {
156+
this.set({ depthOffset });
157+
}
158+
159+
@Input() set direction(direction: 'auto' | 'ltr' | 'rtl') {
160+
this.set({ direction });
161+
}
162+
163+
@Input() set overflowWrap(overflowWrap: 'normal' | 'break-word') {
164+
this.set({ overflowWrap });
165+
}
166+
167+
@Input() set whiteSpace(whiteSpace: 'normal' | 'overflowWrap' | 'nowrap') {
168+
this.set({ whiteSpace });
169+
}
170+
171+
@Input() set outlineWidth(outlineWidth: number | string) {
172+
this.set({ outlineWidth });
173+
}
174+
175+
@Input() set outlineOffsetX(outlineOffsetX: number | string) {
176+
this.set({ outlineOffsetX });
177+
}
178+
179+
@Input() set outlineOffsetY(outlineOffsetY: number | string) {
180+
this.set({ outlineOffsetY });
181+
}
182+
183+
@Input() set outlineBlur(outlineBlur: number | string) {
184+
this.set({ outlineBlur });
185+
}
186+
187+
@Input() set outlineColor(outlineColor: THREE.ColorRepresentation) {
188+
this.set({ outlineColor });
189+
}
190+
191+
@Input() set outlineOpacity(outlineOpacity: number) {
192+
this.set({ outlineOpacity });
193+
}
194+
195+
@Input() set strokeWidth(strokeWidth: number | string) {
196+
this.set({ strokeWidth });
197+
}
198+
199+
@Input() set strokeColor(strokeColor: THREE.ColorRepresentation) {
200+
this.set({ strokeColor });
201+
}
202+
203+
@Input() set strokeOpacity(strokeOpacity: number) {
204+
this.set({ strokeOpacity });
205+
}
206+
207+
@Input() set fillOpacity(fillOpacity: number) {
208+
this.set({ fillOpacity });
209+
}
210+
211+
@Input() set debugSDF(debugSDF: boolean) {
212+
this.set({ debugSDF });
213+
}
214+
215+
@Output() sync = new EventEmitter<Text>();
216+
217+
readonly troikaText = new Text();
218+
219+
readonly #zone = inject(NgZone);
220+
readonly #injector = inject(Injector);
221+
readonly #store = inject(NgtStore);
222+
223+
readonly state = this.select();
224+
225+
constructor() {
226+
super({ fontSize: 1, sdfGlyphSize: 64, anchorX: 'center', anchorY: 'middle' });
227+
inject(DestroyRef).onDestroy(() => {
228+
this.troikaText.dispose();
229+
});
230+
}
231+
232+
ngOnInit() {
233+
this.#zone.runOutsideAngular(() => {
234+
this.#preloadFont();
235+
this.#syncText();
236+
});
237+
}
238+
239+
#preloadFont() {
240+
const trigger = computed(() => {
241+
const font = this.select('font');
242+
const characters = this.select('characters');
243+
return { font: font(), characters: characters() };
244+
});
245+
246+
effect(
247+
() => {
248+
const { font, characters } = trigger();
249+
const invalidate = this.#store.get('invalidate');
250+
preloadFont({ font, characters }, () => invalidate());
251+
},
252+
{ injector: this.#injector }
253+
);
254+
}
255+
256+
#syncText() {
257+
effect(
258+
() => {
259+
this.select()();
260+
const invalidate = this.#store.get('invalidate');
261+
this.troikaText.sync(() => {
262+
invalidate();
263+
if (this.sync.observed) {
264+
this.sync.emit(this.troikaText);
265+
}
266+
});
267+
},
268+
{ injector: this.#injector }
269+
);
270+
}
271+
}

0 commit comments

Comments
 (0)