Skip to content

Commit 4d22f99

Browse files
committed
text
1 parent ca38f8e commit 4d22f99

File tree

6 files changed

+23638
-30635
lines changed

6 files changed

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

libs/soba/src/abstractions/billboard.stories.ts

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core';
22
import { Meta, moduleMetadata } from '@storybook/angular';
33
import { NgtArgs } from 'angular-three';
4-
import { NgtsBillboard } from 'angular-three-soba/abstractions';
4+
import { NgtsBillboard, NgtsText } from 'angular-three-soba/abstractions';
55
// import { NgtsOrbitControls } from 'angular-three-soba/controls';
66
import { BoxGeometry, ConeGeometry, PlaneGeometry } from 'three';
77
import { makeStoryObject, StorybookSetup } from '../setup-canvas';
@@ -20,7 +20,7 @@ import { makeStoryObject, StorybookSetup } from '../setup-canvas';
2020
schemas: [CUSTOM_ELEMENTS_SCHEMA],
2121
})
2222
class Cone {
23-
@Input() args: ConstructorParameters<typeof ConeGeometry> = [];
23+
@Input() args: ConstructorParameters<typeof ConeGeometry> = [1, 1];
2424
@Input() color = 'white';
2525
}
2626

@@ -39,7 +39,7 @@ class Cone {
3939
})
4040
class Box {
4141
@Input() position = [0, 0, 0];
42-
@Input() args: ConstructorParameters<typeof BoxGeometry> = [];
42+
@Input() args: ConstructorParameters<typeof BoxGeometry> = [1, 1, 1];
4343
@Input() color = 'white';
4444
}
4545

@@ -61,46 +61,46 @@ class Plane {
6161
@Input() color = 'white';
6262
}
6363

64-
// @Component({
65-
// standalone: true,
66-
// template: `
67-
// <ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0.5, 2.05, 0.5]">
68-
// <ngts-text text="box" [fontSize]="1" [outlineWidth]="'5%'" [outlineColor]="'#000'" [outlineOpacity]="1" />
69-
// </ngts-billboard>
70-
// <BillboardBox [position]="[0.5, 1, 0.5]" color="red">
71-
// <ngt-mesh-standard-material />
72-
// </BillboardBox>
73-
// <ngt-group [position]="[-2.5, -3, -1]">
74-
// <ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 1.05, 0]">
75-
// <ngts-text
76-
// text="cone"
77-
// [fontSize]="1"
78-
// [outlineWidth]="'5%'"
79-
// [outlineColor]="'#000'"
80-
// [outlineOpacity]="1"
81-
// />
82-
// </ngts-billboard>
83-
// <BillboardCone color="green">
84-
// <ngt-mesh-standard-material />
85-
// </BillboardCone>
86-
// </ngt-group>
87-
// <ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 0, -5]">
88-
// <BillboardPlane [args]="[2, 2]" color="#000066">
89-
// <ngt-mesh-standard-material />
90-
// </BillboardPlane>
91-
// </ngts-billboard>
92-
//
93-
// <ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" />
94-
// `,
95-
// imports: [NgtsBillboard, NgtsOrbitControls, NgtsText, Cone, Box, Plane],
96-
// schemas: [CUSTOM_ELEMENTS_SCHEMA],
97-
// })
98-
// class TextBillboardStory {
99-
// @Input() follow = true;
100-
// @Input() lockX = false;
101-
// @Input() lockY = false;
102-
// @Input() lockZ = false;
103-
// }
64+
@Component({
65+
standalone: true,
66+
template: `
67+
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0.5, 2.05, 0.5]">
68+
<ngts-text text="box" [fontSize]="1" [outlineWidth]="'5%'" [outlineColor]="'#000'" [outlineOpacity]="1" />
69+
</ngts-billboard>
70+
<BillboardBox [position]="[0.5, 1, 0.5]" color="red">
71+
<ngt-mesh-standard-material />
72+
</BillboardBox>
73+
<ngt-group [position]="[-2.5, -3, -1]">
74+
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 1.05, 0]">
75+
<ngts-text
76+
text="cone"
77+
[fontSize]="1"
78+
[outlineWidth]="'5%'"
79+
[outlineColor]="'#000'"
80+
[outlineOpacity]="1"
81+
/>
82+
</ngts-billboard>
83+
<BillboardCone color="green">
84+
<ngt-mesh-standard-material />
85+
</BillboardCone>
86+
</ngt-group>
87+
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 0, -5]">
88+
<BillboardPlane [args]="[2, 2]" color="#000066">
89+
<ngt-mesh-standard-material />
90+
</BillboardPlane>
91+
</ngts-billboard>
92+
93+
<!-- <ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" /> -->
94+
`,
95+
imports: [NgtsBillboard, NgtsText, Cone, Box, Plane],
96+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
97+
})
98+
class TextBillboardStory {
99+
@Input() follow = true;
100+
@Input() lockX = false;
101+
@Input() lockY = false;
102+
@Input() lockZ = false;
103+
}
104104

105105
@Component({
106106
standalone: true,
@@ -144,7 +144,8 @@ export const Default = makeStoryObject(DefaultBillboardStory, {
144144
canvasOptions,
145145
argsOptions: { follow: true, lockX: false, lockY: false, lockZ: false },
146146
});
147-
// export const Text = makeStoryObject(TextBillboardStory, {
148-
// canvasOptions,
149-
// argsOptions: { follow: true, lockX: false, lockY: false, lockZ: false },
150-
// });
147+
148+
export const Text = makeStoryObject(TextBillboardStory, {
149+
canvasOptions,
150+
argsOptions: { follow: true, lockX: false, lockY: false, lockZ: false },
151+
});

0 commit comments

Comments
 (0)