sparcoon 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,31 @@
1
+ import type { Camera } from "three";
1
2
  import type { FXPropertyName, GLTypeInfo } from "../instancedParticle/shared";
3
+ import type { FXEmitter } from "./FXEmitter";
4
+ export interface FXEmitterOptions {
5
+ expectedCapacity: number;
6
+ capacityStep: number;
7
+ automaticallyDestroyModules: boolean;
8
+ castShadow: boolean;
9
+ receiveShadow: boolean;
10
+ sortCamera: Camera;
11
+ sortFraction: number;
12
+ }
13
+ export interface FXEmitterBurstOptions {
14
+ delay: number;
15
+ }
2
16
  export interface FXEmitterPlayOptions {
17
+ delay: number;
3
18
  duration: number;
4
19
  }
20
+ export declare const EMITTERS: FXEmitter[];
5
21
  export declare const EMITTER_DEFAULT_EXPECTED_CAPACITY = 32;
6
22
  export declare const EMITTER_DEFAULT_CAPACITY_STEP = 32;
7
23
  export declare const EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES = true;
8
24
  export declare const EMITTER_DEFAULT_CAST_SHADOW = false;
9
25
  export declare const EMITTER_DEFAULT_RECEIVE_SHADOW = false;
10
26
  export declare const EMITTER_DEFAULT_SORT_FRACTION: number;
27
+ export declare const EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION: number;
28
+ export declare const EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT = 10;
11
29
  export declare function collectProperties(keeper: Record<string, GLTypeInfo>, modules: readonly {
12
30
  requiredProperties?: Record<string, FXPropertyName>;
13
31
  }[], debugContext: string): void;
@@ -1,11 +1,14 @@
1
1
  import { resolveGLSLTypeInfo } from '../instancedParticle/shared.js';
2
2
 
3
+ const EMITTERS = new Array();
3
4
  const EMITTER_DEFAULT_EXPECTED_CAPACITY = 32;
4
5
  const EMITTER_DEFAULT_CAPACITY_STEP = 32;
5
6
  const EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES = true;
6
7
  const EMITTER_DEFAULT_CAST_SHADOW = false;
7
8
  const EMITTER_DEFAULT_RECEIVE_SHADOW = false;
8
9
  const EMITTER_DEFAULT_SORT_FRACTION = 1 / 10;
10
+ const EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION = 1 / 60;
11
+ const EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT = 10;
9
12
  function collectProperties(keeper, modules, debugContext) {
10
13
  for (const module of modules) {
11
14
  if (module.requiredProperties === undefined) {
@@ -24,4 +27,4 @@ function collectProperties(keeper, modules, debugContext) {
24
27
  }
25
28
  }
26
29
 
27
- export { EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_SORT_FRACTION, collectProperties };
30
+ export { EMITTERS, EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT, EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_SORT_FRACTION, collectProperties };
@@ -2,17 +2,8 @@ import type { Camera } from "three";
2
2
  import { Object3D } from "three";
3
3
  import type { FXMaterial } from "../materials/FXMaterial/FXMaterial";
4
4
  import type { FXBehavior } from "./behavior/FXBehavior";
5
- import type { FXEmitterPlayOptions } from "./FXEmitter.Internal";
5
+ import type { FXEmitterBurstOptions, FXEmitterOptions, FXEmitterPlayOptions } from "./FXEmitter.Internal";
6
6
  import type { FXSpawn } from "./spawn/FXSpawn";
7
- export interface FXEmitterOptions {
8
- expectedCapacity: number;
9
- capacityStep: number;
10
- automaticallyDestroyModules: boolean;
11
- castShadow: boolean;
12
- receiveShadow: boolean;
13
- sortCamera: Camera;
14
- sortFraction: number;
15
- }
16
7
  export declare class FXEmitter extends Object3D {
17
8
  private readonly spawnSequence;
18
9
  private readonly behaviorSequence;
@@ -21,18 +12,20 @@ export declare class FXEmitter extends Object3D {
21
12
  sortFraction: number;
22
13
  private readonly mesh;
23
14
  private readonly material;
24
- private emissionRate;
25
- private emissionAccumulator;
26
- private emissionDuration;
27
- private emissionElapsed;
15
+ private nextHandler;
16
+ private readonly pendingBursts;
17
+ private readonly activePlays;
28
18
  private sortingAccumulator;
29
19
  private readonly sortingCameraWorldPosition;
30
20
  constructor(spawnSequence: FXSpawn[], behaviorSequence: readonly FXBehavior[], material: FXMaterial, options?: Partial<FXEmitterOptions>);
31
21
  static onWillRender: (deltaTime: number) => void;
32
22
  destroy(): void;
33
- burst(count: number): void;
23
+ burst(count: number, options?: Partial<FXEmitterBurstOptions>): number;
24
+ play(rate: number, options?: Partial<FXEmitterPlayOptions>): number;
25
+ stop(handler?: number): boolean;
26
+ prewarm(duration: number, stepDuration?: number): void;
34
27
  reset(): void;
35
- play(rate: number, options?: Partial<FXEmitterPlayOptions>): void;
36
- stop(): void;
28
+ private tick;
37
29
  private readonly onRendering;
30
+ private spawnBurst;
38
31
  }
@@ -2,76 +2,21 @@ import { Object3D, Vector3 } from 'three';
2
2
  import { FXInstancedParticle } from '../instancedParticle/FXInstancedParticle.js';
3
3
  import { resolveGLSLTypeInfo } from '../instancedParticle/shared.js';
4
4
  import { BUILTIN_OFFSET_AGE, BUILTIN_OFFSET_POSITION_X, BUILTIN_OFFSET_VELOCITY_X, BUILTIN_OFFSET_POSITION_Y, BUILTIN_OFFSET_VELOCITY_Y, BUILTIN_OFFSET_POSITION_Z, BUILTIN_OFFSET_VELOCITY_Z, BUILTIN_OFFSET_ROTATION, BUILTIN_OFFSET_TORQUE, BUILTIN_OFFSET_RANDOM_A, BUILTIN_OFFSET_RANDOM_B, BUILTIN_OFFSET_RANDOM_C } from '../miscellaneous/miscellaneous.js';
5
- import { collectProperties, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_SORT_FRACTION } from './FXEmitter.Internal.js';
5
+ import { EMITTERS, collectProperties, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_SORT_FRACTION, EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION, EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT } from './FXEmitter.Internal.js';
6
6
 
7
- const EMITTERS = new Array();
8
7
  class FXEmitter extends Object3D {
9
8
  constructor(_private_spawnSequence, _private_behaviorSequence, material, options = {}) {
10
9
  super();
11
10
  this._private_spawnSequence = _private_spawnSequence;
12
11
  this._private_behaviorSequence = _private_behaviorSequence;
13
- this._private_emissionRate = 0;
14
- this._private_emissionAccumulator = 0;
15
- this._private_emissionDuration = Infinity;
16
- this._private_emissionElapsed = 0;
12
+ this._private_nextHandler = 0;
13
+ this._private_pendingBursts = [];
14
+ this._private_activePlays = [];
17
15
  this._private_sortingAccumulator = 0;
18
16
  this._private_sortingCameraWorldPosition = new Vector3();
19
17
  this._private_onRendering = (deltaTime) => {
20
- {
21
- const { builtin } = this._private_mesh.propertyBuffers;
22
- const { array, itemSize } = builtin;
23
- for (let i = 0; i < this._private_mesh.instanceCount; i++) {
24
- array[i * itemSize + BUILTIN_OFFSET_AGE] += deltaTime;
25
- }
26
- builtin.needsUpdate = true;
27
- this._private_mesh.removeDeadParticles();
28
- }
29
- if (this._private_emissionRate > 0) {
30
- this._private_emissionElapsed += deltaTime;
31
- if (this._private_emissionElapsed >= this._private_emissionDuration) {
32
- this.stop();
33
- }
34
- else {
35
- this._private_emissionAccumulator += this._private_emissionRate * deltaTime;
36
- const particlesToSpawn = Math.floor(this._private_emissionAccumulator);
37
- if (particlesToSpawn > 0) {
38
- this.burst(particlesToSpawn);
39
- this._private_emissionAccumulator -= particlesToSpawn;
40
- }
41
- }
42
- }
43
- if (this._private_mesh.instanceCount === 0) {
44
- return;
45
- }
46
- {
47
- const { propertyBuffers, instanceCount } = this._private_mesh;
48
- for (const module of this._private_behaviorSequence) {
49
- module.update(propertyBuffers, instanceCount, deltaTime);
50
- }
51
- }
52
- const { builtin } = this._private_mesh.propertyBuffers;
53
- const { array, itemSize } = builtin;
54
- {
55
- for (let i = 0; i < this._private_mesh.instanceCount; i++) {
56
- const itemOffset = i * itemSize;
57
- array[itemOffset + BUILTIN_OFFSET_POSITION_X] +=
58
- array[itemOffset + BUILTIN_OFFSET_VELOCITY_X] * deltaTime;
59
- array[itemOffset + BUILTIN_OFFSET_POSITION_Y] +=
60
- array[itemOffset + BUILTIN_OFFSET_VELOCITY_Y] * deltaTime;
61
- array[itemOffset + BUILTIN_OFFSET_POSITION_Z] +=
62
- array[itemOffset + BUILTIN_OFFSET_VELOCITY_Z] * deltaTime;
63
- }
64
- builtin.needsUpdate = true;
65
- }
66
- {
67
- for (let i = 0; i < this._private_mesh.instanceCount; i++) {
68
- const itemOffset = i * itemSize;
69
- array[itemOffset + BUILTIN_OFFSET_ROTATION] +=
70
- array[itemOffset + BUILTIN_OFFSET_TORQUE] * deltaTime;
71
- }
72
- builtin.needsUpdate = true;
73
- }
74
- if (this.sortCamera !== undefined) {
18
+ this._private_tick(deltaTime);
19
+ if (this.sortCamera !== undefined && this._private_mesh.instanceCount > 0) {
75
20
  this._private_sortingAccumulator += this.sortFraction;
76
21
  if (this._private_sortingAccumulator >= 1) {
77
22
  this._private_sortingAccumulator -= 1;
@@ -119,7 +64,136 @@ class FXEmitter extends Object3D {
119
64
  }
120
65
  this.removeFromParent();
121
66
  }
122
- burst(count) {
67
+ burst(count, options = {}) {
68
+ const handler = this._private_nextHandler++;
69
+ const delay = options.delay ?? 0;
70
+ if (delay <= 0) {
71
+ this._private_spawnBurst(count);
72
+ }
73
+ else {
74
+ this._private_pendingBursts.push({ handler, count, delay });
75
+ }
76
+ return handler;
77
+ }
78
+ play(rate, options = {}) {
79
+ const handler = this._private_nextHandler++;
80
+ this._private_activePlays.push({
81
+ handler,
82
+ rate,
83
+ duration: options.duration ?? Infinity,
84
+ elapsed: 0,
85
+ accumulator: 0,
86
+ delay: options.delay ?? 0,
87
+ });
88
+ return handler;
89
+ }
90
+ stop(handler) {
91
+ if (handler === undefined) {
92
+ const hadAnything = this._private_pendingBursts.length > 0 || this._private_activePlays.length > 0;
93
+ this._private_pendingBursts.length = 0;
94
+ this._private_activePlays.length = 0;
95
+ return hadAnything;
96
+ }
97
+ const burstIndex = this._private_pendingBursts.findIndex((b) => b.handler === handler);
98
+ if (burstIndex !== -1) {
99
+ this._private_pendingBursts.splice(burstIndex, 1);
100
+ return true;
101
+ }
102
+ const playIndex = this._private_activePlays.findIndex((p) => p.handler === handler);
103
+ if (playIndex !== -1) {
104
+ this._private_activePlays.splice(playIndex, 1);
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ prewarm(duration, stepDuration = EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION) {
110
+ const stepCount = Math.min(duration / Math.max(stepDuration, EMITTER_DEFAULT_PREWARM_MIN_STEP_DURATION), EMITTER_DEFAULT_PREWARM_MAX_STEP_COUNT);
111
+ const step = duration / stepCount;
112
+ let remaining = duration;
113
+ while (remaining > 0) {
114
+ const deltaTime = Math.min(remaining, step);
115
+ this._private_tick(deltaTime);
116
+ remaining -= deltaTime;
117
+ }
118
+ }
119
+ reset() {
120
+ this._private_pendingBursts.length = 0;
121
+ this._private_activePlays.length = 0;
122
+ this._private_mesh.drop();
123
+ }
124
+ _private_tick(deltaTime) {
125
+ // Age increment & cull dead particles
126
+ {
127
+ const { builtin } = this._private_mesh.propertyBuffers;
128
+ const { array, itemSize } = builtin;
129
+ for (let i = 0; i < this._private_mesh.instanceCount; i++) {
130
+ array[i * itemSize + BUILTIN_OFFSET_AGE] += deltaTime;
131
+ }
132
+ }
133
+ this._private_mesh.removeDeadParticles();
134
+ // Pending bursts (delayed)
135
+ for (let i = this._private_pendingBursts.length - 1; i >= 0; i--) {
136
+ const pending = this._private_pendingBursts[i];
137
+ pending.delay -= deltaTime;
138
+ if (pending.delay <= 0) {
139
+ this._private_pendingBursts.splice(i, 1);
140
+ this._private_spawnBurst(pending.count);
141
+ }
142
+ }
143
+ // Active plays
144
+ for (let i = this._private_activePlays.length - 1; i >= 0; i--) {
145
+ const play = this._private_activePlays[i];
146
+ let effectiveDeltaTime = deltaTime;
147
+ if (play.delay > 0) {
148
+ play.delay -= deltaTime;
149
+ if (play.delay > 0) {
150
+ continue;
151
+ }
152
+ // Delay just expired - use the overshoot as effective dt this tick
153
+ effectiveDeltaTime = -play.delay;
154
+ play.delay = 0;
155
+ }
156
+ play.elapsed += effectiveDeltaTime;
157
+ if (play.elapsed >= play.duration) {
158
+ this._private_activePlays.splice(i, 1);
159
+ continue;
160
+ }
161
+ play.accumulator += play.rate * effectiveDeltaTime;
162
+ const particlesToSpawn = Math.floor(play.accumulator);
163
+ if (particlesToSpawn > 0) {
164
+ this._private_spawnBurst(particlesToSpawn);
165
+ play.accumulator -= particlesToSpawn;
166
+ }
167
+ }
168
+ if (this._private_mesh.instanceCount === 0) {
169
+ return;
170
+ }
171
+ {
172
+ const { propertyBuffers, instanceCount } = this._private_mesh;
173
+ for (const module of this._private_behaviorSequence) {
174
+ module.update(propertyBuffers, instanceCount, deltaTime);
175
+ }
176
+ }
177
+ {
178
+ const { builtin } = this._private_mesh.propertyBuffers;
179
+ const { array, itemSize } = builtin;
180
+ for (let i = 0; i < this._private_mesh.instanceCount; i++) {
181
+ const offset = i * itemSize;
182
+ // Position += velocity * dt
183
+ array[offset + BUILTIN_OFFSET_POSITION_X] +=
184
+ array[offset + BUILTIN_OFFSET_VELOCITY_X] * deltaTime;
185
+ array[offset + BUILTIN_OFFSET_POSITION_Y] +=
186
+ array[offset + BUILTIN_OFFSET_VELOCITY_Y] * deltaTime;
187
+ array[offset + BUILTIN_OFFSET_POSITION_Z] +=
188
+ array[offset + BUILTIN_OFFSET_VELOCITY_Z] * deltaTime;
189
+ // Rotation += torque * dt
190
+ array[offset + BUILTIN_OFFSET_ROTATION] +=
191
+ array[offset + BUILTIN_OFFSET_TORQUE] * deltaTime;
192
+ }
193
+ builtin.needsUpdate = true;
194
+ }
195
+ }
196
+ _private_spawnBurst(count) {
123
197
  const instanceBegin = this._private_mesh.instanceCount;
124
198
  this._private_mesh.createInstances(count);
125
199
  const instanceEnd = this._private_mesh.instanceCount;
@@ -138,21 +212,6 @@ class FXEmitter extends Object3D {
138
212
  spawnModule.spawn(this._private_mesh.propertyBuffers, instanceBegin, instanceEnd);
139
213
  }
140
214
  }
141
- reset() {
142
- this._private_mesh.drop();
143
- }
144
- play(rate, options = {}) {
145
- this._private_emissionRate = rate;
146
- this._private_emissionAccumulator = 0;
147
- this._private_emissionDuration = options.duration ?? Infinity;
148
- this._private_emissionElapsed = 0;
149
- }
150
- stop() {
151
- this._private_emissionRate = 0;
152
- this._private_emissionAccumulator = 0;
153
- this._private_emissionDuration = Infinity;
154
- this._private_emissionElapsed = 0;
155
- }
156
215
  }
157
216
  FXEmitter.onWillRender = (deltaTime) => {
158
217
  for (const instance of EMITTERS) {
@@ -21,8 +21,8 @@ export declare class FXSpawnSphere extends FXSpawn<{
21
21
  constructor(innerRadius?: number, outerRadius?: number, angle?: number);
22
22
  /**
23
23
  * Polar half-angle from the equatorial plane.
24
- * - `0` — flat disk
25
- * - `Math.PI / 2` — full sphere
24
+ * - `0` - flat disk
25
+ * - `Math.PI / 2` - full sphere
26
26
  */
27
27
  get angle(): number;
28
28
  /** Minimum spawn radius */
@@ -30,8 +30,8 @@ class FXSpawnSphere extends FXSpawn {
30
30
  }
31
31
  /**
32
32
  * Polar half-angle from the equatorial plane.
33
- * - `0` — flat disk
34
- * - `Math.PI / 2` — full sphere
33
+ * - `0` - flat disk
34
+ * - `Math.PI / 2` - full sphere
35
35
  */
36
36
  get angle() {
37
37
  return this._private_angleInternal;
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { FXUnlitMaterial } from "./materials/FXUnlitMaterial/FXUnlitMaterial";
20
20
  export { FXColor } from "./miscellaneous/color/FXColor";
21
21
  export { FXColorNode } from "./nodes/color/FXColorNode";
22
22
  export { FXColorOverLifeNode } from "./nodes/color/FXColorOverLifeNode";
23
+ export { FXSphericalClipNode } from "./nodes/color/FXSphericalClipNode";
23
24
  export { FXFlatNormalNode } from "./nodes/normal/FXFlatNormalNode";
24
25
  export { FXNormalNode } from "./nodes/normal/FXNormalNode";
25
26
  export { FXSphericalNormalNode } from "./nodes/normal/FXSphericalNormalNode";
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ export { FXUnlitMaterial } from './materials/FXUnlitMaterial/FXUnlitMaterial.js'
20
20
  export { FXColor } from './miscellaneous/color/FXColor.js';
21
21
  export { FXColorNode } from './nodes/color/FXColorNode.js';
22
22
  export { FXColorOverLifeNode } from './nodes/color/FXColorOverLifeNode.js';
23
+ export { FXSphericalClipNode } from './nodes/color/FXSphericalClipNode.js';
23
24
  export { FXFlatNormalNode } from './nodes/normal/FXFlatNormalNode.js';
24
25
  export { FXNormalNode } from './nodes/normal/FXNormalNode.js';
25
26
  export { FXSphericalNormalNode } from './nodes/normal/FXSphericalNormalNode.js';
@@ -11,7 +11,7 @@ import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
11
11
  *
12
12
  * `vWorldPosition` is a Three.js convention consumed by the distance fragment
13
13
  * shader. `viewMatrix` here is the shadow camera's view matrix, so the
14
- * billboard faces the light — which is correct for shadow shape.
14
+ * billboard faces the light - which is correct for shadow shape.
15
15
  */
16
16
  const DISTANCE_VERTEX_SHADER = `
17
17
  #define DISTANCE
@@ -77,9 +77,9 @@ const DISTANCE_VERTEX_SHADER = `
77
77
  * the smaller scale dimension. Fragments outside the sphere are discarded.
78
78
  *
79
79
  * The depth offset is subtracted from the flat distance, bringing the
80
- * perceived surface closer to the light. This is an approximation — the
80
+ * perceived surface closer to the light. This is an approximation - the
81
81
  * true offset direction depends on the light-to-fragment vector, not the
82
- * camera-to-fragment vector — but for smoke particles the error is
82
+ * camera-to-fragment vector - but for smoke particles the error is
83
83
  * negligible.
84
84
  */
85
85
  const SPHERICAL_DISTANCE_SNIPPET = `
@@ -130,7 +130,7 @@ function buildDistanceMaterial(albedoNodes, distanceAlphaTest, useDistanceAlphaH
130
130
  Object.assign(shader.uniforms, node.uniforms);
131
131
  }
132
132
  let fragmentShader = shader.fragmentShader;
133
- // Alpha from nodes — MeshDistanceMaterial may or may not include
133
+ // Alpha from nodes - MeshDistanceMaterial may or may not include
134
134
  // <map_fragment> depending on the Three.js version. If the chunk is
135
135
  // absent, insert the alpha discard before the distance calculation.
136
136
  if (distanceNodes.length > 0) {
@@ -149,7 +149,7 @@ function buildDistanceMaterial(albedoNodes, distanceAlphaTest, useDistanceAlphaH
149
149
  "This may indicate a Three.js version mismatch.");
150
150
  }
151
151
  }
152
- // Spherical distance adjustment — offset the flat-billboard distance by
152
+ // Spherical distance adjustment - offset the flat-billboard distance by
153
153
  // the sphere's front surface depth.
154
154
  if (useSphericalDepth) {
155
155
  if (!DISTANCE_CALCULATION_PATTERN.test(fragmentShader)) {
@@ -0,0 +1,5 @@
1
+ import { FXColorNode } from "./FXColorNode";
2
+ export declare class FXSphericalClipNode extends FXColorNode {
3
+ constructor(innerRadius?: number);
4
+ destroy(): void;
5
+ }
@@ -0,0 +1,30 @@
1
+ import { FXColorNode } from './FXColorNode.js';
2
+
3
+ const INSTANCES = [];
4
+ class FXSphericalClipNode extends FXColorNode {
5
+ constructor(innerRadius = 0.5) {
6
+ super();
7
+ INSTANCES.push(this);
8
+ const uniformName = `u_SphericalClip_${INSTANCES.length - 1}`;
9
+ this.cacheKey = "spherical-clip";
10
+ this.uniformDeclarations = [`uniform float ${uniformName};`];
11
+ this.uniforms = {
12
+ [uniformName]: { value: innerRadius },
13
+ };
14
+ this.helperFunctions = `
15
+ vec4 applySphericalClip(float innerRadius) {
16
+ float distanceToCenter = distance(p_uv, vec2(0.5, 0.5));
17
+ return vec4(1.0, 1.0, 1.0, 1.0 - smoothstep(innerRadius, 0.5, distanceToCenter));
18
+ }
19
+ `;
20
+ this.colorExpression = `applySphericalClip(${uniformName})`;
21
+ }
22
+ destroy() {
23
+ const index = INSTANCES.indexOf(this);
24
+ if (index !== -1) {
25
+ INSTANCES.splice(index, 1);
26
+ }
27
+ }
28
+ }
29
+
30
+ export { FXSphericalClipNode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sparcoon",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "particles",