Skip to content

Commit a0bfa2b

Browse files
committed
camera shake
1 parent ffe0275 commit a0bfa2b

File tree

7 files changed

+296
-27
lines changed

7 files changed

+296
-27
lines changed

libs/soba/cameras/src/cube-camera/cube-camera.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ declare global {
118118
/**
119119
* @extends ngt-group
120120
*/
121-
'ngts-cube-camera': NgtsCubeCameraState & NgtGroup & { frames?: number };
121+
'ngts-cube-camera': NgtsCubeCameraComponentState & NgtGroup;
122122
}
123123
}
124124

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { BoxGeometry, ConeGeometry, PlaneGeometry } from 'three';
77
import { makeDecorators, makeStoryObject } from '../setup-canvas';
88

99
@Component({
10-
selector: 'BillboardCone',
10+
selector: 'billboard-cone',
1111
standalone: true,
1212
template: `
1313
<ngt-mesh>
@@ -25,7 +25,7 @@ class Cone {
2525
}
2626

2727
@Component({
28-
selector: 'BillboardBox',
28+
selector: 'billboard-box',
2929
standalone: true,
3030
template: `
3131
<ngt-mesh [position]="position">
@@ -44,7 +44,7 @@ class Box {
4444
}
4545

4646
@Component({
47-
selector: 'BillboardPlane',
47+
selector: 'billboard-plane',
4848
standalone: true,
4949
template: `
5050
<ngt-mesh>
@@ -67,9 +67,9 @@ class Plane {
6767
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0.5, 2.05, 0.5]">
6868
<ngts-text text="box" [fontSize]="1" [outlineWidth]="'5%'" [outlineColor]="'#000'" [outlineOpacity]="1" />
6969
</ngts-billboard>
70-
<BillboardBox [position]="[0.5, 1, 0.5]" color="red">
70+
<billboard-box [position]="[0.5, 1, 0.5]" color="red">
7171
<ngt-mesh-standard-material />
72-
</BillboardBox>
72+
</billboard-box>
7373
<ngt-group [position]="[-2.5, -3, -1]">
7474
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 1.05, 0]">
7575
<ngts-text
@@ -80,14 +80,14 @@ class Plane {
8080
[outlineOpacity]="1"
8181
/>
8282
</ngts-billboard>
83-
<BillboardCone color="green">
83+
<billboard-cone color="green">
8484
<ngt-mesh-standard-material />
85-
</BillboardCone>
85+
</billboard-cone>
8686
</ngt-group>
8787
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 0, -5]">
88-
<BillboardPlane [args]="[2, 2]" color="#000066">
88+
<billboard-plane [args]="[2, 2]" color="#000066">
8989
<ngt-mesh-standard-material />
90-
</BillboardPlane>
90+
</billboard-plane>
9191
</ngts-billboard>
9292
9393
<ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" />
@@ -106,19 +106,19 @@ class TextBillboardStory {
106106
standalone: true,
107107
template: `
108108
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[-4, -2, 0]">
109-
<BillboardPlane [args]="[3, 2]" color="red" />
109+
<billboard-plane [args]="[3, 2]" color="red" />
110110
</ngts-billboard>
111111
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[-4, 2, 0]">
112-
<BillboardPlane [args]="[3, 2]" color="orange" />
112+
<billboard-plane [args]="[3, 2]" color="orange" />
113113
</ngts-billboard>
114114
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[0, 0, 0]">
115-
<BillboardPlane [args]="[3, 2]" color="green" />
115+
<billboard-plane [args]="[3, 2]" color="green" />
116116
</ngts-billboard>
117117
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[4, -2, 0]">
118-
<BillboardPlane [args]="[3, 2]" color="blue" />
118+
<billboard-plane [args]="[3, 2]" color="blue" />
119119
</ngts-billboard>
120120
<ngts-billboard [follow]="follow" [lockX]="lockX" [lockY]="lockY" [lockZ]="lockZ" [position]="[4, 2, 0]">
121-
<BillboardPlane [args]="[3, 2]" color="yellow" />
121+
<billboard-plane [args]="[3, 2]" color="yellow" />
122122
</ngts-billboard>
123123
124124
<ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" />
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core';
2+
import { Meta } from '@storybook/angular';
3+
import { NgtArgs, NgtBeforeRenderEvent } from 'angular-three';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { NgtsCameraShake } from 'angular-three-soba/staging';
6+
import * as THREE from 'three';
7+
import { makeDecorators, makeStoryObject, number } from '../setup-canvas';
8+
9+
const numberArgs = number(0.05, { range: true, max: 1, min: 0, step: 0.05 });
10+
const frequencyArgs = number(0.8, { range: true, max: 10, min: 0, step: 0.1 });
11+
const argsOptions = {
12+
maxPitch: numberArgs,
13+
maxRoll: numberArgs,
14+
maxYaw: numberArgs,
15+
pitchFrequency: frequencyArgs,
16+
rollFrequency: frequencyArgs,
17+
yawFrequency: frequencyArgs,
18+
};
19+
20+
@Component({
21+
selector: 'camera-shake-scene',
22+
standalone: true,
23+
template: `
24+
<ngt-mesh (beforeRender)="onBeforeRender($event)">
25+
<ngt-box-geometry *args="[2, 2, 2]" />
26+
<ngt-mesh-standard-material [wireframe]="true" />
27+
</ngt-mesh>
28+
<ngt-mesh [position]="[0, -6, 0]" [rotation]="[Math.PI / -2, 0, 0]">
29+
<ngt-plane-geometry *args="[200, 200, 75, 75]" />
30+
<ngt-mesh-basic-material [wireframe]="true" color="red" [side]="DoubleSide" />
31+
</ngt-mesh>
32+
`,
33+
imports: [NgtArgs],
34+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
35+
})
36+
class Scene {
37+
readonly Math = Math;
38+
readonly DoubleSide = THREE.DoubleSide;
39+
40+
onBeforeRender({ object: mesh }: NgtBeforeRenderEvent<THREE.Mesh>) {
41+
mesh.rotation.x = mesh.rotation.y += 0.01;
42+
}
43+
}
44+
45+
@Component({
46+
standalone: true,
47+
template: `
48+
<ngts-orbit-controls [makeDefault]="true" />
49+
<ngts-camera-shake
50+
[maxPitch]="maxPitch"
51+
[maxRoll]="maxRoll"
52+
[maxYaw]="maxYaw"
53+
[pitchFrequency]="pitchFrequency"
54+
[rollFrequency]="rollFrequency"
55+
[yawFrequency]="yawFrequency"
56+
/>
57+
<camera-shake-scene />
58+
`,
59+
imports: [NgtsCameraShake, Scene, NgtsOrbitControls],
60+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
61+
})
62+
class WithOrbitControlsStory {
63+
@Input() maxPitch = argsOptions.maxPitch.defaultValue;
64+
@Input() maxRoll = argsOptions.maxRoll.defaultValue;
65+
@Input() maxYaw = argsOptions.maxYaw.defaultValue;
66+
@Input() pitchFrequency = argsOptions.pitchFrequency.defaultValue;
67+
@Input() rollFrequency = argsOptions.rollFrequency.defaultValue;
68+
@Input() yawFrequency = argsOptions.yawFrequency.defaultValue;
69+
}
70+
71+
@Component({
72+
standalone: true,
73+
template: `
74+
<ngts-camera-shake
75+
[maxPitch]="maxPitch"
76+
[maxRoll]="maxRoll"
77+
[maxYaw]="maxYaw"
78+
[pitchFrequency]="pitchFrequency"
79+
[rollFrequency]="rollFrequency"
80+
[yawFrequency]="yawFrequency"
81+
/>
82+
<camera-shake-scene />
83+
`,
84+
imports: [NgtsCameraShake, Scene],
85+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
86+
})
87+
class DefaultCameraShakeStory {
88+
@Input() maxPitch = argsOptions.maxPitch.defaultValue;
89+
@Input() maxRoll = argsOptions.maxRoll.defaultValue;
90+
@Input() maxYaw = argsOptions.maxYaw.defaultValue;
91+
@Input() pitchFrequency = argsOptions.pitchFrequency.defaultValue;
92+
@Input() rollFrequency = argsOptions.rollFrequency.defaultValue;
93+
@Input() yawFrequency = argsOptions.yawFrequency.defaultValue;
94+
}
95+
96+
export default {
97+
title: 'Staging/Camera Shake',
98+
decorators: makeDecorators(),
99+
} as Meta;
100+
101+
export const Default = makeStoryObject(DefaultCameraShakeStory, {
102+
canvasOptions: { camera: { position: [0, 0, 10] }, controls: false },
103+
argsOptions,
104+
});
105+
106+
export const WithOrbitControls = makeStoryObject(WithOrbitControlsStory, {
107+
canvasOptions: { camera: { position: [0, 0, 10] }, controls: false },
108+
argsOptions,
109+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Directive, Input, Signal, computed, effect } from '@angular/core';
2+
import { injectBeforeRender, injectNgtStore, signalStore } from 'angular-three';
3+
import { SimplexNoise } from 'three-stdlib';
4+
5+
type ControlsProto = {
6+
update(): void;
7+
target: THREE.Vector3;
8+
addEventListener: (event: string, callback: (event: any) => void) => void;
9+
removeEventListener: (event: string, callback: (event: any) => void) => void;
10+
};
11+
12+
export type NgtsCameraShakeState = {
13+
decay?: boolean;
14+
intensity: number;
15+
decayRate: number;
16+
maxYaw: number;
17+
maxPitch: number;
18+
maxRoll: number;
19+
yawFrequency: number;
20+
pitchFrequency: number;
21+
rollFrequency: number;
22+
};
23+
24+
declare global {
25+
interface HTMLElementTagNameMap {
26+
'ngts-camera-shake': NgtsCameraShakeState;
27+
}
28+
}
29+
30+
@Directive({
31+
selector: 'ngts-camera-shake',
32+
standalone: true,
33+
})
34+
export class NgtsCameraShake {
35+
private inputs = signalStore<NgtsCameraShakeState>({
36+
intensity: 1,
37+
decayRate: 0.65,
38+
maxYaw: 0.1,
39+
maxPitch: 0.1,
40+
maxRoll: 0.1,
41+
yawFrequency: 0.1,
42+
pitchFrequency: 0.1,
43+
rollFrequency: 0.1,
44+
});
45+
46+
@Input({ alias: 'intensity' }) set _intensity(intensity: number) {
47+
this.inputs.set({ intensity });
48+
}
49+
50+
@Input({ alias: 'decay' }) set _decay(decay: boolean) {
51+
this.inputs.set({ decay });
52+
}
53+
54+
@Input({ alias: 'decayRate' }) set _decayRate(decayRate: number) {
55+
this.inputs.set({ decayRate });
56+
}
57+
58+
@Input({ alias: 'maxYaw' }) set _maxYaw(maxYaw: number) {
59+
this.inputs.set({ maxYaw });
60+
}
61+
62+
@Input({ alias: 'maxPitch' }) set _maxPitch(maxPitch: number) {
63+
this.inputs.set({ maxPitch });
64+
}
65+
66+
@Input({ alias: 'maxRoll' }) set _maxRoll(maxRoll: number) {
67+
this.inputs.set({ maxRoll });
68+
}
69+
70+
@Input({ alias: 'yawFrequency' }) set _yawFrequency(yawFrequency: number) {
71+
this.inputs.set({ yawFrequency });
72+
}
73+
74+
@Input({ alias: 'pitchFrequency' }) set _pitchFrequency(pitchFrequency: number) {
75+
this.inputs.set({ pitchFrequency });
76+
}
77+
78+
@Input({ alias: 'rollFrequency' }) set _rollFrequency(rollFrequency: number) {
79+
this.inputs.set({ rollFrequency });
80+
}
81+
82+
private store = injectNgtStore();
83+
84+
private initialRotation = this.store.get('camera').rotation.clone();
85+
private yawNoise = new SimplexNoise();
86+
private rollNoise = new SimplexNoise();
87+
private pitchNoise = new SimplexNoise();
88+
89+
private intensity = this.inputs.select('intensity');
90+
private constrainedIntensity = computed(() => {
91+
const intensity = this.intensity();
92+
if (intensity < 0 || intensity > 1) {
93+
return intensity < 0 ? 0 : 1;
94+
}
95+
return intensity;
96+
});
97+
98+
constructor() {
99+
this.beforeRender();
100+
this.setEvents();
101+
}
102+
103+
private beforeRender() {
104+
injectBeforeRender(({ clock, delta }) => {
105+
const { maxYaw, yawFrequency, maxPitch, pitchFrequency, maxRoll, rollFrequency, decay, decayRate } =
106+
this.inputs.get();
107+
const intensity = this.constrainedIntensity();
108+
const camera = this.store.get('camera');
109+
110+
const shake = Math.pow(intensity, 2);
111+
const yaw = maxYaw * shake * this.yawNoise.noise(clock.elapsedTime * yawFrequency, 1);
112+
const pitch = maxPitch * shake * this.pitchNoise.noise(clock.elapsedTime * pitchFrequency, 1);
113+
const roll = maxRoll * shake * this.rollNoise.noise(clock.elapsedTime * rollFrequency, 1);
114+
115+
camera.rotation.set(
116+
this.initialRotation.x + pitch,
117+
this.initialRotation.y + yaw,
118+
this.initialRotation.z + roll,
119+
);
120+
121+
if (decay && intensity > 0) {
122+
this.inputs.set((state) => ({ intensity: state.intensity - decayRate * delta }));
123+
}
124+
});
125+
}
126+
127+
private setEvents() {
128+
const camera = this.store.select('camera');
129+
const controls = this.store.select('controls') as unknown as Signal<ControlsProto>;
130+
const trigger = computed(() => ({ camera: camera(), controls: controls() }));
131+
132+
effect((onCleanup) => {
133+
const { camera, controls } = trigger();
134+
if (controls) {
135+
const callback = () => void (this.initialRotation = camera.rotation.clone());
136+
controls.addEventListener('change', callback);
137+
callback();
138+
onCleanup(() => void controls.removeEventListener('change', callback));
139+
}
140+
});
141+
}
142+
}

libs/soba/staging/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './camera-shake/camera-shake';
12
export * from './center/center';
23
export * from './float/float';
34
export * from './matcap-texture/matcap-texture';

tools/scripts/generate-soba-json.mjs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const sobaMap = {
2929
const entryPoints = {
3030
controls: ['orbit-controls'],
3131
abstractions: ['billboard', 'text', 'grid', 'text-3d'],
32-
cameras: ['perspective-camera', 'orthographic-camera'],
33-
staging: ['center', 'float'],
32+
cameras: ['perspective-camera', 'orthographic-camera', 'cube-camera'],
33+
staging: ['center', 'float', 'camera-shake'],
3434
};
3535

3636
const paths = [];
@@ -43,7 +43,8 @@ for (const [entryPoint, entryPointEntities] of Object.entries(entryPoints)) {
4343
const { metadataJson, webTypesJson, write } = createBareJsons('angular-three-soba', 'soba');
4444

4545
for (const path of paths) {
46-
const { sourceFile, processIntersectionTypeNode, processTypeMembers } = createProgram([path]);
46+
const { sourceFile, processIntersectionTypeNode, processTypeMembers, typesMap, processTypeReferenceNode } =
47+
createProgram([path]);
4748

4849
ts.forEachChild(sourceFile, (node) => {
4950
if (ts.isModuleDeclaration(node)) {
@@ -105,6 +106,11 @@ for (const path of paths) {
105106

106107
if (ts.isIntersectionTypeNode(memberType)) {
107108
processIntersectionTypeNode(metadataAtMember, memberType, sobaMap, externalsMap);
109+
} else if (ts.isTypeReferenceNode(memberType)) {
110+
if (typesMap[memberType.typeName.text]) {
111+
const typeDeclaration = typesMap[memberType.typeName.text];
112+
processTypeReferenceNode(metadataAtMember, typeDeclaration, sobaMap);
113+
}
108114
}
109115

110116
metadataJson.tags.push(metadataAtMember);

0 commit comments

Comments
 (0)