sparcoon 0.5.0 → 0.6.1

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.
Files changed (106) hide show
  1. package/README.md +6 -6
  2. package/dist/emitter/FXEmitter.Internal.d.ts +50 -2
  3. package/dist/emitter/FXEmitter.Internal.js +1 -2
  4. package/dist/emitter/FXEmitter.d.ts +48 -1
  5. package/dist/emitter/FXEmitter.js +74 -11
  6. package/dist/emitter/behavior/FXBehavior.d.ts +12 -2
  7. package/dist/emitter/behavior/FXBehavior.js +3 -2
  8. package/dist/emitter/behavior/FXBehaviorDirectionalForce.d.ts +25 -0
  9. package/dist/emitter/behavior/FXBehaviorDirectionalForce.js +74 -0
  10. package/dist/emitter/behavior/FXBehaviorPointForce.d.ts +40 -0
  11. package/dist/emitter/behavior/FXBehaviorPointForce.js +98 -0
  12. package/dist/emitter/behavior/FXBehaviorScaleOverLife.d.ts +9 -8
  13. package/dist/emitter/behavior/FXBehaviorScaleOverLife.js +12 -29
  14. package/dist/emitter/behavior/FXBehaviorTorqueDamping.d.ts +4 -3
  15. package/dist/emitter/behavior/FXBehaviorTorqueDamping.js +3 -2
  16. package/dist/emitter/behavior/FXBehaviorTorqueNoise.d.ts +4 -3
  17. package/dist/emitter/behavior/FXBehaviorTorqueNoise.js +3 -2
  18. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.d.ts +17 -0
  19. package/dist/emitter/behavior/FXBehaviorTorqueOverLife.js +34 -0
  20. package/dist/emitter/behavior/FXBehaviorVelocityDamping.d.ts +3 -2
  21. package/dist/emitter/behavior/FXBehaviorVelocityDamping.js +3 -2
  22. package/dist/emitter/behavior/FXBehaviorVelocityNoise.d.ts +6 -5
  23. package/dist/emitter/behavior/FXBehaviorVelocityNoise.js +6 -5
  24. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.d.ts +20 -0
  25. package/dist/emitter/behavior/FXBehaviorVelocityOverLife.js +48 -0
  26. package/dist/emitter/spawn/FXSpawn.d.ts +4 -2
  27. package/dist/emitter/spawn/FXSpawn.js +3 -2
  28. package/dist/emitter/spawn/FXSpawnBox.d.ts +8 -16
  29. package/dist/emitter/spawn/FXSpawnBox.js +23 -39
  30. package/dist/emitter/spawn/{FXSpawnRandomLifetime.d.ts → FXSpawnLifetime.d.ts} +3 -5
  31. package/dist/emitter/spawn/{FXSpawnRandomLifetime.js → FXSpawnLifetime.js} +8 -10
  32. package/dist/emitter/spawn/FXSpawnOffset.d.ts +9 -0
  33. package/dist/emitter/spawn/FXSpawnOffset.js +9 -0
  34. package/dist/emitter/spawn/FXSpawnPoint.d.ts +8 -0
  35. package/dist/emitter/spawn/FXSpawnPoint.js +8 -0
  36. package/dist/emitter/spawn/{FXSpawnRandomRotation.d.ts → FXSpawnRotation.d.ts} +3 -5
  37. package/dist/emitter/spawn/{FXSpawnRandomRotation.js → FXSpawnRotation.js} +8 -10
  38. package/dist/emitter/spawn/{FXSpawnRandomScale.d.ts → FXSpawnScale.d.ts} +4 -6
  39. package/dist/emitter/spawn/{FXSpawnRandomScale.js → FXSpawnScale.js} +12 -14
  40. package/dist/emitter/spawn/FXSpawnSphere.d.ts +11 -6
  41. package/dist/emitter/spawn/FXSpawnSphere.js +11 -6
  42. package/dist/emitter/spawn/{FXSpawnRandomTorque.d.ts → FXSpawnTorque.d.ts} +3 -5
  43. package/dist/emitter/spawn/{FXSpawnRandomTorque.js → FXSpawnTorque.js} +8 -10
  44. package/dist/emitter/spawn/FXSpawnVelocity.d.ts +32 -0
  45. package/dist/emitter/spawn/{FXSpawnRandomVelocity.js → FXSpawnVelocity.js} +26 -11
  46. package/dist/index.d.ts +23 -16
  47. package/dist/index.js +20 -16
  48. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +4 -4
  49. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +2 -2
  50. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.d.ts +55 -5
  51. package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +9 -2
  52. package/dist/materials/FXMaterial/FXDepthMaterial.Internal.d.ts +3 -3
  53. package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.d.ts +3 -3
  54. package/dist/materials/FXMaterial/FXMaterial.d.ts +51 -5
  55. package/dist/materials/FXMaterial/FXMaterial.js +5 -0
  56. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.d.ts +3 -3
  57. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.d.ts +3 -3
  58. package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.js +3 -3
  59. package/dist/miscellaneous/curve/FXCurve1D.Internal.d.ts +20 -0
  60. package/dist/miscellaneous/curve/FXCurve1D.d.ts +17 -0
  61. package/dist/miscellaneous/curve/FXCurve1D.js +94 -0
  62. package/dist/miscellaneous/miscellaneous.d.ts +3 -1
  63. package/dist/miscellaneous/miscellaneous.js +8 -4
  64. package/dist/miscellaneous/texture/FXTextureView.Internal.js +6 -0
  65. package/dist/miscellaneous/texture/FXTextureView.js +234 -0
  66. package/dist/nodes/color/FXNodeColor.d.ts +7 -0
  67. package/dist/nodes/color/FXNodeColor.js +11 -0
  68. package/dist/nodes/color/FXNodeColorOverLife.d.ts +25 -0
  69. package/dist/nodes/color/FXNodeColorOverLife.js +60 -0
  70. package/dist/nodes/color/FXNodeSphericalClip.d.ts +18 -0
  71. package/dist/nodes/color/FXNodeSphericalClip.js +45 -0
  72. package/dist/nodes/normal/FXNodeFlatNormal.d.ts +7 -0
  73. package/dist/nodes/normal/FXNodeFlatNormal.js +17 -0
  74. package/dist/nodes/normal/FXNodeNormal.d.ts +7 -0
  75. package/dist/nodes/normal/FXNodeNormal.js +7 -0
  76. package/dist/nodes/normal/FXNodeSphericalNormal.d.ts +7 -0
  77. package/dist/nodes/normal/{FXSphericalNormalNode.js → FXNodeSphericalNormal.js} +7 -4
  78. package/dist/nodes/texture/FXNodeAnimatedTexture.d.ts +47 -0
  79. package/dist/nodes/texture/FXNodeAnimatedTexture.js +137 -0
  80. package/dist/nodes/texture/FXNodeStaticTexture.d.ts +18 -0
  81. package/dist/nodes/texture/FXNodeStaticTexture.js +60 -0
  82. package/dist/nodes/texture/FXNodeTexture.d.ts +7 -0
  83. package/dist/nodes/texture/FXNodeTexture.js +11 -0
  84. package/package.json +2 -4
  85. package/dist/emitter/behavior/FXBehaviorDirectionalGravity.d.ts +0 -21
  86. package/dist/emitter/behavior/FXBehaviorDirectionalGravity.js +0 -51
  87. package/dist/emitter/behavior/FXBehaviorPointGravity.d.ts +0 -39
  88. package/dist/emitter/behavior/FXBehaviorPointGravity.js +0 -116
  89. package/dist/emitter/spawn/FXSpawnRandomVelocity.d.ts +0 -17
  90. package/dist/nodes/color/FXColorNode.d.ts +0 -3
  91. package/dist/nodes/color/FXColorNode.js +0 -8
  92. package/dist/nodes/color/FXColorOverLifeNode.d.ts +0 -6
  93. package/dist/nodes/color/FXColorOverLifeNode.js +0 -44
  94. package/dist/nodes/color/FXSphericalClipNode.d.ts +0 -5
  95. package/dist/nodes/color/FXSphericalClipNode.js +0 -30
  96. package/dist/nodes/normal/FXFlatNormalNode.d.ts +0 -4
  97. package/dist/nodes/normal/FXFlatNormalNode.js +0 -14
  98. package/dist/nodes/normal/FXNormalNode.d.ts +0 -3
  99. package/dist/nodes/normal/FXNormalNode.js +0 -4
  100. package/dist/nodes/normal/FXSphericalNormalNode.d.ts +0 -4
  101. package/dist/nodes/texture/FXAnimatedTextureNode.d.ts +0 -12
  102. package/dist/nodes/texture/FXAnimatedTextureNode.js +0 -88
  103. package/dist/nodes/texture/FXStaticTextureNode.d.ts +0 -6
  104. package/dist/nodes/texture/FXStaticTextureNode.js +0 -34
  105. package/dist/nodes/texture/FXTextureNode.d.ts +0 -3
  106. package/dist/nodes/texture/FXTextureNode.js +0 -8
@@ -0,0 +1,234 @@
1
+ import { Ferrsign3 } from 'ferrsign';
2
+ import { Matrix3, Vector2, Texture } from 'three';
3
+ import { assertValidPositiveNumber, assertValidNonNegativeNumber } from '../asserts.js';
4
+ import { TEXTURE_DEFAULT_TEXTURE, TEXTURE_DEFAULT_SIZE } from './FXTextureView.Internal.js';
5
+
6
+ /** Wrapper for Three.js texture with atlas and trim support. Does not own the underlying texture; user must dispose it. */
7
+ class FXTextureView {
8
+ /** @param config Texture or atlas configuration */
9
+ constructor(config) {
10
+ this._private_textureInternal = TEXTURE_DEFAULT_TEXTURE;
11
+ this._private_sourceWidth = TEXTURE_DEFAULT_SIZE;
12
+ this._private_sourceHeight = TEXTURE_DEFAULT_SIZE;
13
+ this._private_frameX = 0;
14
+ this._private_frameY = 0;
15
+ this._private_frameWidth = TEXTURE_DEFAULT_SIZE;
16
+ this._private_frameHeight = TEXTURE_DEFAULT_SIZE;
17
+ this._private_rotatedInternal = false;
18
+ this._private_scaleInternal = 1;
19
+ this._private_trimLeft = 0;
20
+ this._private_trimRight = 0;
21
+ this._private_trimTop = 0;
22
+ this._private_trimBottom = 0;
23
+ this._private_textureDirtyInternal = false;
24
+ this._private_uvTransformDirtyInternal = false;
25
+ this._private_trimDirtyInternal = false;
26
+ this._private_signalDiminsionsChangedInternal = new Ferrsign3();
27
+ if (config !== undefined) {
28
+ this.set(config);
29
+ }
30
+ }
31
+ /** Original width before trimming in world units */
32
+ get width() {
33
+ return this._private_sourceWidth / this._private_scaleInternal;
34
+ }
35
+ /** Original height before trimming in world units */
36
+ get height() {
37
+ return this._private_sourceHeight / this._private_scaleInternal;
38
+ }
39
+ /** Visible width after trimming in world units */
40
+ get trimmedWidth() {
41
+ const frameSize = this._private_rotatedInternal ? this._private_frameHeight : this._private_frameWidth;
42
+ return frameSize / this._private_scaleInternal;
43
+ }
44
+ /** Visible height after trimming in world units */
45
+ get trimmedHeight() {
46
+ const frameSize = this._private_rotatedInternal ? this._private_frameWidth : this._private_frameHeight;
47
+ return frameSize / this._private_scaleInternal;
48
+ }
49
+ /** Underlying Three.js texture */
50
+ get texture() {
51
+ return this._private_textureInternal;
52
+ }
53
+ /** Whether sprite is rotated in atlas */
54
+ get rotated() {
55
+ return this._private_rotatedInternal;
56
+ }
57
+ /** Scale factor for resolution independence */
58
+ get scale() {
59
+ return this._private_scaleInternal;
60
+ }
61
+ /** Transparent padding around visible content */
62
+ get trim() {
63
+ const scale = this._private_scaleInternal;
64
+ return {
65
+ left: this._private_trimLeft / scale,
66
+ right: this._private_trimRight / scale,
67
+ top: this._private_trimTop / scale,
68
+ bottom: this._private_trimBottom / scale,
69
+ };
70
+ }
71
+ get textureDirty() {
72
+ return this._private_textureDirtyInternal;
73
+ }
74
+ /** Whether UV transform inputs changed since last check */
75
+ get uvTransformDirty() {
76
+ return this._private_uvTransformDirtyInternal;
77
+ }
78
+ /** Whether trim values changed since last check */
79
+ get trimDirty() {
80
+ return this._private_trimDirtyInternal;
81
+ }
82
+ get signalDiminsionsChanged() {
83
+ return this._private_signalDiminsionsChangedInternal;
84
+ }
85
+ setTextureDirtyFalse() {
86
+ this._private_textureDirtyInternal = false;
87
+ }
88
+ /** Marks UV transform as clean. @internal */
89
+ setUVTransformDirtyFalse() {
90
+ this._private_uvTransformDirtyInternal = false;
91
+ }
92
+ /** Marks trim as clean. @internal */
93
+ setTrimDirtyFalse() {
94
+ this._private_trimDirtyInternal = false;
95
+ }
96
+ /**
97
+ * Calculates UV transform matrix for shader sampling.
98
+ * @param result Matrix to store result in
99
+ * @returns UV transform matrix
100
+ */
101
+ calculateUVTransform(result = new Matrix3()) {
102
+ const atlasWidth = this._private_textureInternal.image?.naturalWidth ?? TEXTURE_DEFAULT_SIZE;
103
+ const atlasHeight = this._private_textureInternal.image?.naturalHeight ?? TEXTURE_DEFAULT_SIZE;
104
+ if (this._private_rotatedInternal) {
105
+ const physicalWidth = this._private_frameHeight;
106
+ const physicalHeight = this._private_frameWidth;
107
+ const u0 = this._private_frameX / atlasWidth;
108
+ const v0 = (atlasHeight - this._private_frameY - physicalHeight) / atlasHeight;
109
+ const uSize = physicalWidth / atlasWidth;
110
+ const vSize = physicalHeight / atlasHeight;
111
+ result.set(0, uSize, u0, -vSize, 0, v0 + vSize, 0, 0, 1);
112
+ }
113
+ else {
114
+ const u0 = this._private_frameX / atlasWidth;
115
+ const v0 = (atlasHeight - this._private_frameY - this._private_frameHeight) / atlasHeight;
116
+ const uSize = this._private_frameWidth / atlasWidth;
117
+ const vSize = this._private_frameHeight / atlasHeight;
118
+ result.set(uSize, 0, u0, 0, vSize, v0, 0, 0, 1);
119
+ }
120
+ return result;
121
+ }
122
+ /**
123
+ * Gets original dimensions before trimming.
124
+ * @param result Vector to store result in
125
+ * @returns Width and height in world units
126
+ */
127
+ getResolution(result = new Vector2()) {
128
+ return result.set(this.width, this.height);
129
+ }
130
+ /**
131
+ * Replaces texture with new configuration.
132
+ * @param config Texture or atlas configuration
133
+ */
134
+ set(config) {
135
+ const lastWidth = this.width;
136
+ const lastHeight = this.height;
137
+ if (config instanceof Texture) {
138
+ this._private_setFromTexture(config);
139
+ }
140
+ else {
141
+ this._private_setFromAtlasConfig(config);
142
+ }
143
+ if (this.width !== lastWidth || this.height !== lastHeight) {
144
+ this._private_signalDiminsionsChangedInternal.emit(this.width, this.height, this);
145
+ }
146
+ }
147
+ _private_setFromTexture(texture) {
148
+ const width = texture.image?.naturalWidth ?? TEXTURE_DEFAULT_SIZE;
149
+ const height = texture.image?.naturalHeight ?? TEXTURE_DEFAULT_SIZE;
150
+ this._private_sourceWidth = width;
151
+ this._private_sourceHeight = height;
152
+ if (this.texture !== texture) {
153
+ this._private_textureInternal = texture;
154
+ this._private_textureDirtyInternal = true;
155
+ }
156
+ if (this._private_frameX !== 0 ||
157
+ this._private_frameY !== 0 ||
158
+ this._private_frameWidth !== width ||
159
+ this._private_frameHeight !== height ||
160
+ this._private_scaleInternal !== 1 || this._private_rotatedInternal) {
161
+ this._private_frameX = 0;
162
+ this._private_frameY = 0;
163
+ this._private_frameWidth = width;
164
+ this._private_frameHeight = height;
165
+ this._private_rotatedInternal = false;
166
+ this._private_scaleInternal = 1;
167
+ this._private_uvTransformDirtyInternal = true;
168
+ }
169
+ if (this._private_trimLeft !== 0 ||
170
+ this._private_trimRight !== 0 ||
171
+ this._private_trimTop !== 0 ||
172
+ this._private_trimBottom !== 0) {
173
+ this._private_trimLeft = 0;
174
+ this._private_trimRight = 0;
175
+ this._private_trimTop = 0;
176
+ this._private_trimBottom = 0;
177
+ this._private_trimDirtyInternal = true;
178
+ }
179
+ }
180
+ _private_setFromAtlasConfig(config) {
181
+ assertValidPositiveNumber(config.sourceSize.w, "FXTextureView.setFromAtlasConfig.config.sourceSize.w");
182
+ assertValidPositiveNumber(config.sourceSize.h, "FXTextureView.setFromAtlasConfig.config.sourceSize.h");
183
+ assertValidNonNegativeNumber(config.frame.x, "FXTextureView.setFromAtlasConfig.config.frame.x");
184
+ assertValidNonNegativeNumber(config.frame.y, "FXTextureView.setFromAtlasConfig.config.frame.y");
185
+ assertValidPositiveNumber(config.frame.w, "FXTextureView.setFromAtlasConfig.config.frame.w");
186
+ assertValidPositiveNumber(config.frame.h, "FXTextureView.setFromAtlasConfig.config.frame.h");
187
+ assertValidNonNegativeNumber(config.spriteSourceSize.x, "FXTextureView.setFromAtlasConfig.config.spriteSourceSize.x");
188
+ assertValidNonNegativeNumber(config.spriteSourceSize.y, "FXTextureView.setFromAtlasConfig.config.spriteSourceSize.y");
189
+ assertValidPositiveNumber(config.spriteSourceSize.w, "FXTextureView.setFromAtlasConfig.config.spriteSourceSize.w");
190
+ assertValidPositiveNumber(config.spriteSourceSize.h, "FXTextureView.setFromAtlasConfig.config.spriteSourceSize.h");
191
+ if (config.scale !== undefined) {
192
+ assertValidPositiveNumber(config.scale, "FXTextureView.setFromAtlasConfig.config.scale");
193
+ }
194
+ this._private_sourceWidth = config.sourceSize.w;
195
+ this._private_sourceHeight = config.sourceSize.h;
196
+ if (this.texture !== config.texture) {
197
+ this._private_textureInternal = config.texture;
198
+ this._private_textureDirtyInternal = true;
199
+ }
200
+ const scale = config.scale ?? 1;
201
+ const rotated = config.rotated ?? false;
202
+ if (this._private_frameX !== 0 ||
203
+ this._private_frameY !== 0 ||
204
+ this._private_frameWidth !== config.frame.w ||
205
+ this._private_frameHeight !== config.frame.h ||
206
+ this._private_scaleInternal !== scale ||
207
+ this._private_rotatedInternal !== rotated) {
208
+ this._private_frameX = config.frame.x;
209
+ this._private_frameY = config.frame.y;
210
+ this._private_frameWidth = config.frame.w;
211
+ this._private_frameHeight = config.frame.h;
212
+ this._private_scaleInternal = scale;
213
+ this._private_rotatedInternal = rotated;
214
+ this._private_uvTransformDirtyInternal = true;
215
+ }
216
+ const spriteSource = config.spriteSourceSize;
217
+ const trimLeft = spriteSource.x;
218
+ const trimTop = spriteSource.y;
219
+ const trimRight = this._private_sourceWidth - spriteSource.x - spriteSource.w;
220
+ const trimBottom = this._private_sourceHeight - spriteSource.y - spriteSource.h;
221
+ if (this._private_trimLeft !== trimLeft ||
222
+ this._private_trimRight !== trimRight ||
223
+ this._private_trimTop !== trimTop ||
224
+ this._private_trimBottom !== trimBottom) {
225
+ this._private_trimLeft = trimLeft;
226
+ this._private_trimRight = trimRight;
227
+ this._private_trimTop = trimTop;
228
+ this._private_trimBottom = trimBottom;
229
+ this._private_trimDirtyInternal = true;
230
+ }
231
+ }
232
+ }
233
+
234
+ export { FXTextureView };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Abstract base class for particle color nodes
3
+ */
4
+ export declare abstract class FXNodeColor {
5
+ /** Releases any GPU resources held by this node */
6
+ destroy?(): void;
7
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Abstract base class for particle color nodes
3
+ */
4
+ class FXNodeColor {
5
+ constructor() {
6
+ /** @internal */
7
+ this.affectsDepth = false;
8
+ }
9
+ }
10
+
11
+ export { FXNodeColor };
@@ -0,0 +1,25 @@
1
+ import type { FXColor } from "../../miscellaneous/color/FXColor";
2
+ import type { FXCurve1DConfig } from "../../miscellaneous/curve/FXCurve1D";
3
+ import { FXCurve1D } from "../../miscellaneous/curve/FXCurve1D";
4
+ import { FXNodeColor } from "./FXNodeColor";
5
+ /**
6
+ * Color node that animates particle color along a gradient curve over its lifetime
7
+ */
8
+ export declare class FXNodeColorOverLife extends FXNodeColor {
9
+ private readonly uniformName;
10
+ private curveInternal;
11
+ /**
12
+ * @param curve - Color gradient defining the animation over the particle lifetime
13
+ */
14
+ constructor(curve: FXCurve1DConfig<FXColor>);
15
+ /** Current color gradient curve */
16
+ get curve(): FXCurve1D<FXColor>;
17
+ /**
18
+ * Replaces the color gradient curve and rebuilds the backing texture
19
+ *
20
+ * @param value - New color gradient configuration
21
+ */
22
+ set curve(value: FXCurve1DConfig<FXColor>);
23
+ /** Releases the backing gradient texture */
24
+ destroy(): void;
25
+ }
@@ -0,0 +1,60 @@
1
+ import { FXCurve1D } from '../../miscellaneous/curve/FXCurve1D.js';
2
+ import { buildGradientTexture, getNextInstanceId } from '../../miscellaneous/miscellaneous.js';
3
+ import { checkSRGBSupport } from '../../miscellaneous/webglCapabilities.js';
4
+ import { FXNodeColor } from './FXNodeColor.js';
5
+
6
+ /**
7
+ * Color node that animates particle color along a gradient curve over its lifetime
8
+ */
9
+ class FXNodeColorOverLife extends FXNodeColor {
10
+ /**
11
+ * @param curve - Color gradient defining the animation over the particle lifetime
12
+ */
13
+ constructor(curve) {
14
+ super();
15
+ const id = getNextInstanceId();
16
+ this._private_uniformName = `u_ColorOverLife_${id}`;
17
+ this._private_curveInternal = new FXCurve1D(curve);
18
+ this._private_gradientTexture = buildGradientTexture(this._private_curveInternal);
19
+ this.affectsDepth = true;
20
+ this.cacheKey = "color-over-life";
21
+ this.uniformDeclarations = [`uniform sampler2D ${this._private_uniformName};`];
22
+ this.uniforms = {
23
+ [this._private_uniformName]: { value: this._private_gradientTexture },
24
+ };
25
+ this.helperFunctions = `
26
+ vec4 fxSampleColorOverLife(sampler2D gradientTexture) {
27
+ // Normalized particle age [0, 1] maps to the gradient texture's x-axis
28
+ float lifetimeRatio = clamp(PARTICLE_AGE / PARTICLE_LIFETIME, 0.0, 1.0);
29
+ vec4 color = texture2D(gradientTexture, vec2(lifetimeRatio, 0.5));
30
+
31
+ // Apply gamma correction when sRGB textures are not natively supported
32
+ ${checkSRGBSupport() ? "" : "color = vec4(pow(color.rgb, vec3(2.2)), color.a);"}
33
+
34
+ return color;
35
+ }
36
+ `;
37
+ this.colorExpression = `fxSampleColorOverLife(${this._private_uniformName})`;
38
+ }
39
+ /** Current color gradient curve */
40
+ get curve() {
41
+ return this._private_curveInternal;
42
+ }
43
+ /**
44
+ * Replaces the color gradient curve and rebuilds the backing texture
45
+ *
46
+ * @param value - New color gradient configuration
47
+ */
48
+ set curve(value) {
49
+ this._private_curveInternal = new FXCurve1D(value);
50
+ this._private_gradientTexture.dispose();
51
+ this._private_gradientTexture = buildGradientTexture(this._private_curveInternal);
52
+ this.uniforms[this._private_uniformName].value = this._private_gradientTexture;
53
+ }
54
+ /** Releases the backing gradient texture */
55
+ destroy() {
56
+ this._private_gradientTexture.dispose();
57
+ }
58
+ }
59
+
60
+ export { FXNodeColorOverLife };
@@ -0,0 +1,18 @@
1
+ import { FXNodeColor } from "./FXNodeColor";
2
+ /**
3
+ * Color node that clips a particle to a circle with a configurable soft edge
4
+ *
5
+ * @remarks
6
+ * Fades alpha from fully opaque at `innerRadius` to fully transparent at the billboard edge
7
+ * (UV radius `0.5`). An `innerRadius` of `0.5` produces a hard circular clip with no gradient.
8
+ */
9
+ export declare class FXNodeSphericalClip extends FXNodeColor {
10
+ private readonly uniformInnerRadius;
11
+ /**
12
+ * @param innerRadius - UV radius where the fade begins; must be non-negative. Defaults to `0.5`
13
+ */
14
+ constructor(innerRadius?: number);
15
+ /** UV radius of the fully opaque inner area */
16
+ get innerRadius(): number;
17
+ set innerRadius(value: number);
18
+ }
@@ -0,0 +1,45 @@
1
+ import { assertValidNonNegativeNumber } from '../../miscellaneous/asserts.js';
2
+ import { getNextInstanceId } from '../../miscellaneous/miscellaneous.js';
3
+ import { FXNodeColor } from './FXNodeColor.js';
4
+
5
+ /**
6
+ * Color node that clips a particle to a circle with a configurable soft edge
7
+ *
8
+ * @remarks
9
+ * Fades alpha from fully opaque at `innerRadius` to fully transparent at the billboard edge
10
+ * (UV radius `0.5`). An `innerRadius` of `0.5` produces a hard circular clip with no gradient.
11
+ */
12
+ class FXNodeSphericalClip extends FXNodeColor {
13
+ /**
14
+ * @param innerRadius - UV radius where the fade begins; must be non-negative. Defaults to `0.5`
15
+ */
16
+ constructor(innerRadius = 0.5) {
17
+ super();
18
+ assertValidNonNegativeNumber(innerRadius, "FXNodeSphericalClip.constructor.innerRadius");
19
+ const id = getNextInstanceId();
20
+ this._private_uniformInnerRadius = `u_SphericalClipInnerRadius_${id}`;
21
+ this.affectsDepth = true;
22
+ this.cacheKey = "spherical-clip";
23
+ this.uniformDeclarations = [`uniform float ${this._private_uniformInnerRadius};`];
24
+ this.uniforms = {
25
+ [this._private_uniformInnerRadius]: { value: innerRadius },
26
+ };
27
+ this.helperFunctions = `
28
+ vec4 fxApplySphericalClip(float innerRadius) {
29
+ float distanceToCenter = distance(p_uv, vec2(0.5, 0.5));
30
+ return vec4(1.0, 1.0, 1.0, 1.0 - smoothstep(innerRadius, 0.5, distanceToCenter));
31
+ }
32
+ `;
33
+ this.colorExpression = `fxApplySphericalClip(${this._private_uniformInnerRadius})`;
34
+ }
35
+ /** UV radius of the fully opaque inner area */
36
+ get innerRadius() {
37
+ return this.uniforms[this._private_uniformInnerRadius].value;
38
+ }
39
+ set innerRadius(value) {
40
+ assertValidNonNegativeNumber(value, "FXNodeSphericalClip.innerRadius");
41
+ this.uniforms[this._private_uniformInnerRadius].value = value;
42
+ }
43
+ }
44
+
45
+ export { FXNodeSphericalClip };
@@ -0,0 +1,7 @@
1
+ import { FXNodeNormal } from "./FXNodeNormal";
2
+ /**
3
+ * Normal node that outputs a constant face-aligned normal pointing toward the camera
4
+ */
5
+ export declare class FXNodeFlatNormal extends FXNodeNormal {
6
+ constructor();
7
+ }
@@ -0,0 +1,17 @@
1
+ import { FXNodeNormal } from './FXNodeNormal.js';
2
+
3
+ /**
4
+ * Normal node that outputs a constant face-aligned normal pointing toward the camera
5
+ */
6
+ class FXNodeFlatNormal extends FXNodeNormal {
7
+ constructor() {
8
+ super();
9
+ this.cacheKey = "flat-normal";
10
+ this.uniformDeclarations = [];
11
+ this.uniforms = {};
12
+ this.helperFunctions = "";
13
+ this.normalExpression = "vec3(0.0, 0.0, 1.0)";
14
+ }
15
+ }
16
+
17
+ export { FXNodeFlatNormal };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Abstract base class for particle normal nodes
3
+ */
4
+ export declare abstract class FXNodeNormal {
5
+ /** Releases any GPU resources held by this node */
6
+ destroy?(): void;
7
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Abstract base class for particle normal nodes
3
+ */
4
+ class FXNodeNormal {
5
+ }
6
+
7
+ export { FXNodeNormal };
@@ -0,0 +1,7 @@
1
+ import { FXNodeNormal } from "./FXNodeNormal";
2
+ /**
3
+ * Normal node that makes flat billboard particles appear as spheres under directional lighting
4
+ */
5
+ export declare class FXNodeSphericalNormal extends FXNodeNormal {
6
+ constructor();
7
+ }
@@ -1,9 +1,12 @@
1
- import { FXNormalNode } from './FXNormalNode.js';
1
+ import { FXNodeNormal } from './FXNodeNormal.js';
2
2
 
3
- class FXSphericalNormalNode extends FXNormalNode {
3
+ /**
4
+ * Normal node that makes flat billboard particles appear as spheres under directional lighting
5
+ */
6
+ class FXNodeSphericalNormal extends FXNodeNormal {
4
7
  constructor() {
5
8
  super();
6
- this.cacheKey = "spherical";
9
+ this.cacheKey = "spherical-normal";
7
10
  this.uniformDeclarations = [];
8
11
  this.uniforms = {};
9
12
  this.helperFunctions = `
@@ -16,4 +19,4 @@ class FXSphericalNormalNode extends FXNormalNode {
16
19
  }
17
20
  }
18
21
 
19
- export { FXSphericalNormalNode };
22
+ export { FXNodeSphericalNormal };
@@ -0,0 +1,47 @@
1
+ import { FXTextureView } from "../../miscellaneous/texture/FXTextureView";
2
+ import type { FXTextureConfig } from "../../miscellaneous/texture/FXTextureView.Internal";
3
+ import { FXNodeTexture } from "./FXNodeTexture";
4
+ /**
5
+ * Options for {@link FXNodeAnimatedTexture}
6
+ */
7
+ export interface FXNodeAnimatedTextureOptions {
8
+ /** Texture or atlas configuration containing all animation frames */
9
+ texture: FXTextureConfig;
10
+ /** Number of frame columns in the sprite sheet */
11
+ columns: number;
12
+ /** Number of frame rows in the sprite sheet */
13
+ rows: number;
14
+ /**
15
+ * If true, blends between adjacent frames for smoother animation
16
+ *
17
+ * @defaultValue `false`
18
+ */
19
+ interpolate?: boolean;
20
+ }
21
+ /**
22
+ * Texture node that animates through sprite sheet frames over a particle's lifetime
23
+ *
24
+ * @remarks
25
+ * Frames advance from the first to the last as the particle ages. The total frame count
26
+ * is `columns * rows`. Both `columns` and `rows` must be positive integers.
27
+ */
28
+ export declare class FXNodeAnimatedTexture extends FXNodeTexture {
29
+ private readonly uniformTexture;
30
+ private readonly uniformUVTransform;
31
+ private readonly uniformColumns;
32
+ private readonly uniformRows;
33
+ private readonly textureViewInternal;
34
+ /**
35
+ * @param options - Sprite sheet configuration
36
+ */
37
+ constructor({ texture, columns, rows, interpolate }: FXNodeAnimatedTextureOptions);
38
+ /** View describing the texture and its atlas region */
39
+ get textureView(): FXTextureView;
40
+ /** Number of frame columns in the sprite sheet */
41
+ get columns(): number;
42
+ /** Number of frame rows in the sprite sheet */
43
+ get rows(): number;
44
+ set textureView(config: FXTextureConfig);
45
+ set columns(value: number);
46
+ set rows(value: number);
47
+ }
@@ -0,0 +1,137 @@
1
+ import { assertValidPositiveNumber } from '../../miscellaneous/asserts.js';
2
+ import { getNextInstanceId } from '../../miscellaneous/miscellaneous.js';
3
+ import { FXTextureView } from '../../miscellaneous/texture/FXTextureView.js';
4
+ import { checkSRGBSupport } from '../../miscellaneous/webglCapabilities.js';
5
+ import { FXNodeTexture } from './FXNodeTexture.js';
6
+
7
+ /**
8
+ * Texture node that animates through sprite sheet frames over a particle's lifetime
9
+ *
10
+ * @remarks
11
+ * Frames advance from the first to the last as the particle ages. The total frame count
12
+ * is `columns * rows`. Both `columns` and `rows` must be positive integers.
13
+ */
14
+ class FXNodeAnimatedTexture extends FXNodeTexture {
15
+ /**
16
+ * @param options - Sprite sheet configuration
17
+ */
18
+ constructor({ texture, columns, rows, interpolate = false }) {
19
+ super();
20
+ assertValidPositiveNumber(columns, "FXNodeAnimatedTexture.constructor.columns");
21
+ assertValidPositiveNumber(rows, "FXNodeAnimatedTexture.constructor.rows");
22
+ const idx = getNextInstanceId();
23
+ this._private_uniformTexture = `u_AnimatedTexture_${idx}`;
24
+ this._private_uniformUVTransform = `u_AnimatedTextureUVTransform_${idx}`;
25
+ this._private_uniformColumns = `u_AnimatedTextureColumns_${idx}`;
26
+ this._private_uniformRows = `u_AnimatedTextureRows_${idx}`;
27
+ this._private_textureViewInternal = new FXTextureView(texture);
28
+ const uvTransform = this._private_textureViewInternal.calculateUVTransform();
29
+ this._private_textureViewInternal.setTextureDirtyFalse();
30
+ this._private_textureViewInternal.setUVTransformDirtyFalse();
31
+ const gammaCorrection = checkSRGBSupport()
32
+ ? ""
33
+ : "color = vec4(pow(color.rgb, vec3(2.2)), color.a);";
34
+ this.affectsDepth = true;
35
+ this.cacheKey = interpolate ? "animated-texture-interpolated" : "animated-texture";
36
+ this.uniformDeclarations = [
37
+ `uniform sampler2D ${this._private_uniformTexture};`,
38
+ `uniform mat3 ${this._private_uniformUVTransform};`,
39
+ `uniform float ${this._private_uniformColumns};`,
40
+ `uniform float ${this._private_uniformRows};`,
41
+ ];
42
+ this.uniforms = {
43
+ [this._private_uniformTexture]: { value: this._private_textureViewInternal.texture },
44
+ [this._private_uniformUVTransform]: { value: uvTransform },
45
+ [this._private_uniformColumns]: { value: columns },
46
+ [this._private_uniformRows]: { value: rows },
47
+ };
48
+ this.helperFunctions = interpolate
49
+ ? `
50
+ vec2 fxFrameToUV(vec2 uv, float frame, float columns, float rows) {
51
+ float column = mod(frame, columns);
52
+ float row = floor(frame / columns);
53
+ return vec2((column + uv.x) / columns, 1.0 - (row + 1.0 - uv.y) / rows);
54
+ }
55
+
56
+ vec4 fxSampleAnimatedTexture(sampler2D spriteSheet, mat3 uvTransform, vec2 uv, float columns, float rows) {
57
+ float totalFrames = columns * rows;
58
+
59
+ // Fractional frame position drives both frame selection and blend weight
60
+ float exactFrame = PARTICLE_AGE / PARTICLE_LIFETIME * totalFrames;
61
+ float currentFrame = clamp(floor(exactFrame), 0.0, totalFrames - 1.0);
62
+ float nextFrame = clamp(currentFrame + 1.0, 0.0, totalFrames - 1.0);
63
+ float blendWeight = fract(exactFrame);
64
+
65
+ vec2 currentUV = (uvTransform * vec3(fxFrameToUV(uv, currentFrame, columns, rows), 1.0)).xy;
66
+ vec2 nextUV = (uvTransform * vec3(fxFrameToUV(uv, nextFrame, columns, rows), 1.0)).xy;
67
+
68
+ vec4 color = mix(
69
+ texture2D(spriteSheet, currentUV),
70
+ texture2D(spriteSheet, nextUV),
71
+ blendWeight
72
+ );
73
+
74
+ // Apply gamma correction when sRGB textures are not natively supported
75
+ ${gammaCorrection}
76
+
77
+ return color;
78
+ }
79
+ `
80
+ : `
81
+ vec4 fxSampleAnimatedTexture(sampler2D spriteSheet, mat3 uvTransform, vec2 uv, float columns, float rows) {
82
+ // Determine which frame to display based on normalized particle age
83
+ float totalFrames = columns * rows;
84
+ float frame = floor(PARTICLE_AGE / PARTICLE_LIFETIME * totalFrames);
85
+ frame = clamp(frame, 0.0, totalFrames - 1.0);
86
+
87
+ // Locate the frame's column and row in the sprite sheet grid
88
+ float column = mod(frame, columns);
89
+ float row = floor(frame / columns);
90
+
91
+ // Remap UV to the frame's tile region, then apply atlas transform
92
+ vec2 frameUV = vec2((column + uv.x) / columns, 1.0 - (row + 1.0 - uv.y) / rows);
93
+ vec2 atlasUV = (uvTransform * vec3(frameUV, 1.0)).xy;
94
+ vec4 color = texture2D(spriteSheet, atlasUV);
95
+
96
+ // Apply gamma correction when sRGB textures are not natively supported
97
+ ${gammaCorrection}
98
+
99
+ return color;
100
+ }
101
+ `;
102
+ this.colorExpression = `fxSampleAnimatedTexture(${this._private_uniformTexture}, ${this._private_uniformUVTransform}, p_uv, ${this._private_uniformColumns}, ${this._private_uniformRows})`;
103
+ }
104
+ /** View describing the texture and its atlas region */
105
+ get textureView() {
106
+ return this._private_textureViewInternal;
107
+ }
108
+ /** Number of frame columns in the sprite sheet */
109
+ get columns() {
110
+ return this.uniforms[this._private_uniformColumns].value;
111
+ }
112
+ /** Number of frame rows in the sprite sheet */
113
+ get rows() {
114
+ return this.uniforms[this._private_uniformRows].value;
115
+ }
116
+ set textureView(config) {
117
+ this._private_textureViewInternal.set(config);
118
+ if (this._private_textureViewInternal.textureDirty) {
119
+ this.uniforms[this._private_uniformTexture].value = this._private_textureViewInternal.texture;
120
+ this._private_textureViewInternal.setTextureDirtyFalse();
121
+ }
122
+ if (this._private_textureViewInternal.uvTransformDirty) {
123
+ this._private_textureViewInternal.calculateUVTransform(this.uniforms[this._private_uniformUVTransform].value);
124
+ this._private_textureViewInternal.setUVTransformDirtyFalse();
125
+ }
126
+ }
127
+ set columns(value) {
128
+ assertValidPositiveNumber(value, "FXNodeAnimatedTexture.columns");
129
+ this.uniforms[this._private_uniformColumns].value = value;
130
+ }
131
+ set rows(value) {
132
+ assertValidPositiveNumber(value, "FXNodeAnimatedTexture.rows");
133
+ this.uniforms[this._private_uniformRows].value = value;
134
+ }
135
+ }
136
+
137
+ export { FXNodeAnimatedTexture };