fireworks-engine 1.0.0
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.
- package/LICENSE +24 -0
- package/README.md +297 -0
- package/index.d.ts +691 -0
- package/package.json +49 -0
- package/src/burstConfigBuilder.js +262 -0
- package/src/burstSpawn.js +764 -0
- package/src/colorUtils.js +80 -0
- package/src/configBuilder.js +300 -0
- package/src/defaults.js +87 -0
- package/src/index.js +13 -0
- package/src/mathResolvers.js +77 -0
- package/src/presets.js +811 -0
- package/src/renderer.js +863 -0
- package/src/rocketLaunch.js +791 -0
- package/src/runtime.js +796 -0
- package/src/runtimeCapabilities.js +7 -0
- package/src/runtimeLifecycle.js +330 -0
- package/src/update.js +307 -0
- package/src/valueNormalizers.js +160 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BURST_FLASH_ATTACK_UNIT,
|
|
3
|
+
DEFAULT_BURST_SINGLE_COLOR,
|
|
4
|
+
DEFAULT_BURST_PALETTE,
|
|
5
|
+
GLYPH_SPRITE_SIZE_MIN,
|
|
6
|
+
GLYPH_SPRITE_RADIUS_SCALE,
|
|
7
|
+
WHITE_BRIGHTNESS_REFERENCE,
|
|
8
|
+
} from './defaults.js';
|
|
9
|
+
import {
|
|
10
|
+
clamp,
|
|
11
|
+
randomRange,
|
|
12
|
+
resolveEnvelopeTimeScale,
|
|
13
|
+
resolveFinite,
|
|
14
|
+
resolveScaledInteger,
|
|
15
|
+
resolveScaledInverseOverride,
|
|
16
|
+
resolveScaledOverride,
|
|
17
|
+
resolveSizeScale,
|
|
18
|
+
} from './mathResolvers.js';
|
|
19
|
+
import {
|
|
20
|
+
normalizeGroupedPublicOptions,
|
|
21
|
+
resolveBurstFormShapeSet,
|
|
22
|
+
resolveBurstFlashColorMode,
|
|
23
|
+
} from './valueNormalizers.js';
|
|
24
|
+
import {
|
|
25
|
+
buildBurstOptions,
|
|
26
|
+
resolveTwinkleParticleSizeScale,
|
|
27
|
+
} from './burstConfigBuilder.js';
|
|
28
|
+
import {
|
|
29
|
+
normalizeHexColor,
|
|
30
|
+
toHexChannel,
|
|
31
|
+
} from './colorUtils.js';
|
|
32
|
+
|
|
33
|
+
const FORM_PARTICLE_TUNING = {
|
|
34
|
+
angleJitter: 0.12,
|
|
35
|
+
dragMin: 0.965,
|
|
36
|
+
dragMax: 0.982,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FORM_SPIN_TUNING = {
|
|
40
|
+
phaseCycleRad: Math.PI * 2,
|
|
41
|
+
phaseSpeedMin: 4.8,
|
|
42
|
+
phaseSpeedMax: 7.6,
|
|
43
|
+
angleSpeedMin: -1.6,
|
|
44
|
+
angleSpeedMax: 1.6,
|
|
45
|
+
minWidthScaleMin: 0.02,
|
|
46
|
+
minWidthScaleMax: 0.12,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const FORM_SPIN_VARIANCE_TUNING = {
|
|
50
|
+
strengthMin: 0.4,
|
|
51
|
+
strengthMax: 1.2,
|
|
52
|
+
phaseSpeedJitterMin: 0.65,
|
|
53
|
+
phaseSpeedJitterMax: 1.35,
|
|
54
|
+
angleSpeedJitterMin: 0.65,
|
|
55
|
+
angleSpeedJitterMax: 1.35,
|
|
56
|
+
flipExponentMin: 0.7,
|
|
57
|
+
flipExponentMax: 1.45,
|
|
58
|
+
rateWaveSpeedMin: 0.9,
|
|
59
|
+
rateWaveSpeedMax: 2.2,
|
|
60
|
+
rateWaveAmplitudeMin: 0.08,
|
|
61
|
+
rateWaveAmplitudeMax: 0.34,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const FORM_SPIN_ACTIVITY_TUNING = {
|
|
65
|
+
budgetJitterMin: 0.85,
|
|
66
|
+
budgetJitterMax: 1.25,
|
|
67
|
+
initialActiveChanceBase: 0.28,
|
|
68
|
+
initialActiveChanceScale: 0.55,
|
|
69
|
+
initialActiveChanceMax: 0.9,
|
|
70
|
+
durationMinFactor: 0.45,
|
|
71
|
+
durationMaxFactor: 1.7,
|
|
72
|
+
pauseMinFactor: 0.28,
|
|
73
|
+
pauseMaxFactor: 1.9,
|
|
74
|
+
minWindow: 0.05,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const FORM_WOBBLE_TUNING = {
|
|
78
|
+
lateralSpeedMin: 20,
|
|
79
|
+
lateralSpeedMax: 44,
|
|
80
|
+
targetHoldMin: 0.05,
|
|
81
|
+
targetHoldMax: 0.22,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const BURST_SPARK_TUNING = {
|
|
85
|
+
angleJitter: 0.18,
|
|
86
|
+
simpleFactor: 0.82,
|
|
87
|
+
gravityFactor: 0.74,
|
|
88
|
+
dragMin: 0.958,
|
|
89
|
+
dragMax: 0.978,
|
|
90
|
+
minInitialSpeed: 1,
|
|
91
|
+
depthMinProjection: 0.12,
|
|
92
|
+
depthGamma: 1.1,
|
|
93
|
+
depthSpeedRangeEpsilon: 0.0001,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const BURST_FLASH_RUNTIME_LIMITS = {
|
|
97
|
+
minLife: 0.02,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const BURST_FLASH_DURATION_LIMITS = {
|
|
101
|
+
minAttackDuration: 0.0001,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const TWINKLE_CLOUD_RUNTIME_LIMITS = {
|
|
105
|
+
countMin: 1,
|
|
106
|
+
spawnSpreadPowerGuard: 0.001,
|
|
107
|
+
lifeJitterMin: 0.7,
|
|
108
|
+
lifeJitterMax: 1.2,
|
|
109
|
+
lifeClampMin: 0.01,
|
|
110
|
+
sizeScaleMin: 0.7,
|
|
111
|
+
sizeScaleMax: 1.25,
|
|
112
|
+
alphaScaleMin: 0.8,
|
|
113
|
+
alphaScaleMax: 1.15,
|
|
114
|
+
minRenderedSize: 0.12,
|
|
115
|
+
minCloudLife: 0.02,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const BURST_DIRECTION_TUNING = {
|
|
119
|
+
minConeDeg: 0,
|
|
120
|
+
maxConeDeg: 360,
|
|
121
|
+
fullConeEpsilonRad: 0.0001,
|
|
122
|
+
defaultDirectionDeg: 0,
|
|
123
|
+
defaultConeDeg: 360,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const BURST_DIRECTION_CONSTANTS = {
|
|
127
|
+
fullCircleRad: Math.PI * 2,
|
|
128
|
+
degToRad: Math.PI / 180,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const RGB_COLOR_TUNING = {
|
|
132
|
+
channelStep: 32,
|
|
133
|
+
channelMax: 255,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function randomQuantizedColorChannel() {
|
|
137
|
+
const step = RGB_COLOR_TUNING.channelStep;
|
|
138
|
+
const maxStepIndex = Math.ceil(RGB_COLOR_TUNING.channelMax / step);
|
|
139
|
+
const stepIndex = Math.floor(randomRange(0, maxStepIndex + 1));
|
|
140
|
+
return stepIndex >= maxStepIndex
|
|
141
|
+
? RGB_COLOR_TUNING.channelMax
|
|
142
|
+
: stepIndex * step;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveBurstFormGlyphSet(glyph) {
|
|
146
|
+
const symbols = Array.from(glyph);
|
|
147
|
+
return symbols.length > 0 ? symbols : ['★'];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveDirectedBurstAngle(index, totalCount, directionDeg, coneDeg, jitter) {
|
|
151
|
+
const safeDirectionDeg = Number.isFinite(directionDeg)
|
|
152
|
+
? directionDeg
|
|
153
|
+
: BURST_DIRECTION_TUNING.defaultDirectionDeg;
|
|
154
|
+
const safeConeDeg = Number.isFinite(coneDeg)
|
|
155
|
+
? coneDeg
|
|
156
|
+
: BURST_DIRECTION_TUNING.defaultConeDeg;
|
|
157
|
+
const boundedConeDeg = clamp(
|
|
158
|
+
safeConeDeg,
|
|
159
|
+
BURST_DIRECTION_TUNING.minConeDeg,
|
|
160
|
+
BURST_DIRECTION_TUNING.maxConeDeg,
|
|
161
|
+
);
|
|
162
|
+
const coneRad = boundedConeDeg * BURST_DIRECTION_CONSTANTS.degToRad;
|
|
163
|
+
|
|
164
|
+
if (coneRad >= BURST_DIRECTION_CONSTANTS.fullCircleRad - BURST_DIRECTION_TUNING.fullConeEpsilonRad) {
|
|
165
|
+
return ((BURST_DIRECTION_CONSTANTS.fullCircleRad * index) / totalCount)
|
|
166
|
+
+ randomRange(-jitter, jitter);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const directionRad = -safeDirectionDeg * BURST_DIRECTION_CONSTANTS.degToRad;
|
|
170
|
+
const halfCone = coneRad / 2;
|
|
171
|
+
const startAngle = directionRad - halfCone;
|
|
172
|
+
const endAngle = directionRad + halfCone;
|
|
173
|
+
const baseAngle = totalCount <= 1
|
|
174
|
+
? directionRad
|
|
175
|
+
: startAngle + (coneRad * (index / (totalCount - 1)));
|
|
176
|
+
|
|
177
|
+
return clamp(baseAngle + randomRange(-jitter, jitter), startAngle, endAngle);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function resolveBurstBaseColor(engine, palette, colorMode = engine.burstForms.colorMode, singleColor = engine.burstForms.singleColor) {
|
|
181
|
+
if (colorMode === 'single') {
|
|
182
|
+
return singleColor;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (colorMode === 'rgb') {
|
|
186
|
+
const r = randomQuantizedColorChannel();
|
|
187
|
+
const g = randomQuantizedColorChannel();
|
|
188
|
+
const b = randomQuantizedColorChannel();
|
|
189
|
+
return `#${toHexChannel(r)}${toHexChannel(g)}${toHexChannel(b)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const safePalette = Array.isArray(palette) && palette.length > 0
|
|
193
|
+
? palette
|
|
194
|
+
: [DEFAULT_BURST_SINGLE_COLOR];
|
|
195
|
+
const paletteIndex = Math.floor(randomRange(0, safePalette.length));
|
|
196
|
+
return safePalette[clamp(paletteIndex, 0, safePalette.length - 1)] ?? safePalette[0];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function resolveBurstFlashAttackDuration(attackScale) {
|
|
200
|
+
return Math.max(BURST_FLASH_DURATION_LIMITS.minAttackDuration, attackScale * BURST_FLASH_ATTACK_UNIT);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveBurstCurrentConfig(engine) {
|
|
204
|
+
return {
|
|
205
|
+
sizeScale: engine.sizeScale,
|
|
206
|
+
burstForms: engine.burstForms,
|
|
207
|
+
burstSparks: engine.burstSparks,
|
|
208
|
+
burstCenterFlash: engine.burstCenterFlash,
|
|
209
|
+
burstTwinkleCloud: engine.burstTwinkleCloud,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function createBurstRequests(x, y, options, flashOptions = options) {
|
|
214
|
+
const requests = [];
|
|
215
|
+
const burstForms = options.burstForms ?? options;
|
|
216
|
+
const burstSparks = options.burstSparks ?? options;
|
|
217
|
+
const formCount = burstForms.count ?? 0;
|
|
218
|
+
|
|
219
|
+
if (burstForms.enabled && formCount > 0) {
|
|
220
|
+
requests.push({
|
|
221
|
+
x,
|
|
222
|
+
y,
|
|
223
|
+
options,
|
|
224
|
+
flashOptions,
|
|
225
|
+
layer: 'burstForm',
|
|
226
|
+
totalCount: formCount,
|
|
227
|
+
nextIndex: 0,
|
|
228
|
+
spawnBurstCenterFlash: true,
|
|
229
|
+
spawnBurstTwinkleCloud: true,
|
|
230
|
+
flashSpawned: false,
|
|
231
|
+
twinkleCloudSpawned: false,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sparkCount = burstSparks.count ?? 0;
|
|
236
|
+
if (burstSparks.enabled && sparkCount > 0) {
|
|
237
|
+
requests.push({
|
|
238
|
+
x,
|
|
239
|
+
y,
|
|
240
|
+
options,
|
|
241
|
+
flashOptions,
|
|
242
|
+
layer: 'burstSpark',
|
|
243
|
+
totalCount: sparkCount,
|
|
244
|
+
nextIndex: 0,
|
|
245
|
+
spawnBurstCenterFlash: requests.length === 0,
|
|
246
|
+
spawnBurstTwinkleCloud: requests.length === 0,
|
|
247
|
+
flashSpawned: false,
|
|
248
|
+
twinkleCloudSpawned: false,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return requests;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function enqueueBurst(engine, x, y, options = {}) {
|
|
256
|
+
if (!engine.running) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const burstOptions = buildBurstOptions(
|
|
261
|
+
options,
|
|
262
|
+
resolveBurstCurrentConfig(engine),
|
|
263
|
+
);
|
|
264
|
+
const requests = createBurstRequests(x, y, burstOptions, options);
|
|
265
|
+
|
|
266
|
+
if (requests.length === 0) {
|
|
267
|
+
spawnBurstCenterFlash(engine, x, y, options);
|
|
268
|
+
spawnBurstTwinkleCloud(engine, x, y, burstOptions);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
engine.burstQueue.push(...requests);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function spawnFormParticles(engine, request, startIndex, count) {
|
|
277
|
+
const { x, y, options } = request;
|
|
278
|
+
const burstForms = options.burstForms ?? options;
|
|
279
|
+
const {
|
|
280
|
+
speedMin,
|
|
281
|
+
speedMax,
|
|
282
|
+
gravity,
|
|
283
|
+
lifeMin,
|
|
284
|
+
lifeMax,
|
|
285
|
+
sizeMin,
|
|
286
|
+
sizeMax,
|
|
287
|
+
spin,
|
|
288
|
+
spinCoverage,
|
|
289
|
+
spinDuration,
|
|
290
|
+
spinPause,
|
|
291
|
+
highlight,
|
|
292
|
+
highlightAngleDeg,
|
|
293
|
+
highlightWidth,
|
|
294
|
+
wobble,
|
|
295
|
+
directionDeg,
|
|
296
|
+
coneDeg,
|
|
297
|
+
palette,
|
|
298
|
+
simple,
|
|
299
|
+
colorMode,
|
|
300
|
+
singleColor,
|
|
301
|
+
shape,
|
|
302
|
+
glyph,
|
|
303
|
+
} = burstForms;
|
|
304
|
+
const totalCount = request.totalCount;
|
|
305
|
+
const formShapeSet = resolveBurstFormShapeSet(shape);
|
|
306
|
+
const formShapeCount = formShapeSet.length;
|
|
307
|
+
const hasGlyphShape = formShapeSet.includes('glyph');
|
|
308
|
+
const glyphSet = hasGlyphShape ? resolveBurstFormGlyphSet(glyph) : null;
|
|
309
|
+
const glyphCount = glyphSet?.length ?? 0;
|
|
310
|
+
|
|
311
|
+
for (let index = startIndex; index < startIndex + count; index += 1) {
|
|
312
|
+
const angle = resolveDirectedBurstAngle(
|
|
313
|
+
index,
|
|
314
|
+
totalCount,
|
|
315
|
+
directionDeg,
|
|
316
|
+
coneDeg,
|
|
317
|
+
FORM_PARTICLE_TUNING.angleJitter,
|
|
318
|
+
);
|
|
319
|
+
const speed = randomRange(speedMin, speedMax);
|
|
320
|
+
const radius = randomRange(sizeMin, sizeMax);
|
|
321
|
+
const life = randomRange(lifeMin, lifeMax);
|
|
322
|
+
const baseColor = resolveBurstBaseColor(engine, palette, colorMode, singleColor);
|
|
323
|
+
const rawSpinStrength = Math.max(0, spin ?? 0);
|
|
324
|
+
const spinCoverageFactor = clamp(spinCoverage ?? 1, 0, 1);
|
|
325
|
+
const hasSpinActivity = rawSpinStrength > 0 && spinCoverageFactor > 0;
|
|
326
|
+
const spinStrength = hasSpinActivity
|
|
327
|
+
? rawSpinStrength * randomRange(
|
|
328
|
+
FORM_SPIN_VARIANCE_TUNING.strengthMin,
|
|
329
|
+
FORM_SPIN_VARIANCE_TUNING.strengthMax,
|
|
330
|
+
)
|
|
331
|
+
: 0;
|
|
332
|
+
const spinInitialActiveChance = clamp(
|
|
333
|
+
FORM_SPIN_ACTIVITY_TUNING.initialActiveChanceBase
|
|
334
|
+
+ (spinCoverageFactor * FORM_SPIN_ACTIVITY_TUNING.initialActiveChanceScale),
|
|
335
|
+
0,
|
|
336
|
+
FORM_SPIN_ACTIVITY_TUNING.initialActiveChanceMax,
|
|
337
|
+
);
|
|
338
|
+
const safeSpinDuration = Math.max(FORM_SPIN_ACTIVITY_TUNING.minWindow, spinDuration ?? 0);
|
|
339
|
+
const safeSpinPause = Math.max(FORM_SPIN_ACTIVITY_TUNING.minWindow, spinPause ?? 0);
|
|
340
|
+
const spinActiveHoldMin = Math.max(
|
|
341
|
+
FORM_SPIN_ACTIVITY_TUNING.minWindow,
|
|
342
|
+
safeSpinDuration * FORM_SPIN_ACTIVITY_TUNING.durationMinFactor,
|
|
343
|
+
);
|
|
344
|
+
const spinActiveHoldMax = Math.max(
|
|
345
|
+
spinActiveHoldMin,
|
|
346
|
+
safeSpinDuration * FORM_SPIN_ACTIVITY_TUNING.durationMaxFactor,
|
|
347
|
+
);
|
|
348
|
+
const spinInactiveHoldMin = Math.max(
|
|
349
|
+
FORM_SPIN_ACTIVITY_TUNING.minWindow,
|
|
350
|
+
safeSpinPause * FORM_SPIN_ACTIVITY_TUNING.pauseMinFactor,
|
|
351
|
+
);
|
|
352
|
+
const spinInactiveHoldMax = Math.max(
|
|
353
|
+
spinInactiveHoldMin,
|
|
354
|
+
safeSpinPause * FORM_SPIN_ACTIVITY_TUNING.pauseMaxFactor,
|
|
355
|
+
);
|
|
356
|
+
const spinInitiallyActive = hasSpinActivity && Math.random() < spinInitialActiveChance;
|
|
357
|
+
const spinHoldMin = spinInitiallyActive
|
|
358
|
+
? spinActiveHoldMin
|
|
359
|
+
: spinInactiveHoldMin;
|
|
360
|
+
const spinHoldMax = spinInitiallyActive
|
|
361
|
+
? spinActiveHoldMax
|
|
362
|
+
: spinInactiveHoldMax;
|
|
363
|
+
const spinBudget = hasSpinActivity
|
|
364
|
+
? life
|
|
365
|
+
* spinCoverageFactor
|
|
366
|
+
* randomRange(
|
|
367
|
+
FORM_SPIN_ACTIVITY_TUNING.budgetJitterMin,
|
|
368
|
+
FORM_SPIN_ACTIVITY_TUNING.budgetJitterMax,
|
|
369
|
+
)
|
|
370
|
+
: 0;
|
|
371
|
+
const wobbleStrength = Math.max(0, wobble ?? 0);
|
|
372
|
+
|
|
373
|
+
const particle = engine.formPool.length > 0 ? engine.formPool.pop() : {};
|
|
374
|
+
const formShape = formShapeCount === 1
|
|
375
|
+
? formShapeSet[0]
|
|
376
|
+
: formShapeSet[clamp(Math.floor(randomRange(0, formShapeCount)), 0, formShapeCount - 1)];
|
|
377
|
+
const formGlyph = formShape === 'glyph' && glyphSet
|
|
378
|
+
? glyphSet[clamp(Math.floor(randomRange(0, glyphCount)), 0, glyphCount - 1)]
|
|
379
|
+
: null;
|
|
380
|
+
|
|
381
|
+
particle.type = 'form';
|
|
382
|
+
particle.x = x;
|
|
383
|
+
particle.y = y;
|
|
384
|
+
particle.vx = Math.cos(angle) * speed;
|
|
385
|
+
particle.vy = Math.sin(angle) * speed;
|
|
386
|
+
particle.gravity = gravity;
|
|
387
|
+
particle.life = life;
|
|
388
|
+
particle.maxLife = life;
|
|
389
|
+
particle.radius = radius;
|
|
390
|
+
particle.color = baseColor;
|
|
391
|
+
particle.drag = randomRange(FORM_PARTICLE_TUNING.dragMin, FORM_PARTICLE_TUNING.dragMax);
|
|
392
|
+
particle.simple = simple;
|
|
393
|
+
particle.formShape = formShape;
|
|
394
|
+
particle.formGlyph = formGlyph;
|
|
395
|
+
particle.formSpin = spinStrength;
|
|
396
|
+
particle.formSpinActive = spinInitiallyActive;
|
|
397
|
+
particle.formSpinBudget = spinBudget;
|
|
398
|
+
particle.formSpinToggleTimer = hasSpinActivity
|
|
399
|
+
? randomRange(spinHoldMin, spinHoldMax)
|
|
400
|
+
: 0;
|
|
401
|
+
particle.formSpinBlend = spinInitiallyActive ? randomRange(0.1, 1) : 0;
|
|
402
|
+
particle.formSpinActiveHoldMin = spinActiveHoldMin;
|
|
403
|
+
particle.formSpinActiveHoldMax = spinActiveHoldMax;
|
|
404
|
+
particle.formSpinInactiveHoldMin = spinInactiveHoldMin;
|
|
405
|
+
particle.formSpinInactiveHoldMax = spinInactiveHoldMax;
|
|
406
|
+
particle.formHighlight = Math.max(0, highlight ?? 0);
|
|
407
|
+
particle.formHighlightAngleDeg = Number.isFinite(highlightAngleDeg) ? highlightAngleDeg : 0;
|
|
408
|
+
particle.formHighlightWidth = Math.max(0, highlightWidth ?? 0);
|
|
409
|
+
particle.formSpinPhase = randomRange(0, FORM_SPIN_TUNING.phaseCycleRad);
|
|
410
|
+
particle.formSpinPhaseSpeed = spinStrength > 0
|
|
411
|
+
? spinStrength
|
|
412
|
+
* randomRange(FORM_SPIN_TUNING.phaseSpeedMin, FORM_SPIN_TUNING.phaseSpeedMax)
|
|
413
|
+
* randomRange(
|
|
414
|
+
FORM_SPIN_VARIANCE_TUNING.phaseSpeedJitterMin,
|
|
415
|
+
FORM_SPIN_VARIANCE_TUNING.phaseSpeedJitterMax,
|
|
416
|
+
)
|
|
417
|
+
: 0;
|
|
418
|
+
particle.formSpinAngle = randomRange(0, FORM_SPIN_TUNING.phaseCycleRad);
|
|
419
|
+
particle.formSpinAngleSpeed = spinStrength > 0
|
|
420
|
+
? spinStrength
|
|
421
|
+
* randomRange(FORM_SPIN_TUNING.angleSpeedMin, FORM_SPIN_TUNING.angleSpeedMax)
|
|
422
|
+
* randomRange(
|
|
423
|
+
FORM_SPIN_VARIANCE_TUNING.angleSpeedJitterMin,
|
|
424
|
+
FORM_SPIN_VARIANCE_TUNING.angleSpeedJitterMax,
|
|
425
|
+
)
|
|
426
|
+
: 0;
|
|
427
|
+
particle.formSpinMinWidthScale = spinStrength > 0
|
|
428
|
+
? randomRange(FORM_SPIN_TUNING.minWidthScaleMin, FORM_SPIN_TUNING.minWidthScaleMax)
|
|
429
|
+
: 1;
|
|
430
|
+
particle.formSpinFlipExponent = spinStrength > 0
|
|
431
|
+
? randomRange(
|
|
432
|
+
FORM_SPIN_VARIANCE_TUNING.flipExponentMin,
|
|
433
|
+
FORM_SPIN_VARIANCE_TUNING.flipExponentMax,
|
|
434
|
+
)
|
|
435
|
+
: 1;
|
|
436
|
+
particle.formSpinRateWavePhase = randomRange(0, FORM_SPIN_TUNING.phaseCycleRad);
|
|
437
|
+
particle.formSpinRateWaveSpeed = spinStrength > 0
|
|
438
|
+
? randomRange(
|
|
439
|
+
FORM_SPIN_VARIANCE_TUNING.rateWaveSpeedMin,
|
|
440
|
+
FORM_SPIN_VARIANCE_TUNING.rateWaveSpeedMax,
|
|
441
|
+
)
|
|
442
|
+
: 0;
|
|
443
|
+
particle.formSpinRateWaveAmplitude = spinStrength > 0
|
|
444
|
+
? randomRange(
|
|
445
|
+
FORM_SPIN_VARIANCE_TUNING.rateWaveAmplitudeMin,
|
|
446
|
+
FORM_SPIN_VARIANCE_TUNING.rateWaveAmplitudeMax,
|
|
447
|
+
)
|
|
448
|
+
: 0;
|
|
449
|
+
particle.formWidthScale = 1;
|
|
450
|
+
particle.formWobble = wobbleStrength;
|
|
451
|
+
particle.formWobbleLateralSpeed = wobbleStrength > 0
|
|
452
|
+
? randomRange(FORM_WOBBLE_TUNING.lateralSpeedMin, FORM_WOBBLE_TUNING.lateralSpeedMax) * wobbleStrength
|
|
453
|
+
: 0;
|
|
454
|
+
particle.formWobbleVelocity = 0;
|
|
455
|
+
particle.formWobbleTarget = particle.formWobbleLateralSpeed > 0
|
|
456
|
+
? randomRange(-particle.formWobbleLateralSpeed, particle.formWobbleLateralSpeed)
|
|
457
|
+
: 0;
|
|
458
|
+
particle.formWobbleTargetTimer = randomRange(
|
|
459
|
+
FORM_WOBBLE_TUNING.targetHoldMin,
|
|
460
|
+
FORM_WOBBLE_TUNING.targetHoldMax,
|
|
461
|
+
);
|
|
462
|
+
if (formShape === 'glyph') {
|
|
463
|
+
const glyphSpriteSize = Math.max(GLYPH_SPRITE_SIZE_MIN, radius * GLYPH_SPRITE_RADIUS_SCALE);
|
|
464
|
+
particle.glyphSpriteSize = glyphSpriteSize;
|
|
465
|
+
particle.spriteKey = `${formGlyph}|${particle.color}|${Math.round(glyphSpriteSize)}`;
|
|
466
|
+
} else {
|
|
467
|
+
particle.glyphSpriteSize = null;
|
|
468
|
+
particle.spriteKey = null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
engine.particles.push(particle);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function spawnBurstSparkParticles(engine, request, startIndex, count) {
|
|
476
|
+
const { x, y, options } = request;
|
|
477
|
+
const burstSparks = options.burstSparks ?? options;
|
|
478
|
+
const {
|
|
479
|
+
speedMin,
|
|
480
|
+
speedMax,
|
|
481
|
+
gravity,
|
|
482
|
+
lifeMin,
|
|
483
|
+
lifeMax,
|
|
484
|
+
directionDeg,
|
|
485
|
+
coneDeg,
|
|
486
|
+
palette,
|
|
487
|
+
simple,
|
|
488
|
+
length,
|
|
489
|
+
tailScale,
|
|
490
|
+
thickness,
|
|
491
|
+
shape,
|
|
492
|
+
colorMode,
|
|
493
|
+
singleColor,
|
|
494
|
+
} = burstSparks;
|
|
495
|
+
const totalCount = request.totalCount;
|
|
496
|
+
const simpleFactor = simple ? BURST_SPARK_TUNING.simpleFactor : 1;
|
|
497
|
+
const speedRange = speedMax - speedMin;
|
|
498
|
+
const hasSpeedRange = speedRange > BURST_SPARK_TUNING.depthSpeedRangeEpsilon;
|
|
499
|
+
const minProjection = BURST_SPARK_TUNING.depthMinProjection;
|
|
500
|
+
const projectionRange = 1 - minProjection;
|
|
501
|
+
|
|
502
|
+
for (let index = startIndex; index < startIndex + count; index += 1) {
|
|
503
|
+
const angle = resolveDirectedBurstAngle(
|
|
504
|
+
index,
|
|
505
|
+
totalCount,
|
|
506
|
+
directionDeg,
|
|
507
|
+
coneDeg,
|
|
508
|
+
BURST_SPARK_TUNING.angleJitter,
|
|
509
|
+
);
|
|
510
|
+
const baseSpeed = randomRange(speedMin, speedMax);
|
|
511
|
+
const speed = baseSpeed * simpleFactor;
|
|
512
|
+
const life = randomRange(lifeMin, lifeMax) * simpleFactor;
|
|
513
|
+
const baseColor = resolveBurstBaseColor(engine, palette, colorMode, singleColor);
|
|
514
|
+
const sparkLength = length * simpleFactor;
|
|
515
|
+
const sparkThickness = thickness;
|
|
516
|
+
const speedT = hasSpeedRange
|
|
517
|
+
? clamp((baseSpeed - speedMin) / speedRange, 0, 1)
|
|
518
|
+
: 1;
|
|
519
|
+
const sparkDepthFactor = hasSpeedRange
|
|
520
|
+
? minProjection + (Math.pow(speedT, BURST_SPARK_TUNING.depthGamma) * projectionRange)
|
|
521
|
+
: 1;
|
|
522
|
+
|
|
523
|
+
const particle = engine.sparkPool.length > 0 ? engine.sparkPool.pop() : {};
|
|
524
|
+
|
|
525
|
+
particle.type = 'burstSpark';
|
|
526
|
+
particle.x = x;
|
|
527
|
+
particle.y = y;
|
|
528
|
+
particle.vx = Math.cos(angle) * speed;
|
|
529
|
+
particle.vy = Math.sin(angle) * speed;
|
|
530
|
+
particle.gravity = gravity * BURST_SPARK_TUNING.gravityFactor;
|
|
531
|
+
particle.life = life;
|
|
532
|
+
particle.maxLife = life;
|
|
533
|
+
particle.color = baseColor;
|
|
534
|
+
particle.drag = randomRange(BURST_SPARK_TUNING.dragMin, BURST_SPARK_TUNING.dragMax);
|
|
535
|
+
particle.simple = simple;
|
|
536
|
+
particle.sparkLength = sparkLength;
|
|
537
|
+
particle.sparkTailScale = tailScale;
|
|
538
|
+
particle.sparkThickness = sparkThickness;
|
|
539
|
+
particle.initialSpeed = Math.max(speed, BURST_SPARK_TUNING.minInitialSpeed);
|
|
540
|
+
particle.sparkDepthFactor = sparkDepthFactor;
|
|
541
|
+
particle.sparkShape = shape;
|
|
542
|
+
|
|
543
|
+
engine.particles.push(particle);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function spawnBurstParticles(engine, request, startIndex, count) {
|
|
548
|
+
if (request.layer === 'burstSpark') {
|
|
549
|
+
spawnBurstSparkParticles(engine, request, startIndex, count);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
spawnFormParticles(engine, request, startIndex, count);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function drainBurstQueue(engine) {
|
|
557
|
+
let remainingBudget = engine.maxBurstParticlesPerFrame;
|
|
558
|
+
|
|
559
|
+
while (remainingBudget > 0 && engine.burstQueue.length > 0) {
|
|
560
|
+
const request = engine.burstQueue[0];
|
|
561
|
+
const remainingParticles = request.totalCount - request.nextIndex;
|
|
562
|
+
|
|
563
|
+
if (remainingParticles <= 0) {
|
|
564
|
+
engine.burstQueue.shift();
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const spawnCount = Math.min(remainingBudget, remainingParticles);
|
|
569
|
+
if (request.spawnBurstCenterFlash && !request.flashSpawned) {
|
|
570
|
+
spawnBurstCenterFlash(engine, request.x, request.y, request.flashOptions ?? request.options);
|
|
571
|
+
request.flashSpawned = true;
|
|
572
|
+
}
|
|
573
|
+
if (request.spawnBurstTwinkleCloud && !request.twinkleCloudSpawned) {
|
|
574
|
+
spawnBurstTwinkleCloud(engine, request.x, request.y, request.options);
|
|
575
|
+
request.twinkleCloudSpawned = true;
|
|
576
|
+
}
|
|
577
|
+
spawnBurstParticles(engine, request, request.nextIndex, spawnCount);
|
|
578
|
+
request.nextIndex += spawnCount;
|
|
579
|
+
remainingBudget -= spawnCount;
|
|
580
|
+
|
|
581
|
+
if (request.nextIndex >= request.totalCount) {
|
|
582
|
+
engine.burstQueue.shift();
|
|
583
|
+
} else {
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function launchBurst(engine, x, y, options = {}) {
|
|
590
|
+
const normalizedOptions = normalizeGroupedPublicOptions(options);
|
|
591
|
+
if (engine.enableBurstQueueThrottling) {
|
|
592
|
+
return enqueueBurst(engine, x, y, normalizedOptions);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!engine.running) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const burstOptions = buildBurstOptions(
|
|
600
|
+
normalizedOptions,
|
|
601
|
+
resolveBurstCurrentConfig(engine),
|
|
602
|
+
);
|
|
603
|
+
spawnBurstCenterFlash(engine, x, y, normalizedOptions);
|
|
604
|
+
spawnBurstTwinkleCloud(engine, x, y, normalizedOptions);
|
|
605
|
+
const requests = createBurstRequests(x, y, burstOptions);
|
|
606
|
+
requests.forEach((request) => {
|
|
607
|
+
spawnBurstParticles(engine, request, 0, request.totalCount);
|
|
608
|
+
});
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
function spawnBurstCenterFlash(engine, x, y, options = {}) {
|
|
614
|
+
const burstCenterFlash = options?.burstCenterFlash ?? {};
|
|
615
|
+
const sizeScale = resolveSizeScale(options?.sizeScale, engine.sizeScale);
|
|
616
|
+
const enabled = burstCenterFlash.enabled ?? engine.burstCenterFlash.enabled;
|
|
617
|
+
if (!enabled) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const startSize = resolveScaledOverride(
|
|
622
|
+
burstCenterFlash.startSize,
|
|
623
|
+
engine.burstCenterFlash.startSize,
|
|
624
|
+
sizeScale,
|
|
625
|
+
);
|
|
626
|
+
const endSize = resolveScaledOverride(
|
|
627
|
+
burstCenterFlash.endSize,
|
|
628
|
+
engine.burstCenterFlash.endSize,
|
|
629
|
+
sizeScale,
|
|
630
|
+
);
|
|
631
|
+
const peakAlpha = burstCenterFlash.peakAlpha ?? engine.burstCenterFlash.peakAlpha;
|
|
632
|
+
const attackScale = resolveFinite(
|
|
633
|
+
burstCenterFlash.attack,
|
|
634
|
+
engine.burstCenterFlash.attack,
|
|
635
|
+
);
|
|
636
|
+
const fade = resolveFinite(
|
|
637
|
+
burstCenterFlash.fade,
|
|
638
|
+
engine.burstCenterFlash.fade,
|
|
639
|
+
);
|
|
640
|
+
const colorMode = resolveBurstFlashColorMode(
|
|
641
|
+
burstCenterFlash.colorMode ?? engine.burstCenterFlash.colorMode,
|
|
642
|
+
);
|
|
643
|
+
const singleColor = normalizeHexColor(
|
|
644
|
+
burstCenterFlash.color,
|
|
645
|
+
engine.burstCenterFlash.color,
|
|
646
|
+
);
|
|
647
|
+
const palette = burstCenterFlash.palette ?? options?.burstForms?.palette ?? engine.burstForms.palette;
|
|
648
|
+
const color = resolveBurstBaseColor(
|
|
649
|
+
engine,
|
|
650
|
+
palette,
|
|
651
|
+
colorMode,
|
|
652
|
+
singleColor,
|
|
653
|
+
);
|
|
654
|
+
const innerStop = burstCenterFlash.innerStop ?? engine.burstCenterFlash.innerStop;
|
|
655
|
+
const life = Math.max(resolveBurstFlashAttackDuration(attackScale) + fade, BURST_FLASH_RUNTIME_LIMITS.minLife);
|
|
656
|
+
|
|
657
|
+
engine.flashes.push({
|
|
658
|
+
type: 'burstFlash',
|
|
659
|
+
x,
|
|
660
|
+
y,
|
|
661
|
+
age: 0,
|
|
662
|
+
life,
|
|
663
|
+
maxLife: life,
|
|
664
|
+
startSize,
|
|
665
|
+
endSize,
|
|
666
|
+
peakAlpha,
|
|
667
|
+
attack: attackScale,
|
|
668
|
+
fade,
|
|
669
|
+
color,
|
|
670
|
+
innerStop,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function spawnBurstTwinkleCloud(engine, x, y, options = {}) {
|
|
675
|
+
const burstTwinkleCloud = options?.burstTwinkleCloud ?? {};
|
|
676
|
+
const sizeScale = resolveSizeScale(options?.sizeScale, engine.sizeScale);
|
|
677
|
+
const envelopeTimeScale = resolveEnvelopeTimeScale(sizeScale);
|
|
678
|
+
const enabled = burstTwinkleCloud.enabled ?? engine.burstTwinkleCloud.enabled;
|
|
679
|
+
if (!enabled) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const count = resolveScaledInteger(
|
|
684
|
+
burstTwinkleCloud.count,
|
|
685
|
+
engine.burstTwinkleCloud.count,
|
|
686
|
+
TWINKLE_CLOUD_RUNTIME_LIMITS.countMin,
|
|
687
|
+
sizeScale,
|
|
688
|
+
);
|
|
689
|
+
const radius = resolveScaledOverride(
|
|
690
|
+
burstTwinkleCloud.radius,
|
|
691
|
+
engine.burstTwinkleCloud.radius,
|
|
692
|
+
sizeScale,
|
|
693
|
+
);
|
|
694
|
+
const startDelay = resolveScaledOverride(
|
|
695
|
+
burstTwinkleCloud.startDelay,
|
|
696
|
+
engine.burstTwinkleCloud.startDelay,
|
|
697
|
+
envelopeTimeScale,
|
|
698
|
+
);
|
|
699
|
+
const duration = resolveScaledOverride(
|
|
700
|
+
burstTwinkleCloud.duration,
|
|
701
|
+
engine.burstTwinkleCloud.duration,
|
|
702
|
+
envelopeTimeScale,
|
|
703
|
+
);
|
|
704
|
+
const twinkleLife = resolveScaledOverride(
|
|
705
|
+
burstTwinkleCloud.twinkleLife,
|
|
706
|
+
engine.burstTwinkleCloud.twinkleLife,
|
|
707
|
+
envelopeTimeScale,
|
|
708
|
+
);
|
|
709
|
+
const brightness = burstTwinkleCloud.brightness ?? engine.burstTwinkleCloud.brightness;
|
|
710
|
+
const particleSizeScale = resolveTwinkleParticleSizeScale(sizeScale);
|
|
711
|
+
const size = resolveScaledOverride(
|
|
712
|
+
burstTwinkleCloud.size,
|
|
713
|
+
engine.burstTwinkleCloud.size,
|
|
714
|
+
particleSizeScale,
|
|
715
|
+
);
|
|
716
|
+
const spawnSpread = resolveScaledInverseOverride(
|
|
717
|
+
burstTwinkleCloud.spawnSpread,
|
|
718
|
+
engine.burstTwinkleCloud.spawnSpread,
|
|
719
|
+
sizeScale,
|
|
720
|
+
);
|
|
721
|
+
const brightnessScale = clamp(brightness / WHITE_BRIGHTNESS_REFERENCE, 0, 1);
|
|
722
|
+
const twinkles = [];
|
|
723
|
+
let maxLife = startDelay;
|
|
724
|
+
|
|
725
|
+
for (let index = 0; index < count; index += 1) {
|
|
726
|
+
const angle = randomRange(0, Math.PI * 2);
|
|
727
|
+
const radialSample = Math.sqrt(randomRange(0, 1));
|
|
728
|
+
const distance = radius * Math.pow(radialSample, 1 / Math.max(spawnSpread, TWINKLE_CLOUD_RUNTIME_LIMITS.spawnSpreadPowerGuard));
|
|
729
|
+
const appearAt = startDelay + randomRange(0, duration);
|
|
730
|
+
const life = Math.max(
|
|
731
|
+
TWINKLE_CLOUD_RUNTIME_LIMITS.lifeClampMin,
|
|
732
|
+
twinkleLife * randomRange(TWINKLE_CLOUD_RUNTIME_LIMITS.lifeJitterMin, TWINKLE_CLOUD_RUNTIME_LIMITS.lifeJitterMax),
|
|
733
|
+
);
|
|
734
|
+
const sizeScale = randomRange(TWINKLE_CLOUD_RUNTIME_LIMITS.sizeScaleMin, TWINKLE_CLOUD_RUNTIME_LIMITS.sizeScaleMax);
|
|
735
|
+
const alphaScale = randomRange(TWINKLE_CLOUD_RUNTIME_LIMITS.alphaScaleMin, TWINKLE_CLOUD_RUNTIME_LIMITS.alphaScaleMax);
|
|
736
|
+
|
|
737
|
+
twinkles.push({
|
|
738
|
+
x: x + (Math.cos(angle) * distance),
|
|
739
|
+
y: y + (Math.sin(angle) * distance),
|
|
740
|
+
appearAt,
|
|
741
|
+
life,
|
|
742
|
+
size: Math.max(TWINKLE_CLOUD_RUNTIME_LIMITS.minRenderedSize, size * sizeScale),
|
|
743
|
+
alphaScale,
|
|
744
|
+
});
|
|
745
|
+
maxLife = Math.max(maxLife, appearAt + life);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
engine.twinkleClouds.push({
|
|
749
|
+
type: 'burstTwinkleCloud',
|
|
750
|
+
x,
|
|
751
|
+
y,
|
|
752
|
+
age: 0,
|
|
753
|
+
maxLife: Math.max(TWINKLE_CLOUD_RUNTIME_LIMITS.minCloudLife, maxLife),
|
|
754
|
+
startDelay,
|
|
755
|
+
duration,
|
|
756
|
+
twinkleLife,
|
|
757
|
+
radius,
|
|
758
|
+
brightness,
|
|
759
|
+
brightnessScale,
|
|
760
|
+
size,
|
|
761
|
+
spawnSpread,
|
|
762
|
+
twinkles,
|
|
763
|
+
});
|
|
764
|
+
}
|