Skip to content

Commit 81084f7

Browse files
committed
docs: add camera-scroll example and adjust animation api
1 parent 3ca940d commit 81084f7

File tree

12 files changed

+454
-10
lines changed

12 files changed

+454
-10
lines changed

apps/kitchen-sink/src/app/soba/aquarium/aquarium.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
22
import { NgtCanvas } from 'angular-three';
33
import { Experience } from './experience';
44

5-
// <Canvas shadows camera={{ position: [30, 0, -3], fov: 35, near: 1, far: 50 }} gl={{ stencil: true }}>
6-
75
@Component({
86
template: `
97
<ngt-canvas

apps/kitchen-sink/src/app/soba/aquarium/turtle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export class Turtle {
3333

3434
constructor() {
3535
effect(() => {
36-
if (!this.animations.ready()) return;
36+
if (!this.animations.isReady) return;
3737

3838
this.animations.mixer.timeScale = 0.5;
39-
this.animations.actions['Swim Cycle']?.play();
39+
this.animations.actions['Swim Cycle'].play();
4040
});
4141

4242
injectBeforeRender(({ clock }) => {

apps/kitchen-sink/src/app/soba/basic/experience.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ export class BotAnimations {
4343
// that the animations are referring to.
4444
const animationsApi = injectAnimations(this.animations, host);
4545
effect((onCleanup) => {
46-
if (animationsApi.ready()) {
46+
if (animationsApi.isReady) {
4747
const actionName = selectedAction();
48-
// animationsApi.actions[actionName];
49-
animationsApi.actions[actionName]?.reset().fadeIn(0.5).play();
48+
animationsApi.actions[actionName].reset().fadeIn(0.5).play();
5049
onCleanup(() => {
51-
animationsApi.actions[actionName]?.fadeOut(0.5);
50+
animationsApi.actions[actionName].fadeOut(0.5);
5251
});
5352
}
5453
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ChangeDetectionStrategy, Component, ElementRef, inject, InjectionToken } from '@angular/core';
2+
import { NgtCanvas } from 'angular-three';
3+
import { Experience } from './experience';
4+
import { Overlay } from './overlay';
5+
6+
export const SCROLL = new InjectionToken('scroll', {
7+
factory: () => ({ value: 0 }),
8+
});
9+
10+
@Component({
11+
template: `
12+
<ngt-canvas shadows [eventSource]="host" [sceneGraph]="sceneGraph" />
13+
<app-overlay />
14+
`,
15+
imports: [NgtCanvas, Overlay],
16+
changeDetection: ChangeDetectionStrategy.OnPush,
17+
host: { class: 'camera-scroll-soba' },
18+
styles: `
19+
@import url('https://rsms.me/inter/inter.css');
20+
21+
:host {
22+
position: fixed;
23+
height: 100%;
24+
width: 100%;
25+
overflow: hidden;
26+
overscroll-behavior-y: none;
27+
background: radial-gradient(circle at bottom center, #212121 0%, #101010 80%);
28+
font-family: 'Inter var', sans-serif;
29+
-webkit-font-smoothing: antialiased;
30+
}
31+
`,
32+
})
33+
export default class CameraScroll {
34+
protected sceneGraph = Experience;
35+
protected host = inject<ElementRef<HTMLElement>>(ElementRef);
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2+
import { NgtsEnvironment } from 'angular-three-soba/staging';
3+
import { Model } from './model';
4+
5+
@Component({
6+
template: `
7+
<ngt-ambient-light [intensity]="Math.PI" />
8+
<app-model />
9+
<ngts-environment [options]="{ preset: 'city' }" />
10+
`,
11+
imports: [Model, NgtsEnvironment],
12+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
host: { class: 'camera-scroll-soba-experience' },
15+
})
16+
export class Experience {
17+
protected readonly Math = Math;
18+
}
Binary file not shown.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**Auto-generated by: https://github.com/angular-threejs/gltf
2+
Command: npx angular-three-gltf&#64;1.1.11 apps/kitchen-sink/src/app/soba/camera-scroll/model.glb -o apps/kitchen-sink/src/app/soba/camera-scroll/model.ts --selector app-model --name Model --transform Size: apps/kitchen-sink/src/app/soba/camera-scroll/model.glb [673.3KB] > /Users/nartc/code/github/angular-threejs/angular-three/apps/kitchen-sink/src/app/soba/camera-scroll/model-transformed.glb [235.96KB] (65%)
3+
**/
4+
5+
import {
6+
ChangeDetectionStrategy,
7+
Component,
8+
CUSTOM_ELEMENTS_SCHEMA,
9+
effect,
10+
ElementRef,
11+
inject,
12+
signal,
13+
viewChild,
14+
} from '@angular/core';
15+
import { injectBeforeRender, NgtArgs, NgtThreeEvent } from 'angular-three';
16+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
17+
import { injectGLTF } from 'angular-three-soba/loaders';
18+
import { injectAnimations, NgtsAnimationApi, NgtsAnimationClips } from 'angular-three-soba/misc';
19+
import type * as THREE from 'three';
20+
import { BufferGeometry, Color, Group, MathUtils, Mesh, MeshStandardMaterial } from 'three';
21+
import { GLTF } from 'three-stdlib';
22+
import { SCROLL } from './camera-scroll';
23+
24+
import { DOCUMENT } from '@angular/common';
25+
import modelUrl from './model-transformed.glb';
26+
27+
type ActionName = 'CameraAction.005';
28+
type ModelAnimationClips = NgtsAnimationClips<ActionName>;
29+
export type ModelAnimationApi = NgtsAnimationApi<ModelAnimationClips> | null;
30+
export type ModelGLTFResult = GLTF & {
31+
animations: ModelAnimationClips[];
32+
nodes: {
33+
Headphones: THREE.Mesh;
34+
Notebook: THREE.Mesh;
35+
Rocket003: THREE.Mesh;
36+
Roundcube001: THREE.Mesh;
37+
Table: THREE.Mesh;
38+
VR_Headset: THREE.Mesh;
39+
Zeppelin: THREE.Mesh;
40+
};
41+
materials: {
42+
M_Headphone: THREE.MeshStandardMaterial;
43+
M_Notebook: THREE.MeshStandardMaterial;
44+
M_Rocket: THREE.MeshStandardMaterial;
45+
M_Roundcube: THREE.MeshStandardMaterial;
46+
M_Table: THREE.MeshStandardMaterial;
47+
M_Headset: THREE.MeshStandardMaterial;
48+
M_Zeppelin: THREE.MeshStandardMaterial;
49+
};
50+
};
51+
52+
@Component({
53+
selector: 'app-model',
54+
template: `
55+
@if (gltf(); as gltf) {
56+
<ngt-group #model [dispose]="null">
57+
<ngt-group
58+
name="Scene"
59+
[position]="[0.061, 4.042, 0.346]"
60+
[scale]="0.25"
61+
(pointerover)="onPointerOver($event)"
62+
(pointerout)="onPointerOut($event)"
63+
>
64+
<ngt-mesh
65+
name="Headphones"
66+
[geometry]="gltf.nodes.Headphones.geometry"
67+
[material]="gltf.materials.M_Headphone"
68+
[castShadow]="true"
69+
[receiveShadow]="true"
70+
/>
71+
<ngt-mesh
72+
name="Notebook"
73+
[geometry]="gltf.nodes.Notebook.geometry"
74+
[material]="gltf.materials.M_Notebook"
75+
[castShadow]="true"
76+
[receiveShadow]="true"
77+
/>
78+
<ngt-mesh
79+
name="Rocket003"
80+
[geometry]="gltf.nodes.Rocket003.geometry"
81+
[material]="gltf.materials.M_Rocket"
82+
[castShadow]="true"
83+
[receiveShadow]="true"
84+
/>
85+
<ngt-mesh
86+
name="Roundcube001"
87+
[geometry]="gltf.nodes.Roundcube001.geometry"
88+
[material]="gltf.materials.M_Roundcube"
89+
[castShadow]="true"
90+
[receiveShadow]="true"
91+
/>
92+
<ngt-mesh
93+
name="Table"
94+
[geometry]="gltf.nodes.Table.geometry"
95+
[material]="gltf.materials.M_Table"
96+
[castShadow]="true"
97+
[receiveShadow]="true"
98+
/>
99+
<ngt-mesh
100+
name="VR_Headset"
101+
[geometry]="gltf.nodes.VR_Headset.geometry"
102+
[material]="gltf.materials.M_Headset"
103+
[castShadow]="true"
104+
[receiveShadow]="true"
105+
/>
106+
<ngt-mesh
107+
name="Zeppelin"
108+
[geometry]="gltf.nodes.Zeppelin.geometry"
109+
[material]="gltf.materials.M_Zeppelin"
110+
[castShadow]="true"
111+
[receiveShadow]="true"
112+
/>
113+
</ngt-group>
114+
115+
<ngt-group name="Camera" [position]="[-1.78, 2.04, 23.58]" [rotation]="[1.62, 0.01, 0.11]">
116+
<ngts-perspective-camera
117+
[options]="{ makeDefault: true, far: 100, near: 0.1, fov: 28, rotation: [-Math.PI / 2, 0, 0] }"
118+
>
119+
<ngt-directional-light * [position]="[10, 20, 15]" [castShadow]="true" [intensity]="Math.PI * 2">
120+
<ngt-value [rawValue]="-0.0001" attach="shadow.bias" />
121+
<ngt-vector2 *args="[1024, 1024]" attach="shadow.mapSize" />
122+
<ngt-value [rawValue]="8" attach="shadow.camera.right" />
123+
<ngt-value [rawValue]="8" attach="shadow.camera.top" />
124+
<ngt-value [rawValue]="-8" attach="shadow.camera.left" />
125+
<ngt-value [rawValue]="-8" attach="shadow.camera.bottom" />
126+
</ngt-directional-light>
127+
</ngts-perspective-camera>
128+
</ngt-group>
129+
130+
<ng-content />
131+
</ngt-group>
132+
}
133+
`,
134+
imports: [NgtsPerspectiveCamera, NgtArgs],
135+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
136+
changeDetection: ChangeDetectionStrategy.OnPush,
137+
})
138+
export class Model {
139+
protected readonly Math = Math;
140+
141+
modelRef = viewChild<ElementRef<Group>>('model');
142+
143+
protected gltf = injectGLTF<ModelGLTFResult>(() => modelUrl, {
144+
useDraco: true,
145+
});
146+
147+
private document = inject(DOCUMENT);
148+
private scroll = inject(SCROLL);
149+
private hovered = signal<string | null>(null);
150+
151+
private color = new Color();
152+
153+
constructor() {
154+
effect(() => {
155+
const scene = this.gltf.scene();
156+
if (!scene) return;
157+
158+
scene.traverse((child) => {
159+
if (child instanceof Mesh) {
160+
child.material['envMapIntensity'] = 0.2;
161+
}
162+
});
163+
});
164+
165+
const animations = injectAnimations(this.gltf, this.modelRef);
166+
167+
effect(() => {
168+
if (!animations.isReady) return;
169+
animations.actions['CameraAction.005'].play().paused = true;
170+
});
171+
172+
effect(() => {
173+
const model = this.modelRef()?.nativeElement;
174+
if (!model) return;
175+
176+
const hovered = this.hovered();
177+
if (hovered) {
178+
const obj = model.getObjectByName(hovered) as Mesh<BufferGeometry, MeshStandardMaterial>;
179+
obj?.material.color.set('white');
180+
}
181+
this.document.body.style.cursor = hovered ? 'pointer' : 'auto';
182+
});
183+
184+
injectBeforeRender(({ clock }) => {
185+
if (!animations.isReady) return;
186+
187+
const model = this.modelRef()?.nativeElement;
188+
if (!model) return;
189+
190+
animations.actions['CameraAction.005'].time = MathUtils.lerp(
191+
animations.actions['CameraAction.005'].time,
192+
animations.actions['CameraAction.005'].getClip().duration * this.scroll.value,
193+
0.05,
194+
);
195+
model.children[0].children.forEach((child, index) => {
196+
const mesh = child as Mesh<BufferGeometry, MeshStandardMaterial>;
197+
mesh.material.color.lerp(
198+
this.color.set(this.hovered() === child.name ? 'tomato' : '#202020'),
199+
this.hovered() ? 0.1 : 0.05,
200+
);
201+
const et = clock.elapsedTime;
202+
child.position.y = Math.sin((et + index * 2000) / 2);
203+
child.rotation.x = Math.sin((et + index * 2000) / 3) / 10;
204+
child.rotation.y = Math.cos((et + index * 2000) / 2) / 10;
205+
child.rotation.z = Math.sin((et + index * 2000) / 3) / 10;
206+
});
207+
});
208+
}
209+
210+
onPointerOver(event: NgtThreeEvent<PointerEvent>) {
211+
event.stopPropagation();
212+
this.hovered.set(event.object.name);
213+
}
214+
215+
onPointerOut(event: NgtThreeEvent<PointerEvent>) {
216+
event.stopPropagation();
217+
this.hovered.set(null);
218+
}
219+
}

0 commit comments

Comments
 (0)