Skip to content

Commit 64db4c2

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(soba): migrate sparkles
1 parent b9e1cfb commit 64db4c2

File tree

5 files changed

+296
-0
lines changed

5 files changed

+296
-0
lines changed

libs/soba/shaders/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './sparkles-material/sparkles-material';
12
export * from './star-field-material/star-field-material';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { shaderMaterial } from '../shader-material/shader-material';
2+
3+
export const SparklesMaterial = shaderMaterial(
4+
{ time: 0, pixelRatio: 1 },
5+
// language=GLSL
6+
`
7+
uniform float pixelRatio;
8+
uniform float time;
9+
10+
attribute float size;
11+
attribute float speed;
12+
attribute float opacity;
13+
attribute vec3 noise;
14+
attribute vec3 color;
15+
16+
varying vec3 vColor;
17+
varying float vOpacity;
18+
19+
void main() {
20+
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
21+
22+
modelPosition.y += sin(time * speed + modelPosition.x * noise.x * 100.0) * 0.2;
23+
modelPosition.z += cos(time * speed + modelPosition.x * noise.y * 100.0) * 0.2;
24+
modelPosition.x += cos(time * speed + modelPosition.x * noise.z * 100.0) * 0.2;
25+
26+
vec4 viewPosition = viewMatrix * modelPosition;
27+
vec4 projectionPostion = projectionMatrix * viewPosition;
28+
29+
gl_Position = projectionPostion;
30+
gl_PointSize = size * 25. * pixelRatio;
31+
gl_PointSize *= (1.0 / - viewPosition.z);
32+
33+
vColor = color;
34+
vOpacity = opacity;
35+
}
36+
`,
37+
// language=GLSL
38+
`
39+
varying vec3 vColor;
40+
varying float vOpacity;
41+
42+
void main() {
43+
float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
44+
float strength = 0.05 / distanceToCenter - 0.1;
45+
46+
gl_FragColor = vec4(vColor, strength * vOpacity);
47+
}
48+
`
49+
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, Input, signal } from '@angular/core';
2+
import { Meta, moduleMetadata } from '@storybook/angular';
3+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { NgtsSparkles } from 'angular-three-soba/staging';
6+
import { makeStoryObject, number, StorybookSetup } from '../setup-canvas';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngts-sparkles
12+
color="orange"
13+
[size]="sizes()"
14+
[count]="sparklesAmount()"
15+
[opacity]="opacity"
16+
[speed]="speed"
17+
[noise]="noise"
18+
/>
19+
<ngts-orbit-controls />
20+
<ngt-axes-helper />
21+
<ngts-perspective-camera [position]="[2, 2, 2]" [makeDefault]="true" />
22+
`,
23+
imports: [NgtsSparkles, NgtsPerspectiveCamera, NgtsOrbitControls],
24+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
25+
})
26+
class DefaultSparklesStory {
27+
@Input() opacity = 1;
28+
@Input() speed = 0.3;
29+
@Input() noise = 1;
30+
31+
readonly #random = signal(true);
32+
@Input() set random(random: boolean) {
33+
this.#random.set(random);
34+
}
35+
36+
readonly #size = signal(5);
37+
@Input() set size(size: number) {
38+
this.#size.set(size);
39+
}
40+
41+
readonly #amount = signal(100);
42+
@Input() set amount(amount: number) {
43+
this.#amount.set(amount);
44+
}
45+
46+
readonly sparklesAmount = this.#amount.asReadonly();
47+
readonly sizes = computed(() => {
48+
if (this.#random())
49+
return new Float32Array(Array.from({ length: this.#amount() }, () => Math.random() * this.#size()));
50+
return this.#size();
51+
});
52+
}
53+
54+
export default {
55+
title: 'Staging/Sparkles',
56+
decorators: [moduleMetadata({ imports: [StorybookSetup] })],
57+
} as Meta;
58+
59+
export const Default = makeStoryObject(DefaultSparklesStory, {
60+
canvasOptions: { camera: { position: [1, 1, 1] }, controls: false },
61+
argsOptions: {
62+
random: true,
63+
amount: number(100, { range: true, max: 500, step: 1 }),
64+
noise: number(1, { range: true, min: 0, max: 1, step: 0.01 }),
65+
size: number(5, { range: true, min: 0, max: 10, step: 1 }),
66+
speed: number(0.3, { range: true, min: 0, max: 20, step: 0.1 }),
67+
opacity: number(1, { range: true, min: 0, max: 1, step: 0.01 }),
68+
},
69+
});

libs/soba/staging/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './cloud/cloud';
33
export * from './contact-shadows/contact-shadows';
44
export * from './float/float';
55
export * from './sky/sky';
6+
export * from './sparkles/sparkles';
67
export * from './stars/stars';
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, inject, Input, isSignal, Signal } from '@angular/core';
2+
import {
3+
extend,
4+
injectBeforeRender,
5+
injectNgtRef,
6+
NgtArgs,
7+
NgtSignalStore,
8+
NgtStore,
9+
type NgtRenderState,
10+
} from 'angular-three';
11+
import { SparklesMaterial } from 'angular-three-soba/shaders';
12+
import * as THREE from 'three';
13+
import { BufferAttribute, BufferGeometry, Color, MathUtils, Points, Vector2, Vector3, Vector4 } from 'three';
14+
15+
extend({ SparklesMaterial, Points, BufferGeometry, BufferAttribute });
16+
17+
const isFloat32Array = (def: any): def is Float32Array => def && (def as Float32Array).constructor === Float32Array;
18+
const expandColor = (v: THREE.Color) => [v.r, v.g, v.b];
19+
const isVector = (v: any): v is THREE.Vector2 | THREE.Vector3 | THREE.Vector4 =>
20+
v instanceof Vector2 || v instanceof Vector3 || v instanceof Vector4;
21+
const normalizeVector = (v: any): number[] => {
22+
if (Array.isArray(v)) return v;
23+
else if (isVector(v)) return v.toArray();
24+
return [v, v, v] as number[];
25+
};
26+
27+
function usePropAsIsOrAsAttribute<T = any>(count: number, prop?: T | Float32Array, setDefault?: (v: T) => number) {
28+
if (prop !== undefined) {
29+
if (isFloat32Array(prop)) {
30+
return prop as Float32Array;
31+
} else {
32+
if (prop instanceof Color) {
33+
const a = Array.from({ length: count * 3 }, () => expandColor(prop)).flat();
34+
return Float32Array.from(a);
35+
} else if (isVector(prop) || Array.isArray(prop)) {
36+
const a = Array.from({ length: count * 3 }, () => normalizeVector(prop)).flat();
37+
return Float32Array.from(a);
38+
}
39+
return Float32Array.from({ length: count }, () => prop as unknown as number);
40+
}
41+
}
42+
return Float32Array.from({ length: count }, setDefault!);
43+
}
44+
45+
export interface NgtsSparklesState {
46+
/** Number of particles (default: 100) */
47+
count: number;
48+
/** Speed of particles (default: 1) */
49+
speed: number | Float32Array;
50+
/** Opacity of particles (default: 1) */
51+
opacity: number | Float32Array;
52+
/** Color of particles (default: 100) */
53+
color?: THREE.ColorRepresentation | Float32Array;
54+
/** Size of particles (default: randomized between 0 and 1) */
55+
size?: number | Float32Array;
56+
/** The space the particles occupy (default: 1) */
57+
scale: number | [number, number, number] | THREE.Vector3;
58+
/** Movement factor (default: 1) */
59+
noise: number | [number, number, number] | THREE.Vector3 | Float32Array;
60+
}
61+
62+
@Component({
63+
selector: 'ngts-sparkles',
64+
standalone: true,
65+
template: `
66+
<ngt-points ngtCompount [ref]="pointsRef">
67+
<ngt-buffer-geometry>
68+
<ngt-buffer-attribute *args="[positions(), 3]" attach="attributes.position" />
69+
<ngt-buffer-attribute *args="[sizes(), 1]" attach="attributes.size" />
70+
<ngt-buffer-attribute *args="[opacities(), 1]" attach="attributes.opacity" />
71+
<ngt-buffer-attribute *args="[speeds(), 1]" attach="attributes.speed" />
72+
<ngt-buffer-attribute *args="[colors(), 3]" attach="attributes.color" />
73+
<ngt-buffer-attribute *args="[noises(), 3]" attach="attributes.noise" />
74+
</ngt-buffer-geometry>
75+
<ngt-sparkles-material [ref]="materialRef" [transparent]="true" [depthWrite]="false" [pixelRatio]="dpr()" />
76+
</ngt-points>
77+
`,
78+
imports: [NgtArgs],
79+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
80+
})
81+
export class NgtsSparkles extends NgtSignalStore<NgtsSparklesState> {
82+
@Input() pointsRef = injectNgtRef<Points>();
83+
84+
/** Number of particles (default: 100) */
85+
@Input() set count(count: number) {
86+
this.set({ count });
87+
}
88+
89+
/** Speed of particles (default: 1) */
90+
@Input() set speed(speed: number | Float32Array) {
91+
this.set({ speed });
92+
}
93+
94+
/** Opacity of particles (default: 1) */
95+
@Input() set opacity(opacity: number | Float32Array) {
96+
this.set({ opacity });
97+
}
98+
99+
/** Color of particles (default: 100) */
100+
@Input() set color(color: THREE.ColorRepresentation | Float32Array) {
101+
this.set({ color });
102+
}
103+
104+
/** Size of particles (default: randomized between 0 and 1) */
105+
@Input() set size(size: number | Float32Array) {
106+
this.set({ size });
107+
}
108+
109+
/** The space the particles occupy (default: 1) */
110+
@Input() set scale(scale: number | [number, number, number] | THREE.Vector3) {
111+
this.set({ scale });
112+
}
113+
114+
/** Movement factor (default: 1) */
115+
@Input() set noise(noise: number | [number, number, number] | THREE.Vector3 | Float32Array) {
116+
this.set({ noise });
117+
}
118+
119+
readonly #store = inject(NgtStore);
120+
121+
readonly #count = this.select('count');
122+
readonly #scale = this.select('scale');
123+
readonly #color = this.select('color');
124+
125+
readonly materialRef = injectNgtRef<InstanceType<typeof SparklesMaterial>>();
126+
127+
readonly dpr = this.#store.select('viewport', 'dpr');
128+
readonly positions = computed(() =>
129+
Float32Array.from(
130+
Array.from({ length: this.#count() }, () =>
131+
normalizeVector(this.#scale()).map(MathUtils.randFloatSpread)
132+
).flat()
133+
)
134+
);
135+
136+
readonly sizes = this.#getComputed('size', this.#count, Math.random);
137+
readonly opacities = this.#getComputed('opacity', this.#count);
138+
readonly speeds = this.#getComputed('speed', this.#count);
139+
readonly noises = this.#getComputed('noise', () => this.#count() * 3);
140+
readonly colors = this.#getComputed(
141+
computed(() => {
142+
const color = this.#color();
143+
return !isFloat32Array(color) ? new THREE.Color(color) : color;
144+
}),
145+
() => (this.#color() === undefined ? this.#count() * 3 : this.#count()),
146+
() => 1
147+
);
148+
149+
#getComputed<TKey extends keyof NgtsSparklesState>(
150+
nameOrComputed: TKey | Signal<NgtsSparklesState[TKey]>,
151+
count: () => number,
152+
setDefault?: (value: NgtsSparklesState[TKey]) => number
153+
) {
154+
const value =
155+
typeof nameOrComputed !== 'string' && isSignal(nameOrComputed)
156+
? nameOrComputed
157+
: this.select(nameOrComputed);
158+
return computed(() => usePropAsIsOrAsAttribute(count(), value(), setDefault));
159+
}
160+
161+
constructor() {
162+
super({
163+
noise: 1,
164+
count: 100,
165+
speed: 1,
166+
opacity: 1,
167+
scale: 1,
168+
});
169+
injectBeforeRender(this.#onBeforeRender.bind(this));
170+
}
171+
172+
#onBeforeRender({ clock }: NgtRenderState) {
173+
if (!this.materialRef.nativeElement) return;
174+
this.materialRef.nativeElement.uniforms['time'].value = clock.elapsedTime;
175+
}
176+
}

0 commit comments

Comments
 (0)