Skip to content

Commit 91c51cf

Browse files
committed
feat(soba): add billboard
1 parent 3d6362c commit 91c51cf

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

libs/soba/abstractions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './lib/billboard';
12
export * from './lib/catmull-rom-line';
23
export * from './lib/cubic-bezier-line';
34
export * from './lib/edges';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
CUSTOM_ELEMENTS_SCHEMA,
5+
ElementRef,
6+
input,
7+
viewChild,
8+
} from '@angular/core';
9+
import { extend, injectBeforeRender, NgtGroup, omit } from 'angular-three';
10+
import { mergeInputs } from 'ngxtension/inject-inputs';
11+
import { Group, Quaternion } from 'three';
12+
13+
export interface NgtsBillboardOptions extends Partial<NgtGroup> {
14+
follow?: boolean;
15+
lockX?: boolean;
16+
lockY?: boolean;
17+
lockZ?: boolean;
18+
}
19+
20+
const defaultOptions: NgtsBillboardOptions = {
21+
follow: true,
22+
lockX: false,
23+
lockY: false,
24+
lockZ: false,
25+
};
26+
27+
@Component({
28+
selector: 'ngts-billboard',
29+
standalone: true,
30+
template: `
31+
<ngt-group #group [parameters]="parameters()">
32+
<ngt-group #inner>
33+
<ng-content />
34+
</ngt-group>
35+
</ngt-group>
36+
`,
37+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
38+
changeDetection: ChangeDetectionStrategy.OnPush,
39+
})
40+
export class NgtsBillboard {
41+
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
42+
parameters = omit(this.options, ['follow', 'lockX', 'lockY', 'lockZ']);
43+
44+
groupRef = viewChild.required<ElementRef<Group>>('group');
45+
innerRef = viewChild.required<ElementRef<Group>>('inner');
46+
47+
constructor() {
48+
extend({ Group });
49+
50+
const q = new Quaternion();
51+
injectBeforeRender(({ camera }) => {
52+
const [{ follow, lockX, lockY, lockZ }, group, inner] = [
53+
this.options(),
54+
this.groupRef().nativeElement,
55+
this.innerRef().nativeElement,
56+
];
57+
58+
if (!follow || !group) return;
59+
60+
// save previous rotation in case we're locking an axis
61+
const prevRotation = group.rotation.clone();
62+
63+
// always face the camera
64+
group.updateMatrix();
65+
group.updateWorldMatrix(false, false);
66+
group.getWorldQuaternion(q);
67+
camera.getWorldQuaternion(inner.quaternion).premultiply(q.invert());
68+
69+
// readjust any axis that is locked
70+
if (lockX) group.rotation.x = prevRotation.x;
71+
if (lockY) group.rotation.y = prevRotation.y;
72+
if (lockZ) group.rotation.z = prevRotation.z;
73+
});
74+
}
75+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, input } from '@angular/core';
2+
import { Meta } from '@storybook/angular';
3+
import { NgtArgs } from 'angular-three';
4+
import { NgtsBillboard, NgtsText } from 'angular-three-soba/abstractions';
5+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
6+
import { makeDecorators, makeStoryObject } from '../setup-canvas';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngts-billboard
12+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [0.5, 2.05, 0.5] }"
13+
>
14+
<ngts-text
15+
text="hello"
16+
[options]="{ fontSize: 1, outlineWidth: '5%', outlineColor: '#000000', outlineOpacity: 1 }"
17+
/>
18+
</ngts-billboard>
19+
<ngt-mesh [position]="[0.5, 1, 0.5]">
20+
<ngt-box-geometry />
21+
<ngt-mesh-standard-material color="red" />
22+
</ngt-mesh>
23+
<ngt-group [position]="[-2.5, -3, -1]">
24+
<ngts-billboard
25+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [0, 1.05, 0] }"
26+
>
27+
<ngts-text
28+
text="cone"
29+
[options]="{ fontSize: 1, outlineWidth: '5%', outlineColor: '#000000', outlineOpacity: 1 }"
30+
/>
31+
</ngts-billboard>
32+
<ngt-mesh>
33+
<ngt-cone-geometry />
34+
<ngt-mesh-standard-material color="green" />
35+
</ngt-mesh>
36+
</ngt-group>
37+
38+
<ngts-billboard
39+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [0, 0, -5] }"
40+
>
41+
<ngt-mesh>
42+
<ngt-plane-geometry />
43+
<ngt-mesh-standard-material color="#000066" />
44+
</ngt-mesh>
45+
</ngts-billboard>
46+
47+
<ngts-orbit-controls [options]="{ enablePan: true, zoomSpeed: 0.5 }" />
48+
`,
49+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
50+
changeDetection: ChangeDetectionStrategy.OnPush,
51+
imports: [NgtsBillboard, NgtsText, NgtsOrbitControls],
52+
})
53+
class TextBillboardStory {
54+
follow = input(true);
55+
lockX = input(false);
56+
lockY = input(false);
57+
lockZ = input(false);
58+
}
59+
60+
@Component({
61+
standalone: true,
62+
template: `
63+
<ngts-billboard
64+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [-4, -2, 0] }"
65+
>
66+
<ngt-mesh>
67+
<ngt-plane-geometry *args="[3, 2]" />
68+
<ngt-value rawValue="red" attach="material.color" />
69+
</ngt-mesh>
70+
</ngts-billboard>
71+
72+
<ngts-billboard
73+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [-4, 2, 0] }"
74+
>
75+
<ngt-mesh>
76+
<ngt-plane-geometry *args="[3, 2]" />
77+
<ngt-value rawValue="orange" attach="material.color" />
78+
</ngt-mesh>
79+
</ngts-billboard>
80+
81+
<ngts-billboard
82+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [0, 0, 0] }"
83+
>
84+
<ngt-mesh>
85+
<ngt-plane-geometry *args="[3, 2]" />
86+
<ngt-value rawValue="green" attach="material.color" />
87+
</ngt-mesh>
88+
</ngts-billboard>
89+
90+
<ngts-billboard
91+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [4, -2, 0] }"
92+
>
93+
<ngt-mesh>
94+
<ngt-plane-geometry *args="[3, 2]" />
95+
<ngt-value rawValue="blue" attach="material.color" />
96+
</ngt-mesh>
97+
</ngts-billboard>
98+
99+
<ngts-billboard
100+
[options]="{ follow: follow(), lockZ: lockZ(), lockY: lockY(), lockX: lockX(), position: [4, 2, 0] }"
101+
>
102+
<ngt-mesh>
103+
<ngt-plane-geometry *args="[3, 2]" />
104+
<ngt-value rawValue="yellow" attach="material.color" />
105+
</ngt-mesh>
106+
</ngts-billboard>
107+
108+
<ngts-orbit-controls [options]="{ enablePan: true, zoomSpeed: 0.5 }" />
109+
`,
110+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
111+
changeDetection: ChangeDetectionStrategy.OnPush,
112+
imports: [NgtsBillboard, NgtsOrbitControls, NgtArgs],
113+
})
114+
class DefaultBillboardStory {
115+
follow = input(true);
116+
lockX = input(false);
117+
lockY = input(false);
118+
lockZ = input(false);
119+
}
120+
121+
export default {
122+
title: 'Abstractions/Billboard',
123+
decorators: makeDecorators(),
124+
} as Meta;
125+
126+
export const Default = makeStoryObject(DefaultBillboardStory, {
127+
canvasOptions: { camera: { position: [0, 0, 10] }, controls: false },
128+
argsOptions: { follow: true, lockX: false, lockY: false, lockZ: false },
129+
});
130+
131+
export const Text = makeStoryObject(TextBillboardStory, {
132+
canvasOptions: { camera: { position: [0, 0, 10] }, controls: false },
133+
argsOptions: { follow: true, lockX: false, lockY: false, lockZ: false },
134+
});

0 commit comments

Comments
 (0)