@wallarm-org/design-system 0.58.1 → 0.59.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/dist/components/AnimatedBackground/AnimatedBackground.js +18 -82
- package/dist/components/AnimatedBackground/GameHud.d.ts +1 -0
- package/dist/components/AnimatedBackground/GameHud.js +2 -2
- package/dist/components/AnimatedBackground/module/celebration-renderer.d.ts +5 -0
- package/dist/components/AnimatedBackground/module/celebration-renderer.js +60 -0
- package/dist/components/AnimatedBackground/module/celebration.d.ts +102 -0
- package/dist/components/AnimatedBackground/module/celebration.js +628 -0
- package/dist/components/AnimatedBackground/module/engine-grid.d.ts +8 -1
- package/dist/components/AnimatedBackground/module/engine-grid.js +19 -5
- package/dist/components/AnimatedBackground/module/engine.d.ts +2 -0
- package/dist/components/AnimatedBackground/module/engine.js +24 -5
- package/dist/components/AnimatedBackground/module/game-logic.d.ts +8 -0
- package/dist/components/AnimatedBackground/module/game-logic.js +81 -37
- package/dist/components/AnimatedBackground/module/game-renderer.d.ts +1 -0
- package/dist/components/AnimatedBackground/module/game-renderer.js +51 -12
- package/dist/components/AnimatedBackground/module/index.d.ts +1 -0
- package/dist/components/AnimatedBackground/module/index.js +2 -1
- package/dist/components/AnimatedBackground/module/math.d.ts +4 -0
- package/dist/components/AnimatedBackground/module/math.js +10 -0
- package/dist/components/AnimatedBackground/module/sfx.d.ts +15 -0
- package/dist/components/AnimatedBackground/module/sfx.js +143 -0
- package/dist/components/AnimatedBackground/module/useGame.d.ts +22 -0
- package/dist/components/AnimatedBackground/module/useGame.js +112 -0
- package/dist/components/AnimatedBackground/module/useGameKeyboard.d.ts +1 -1
- package/dist/components/AnimatedBackground/module/useGameKeyboard.js +23 -14
- package/dist/components/Flex/Flex.d.ts +1 -1
- package/dist/components/SegmentedControl/SegmentedControlSeparator.d.ts +1 -1
- package/dist/components/Separator/Separator.d.ts +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +1 -1
- package/dist/components/Stack/Stack.d.ts +1 -1
- package/dist/metadata/components.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { SPAWN_EDGE } from "./game-logic.js";
|
|
2
|
+
import { clamp01, easeIn, easeOut } from "./math.js";
|
|
3
|
+
const CEL_DUR = [
|
|
4
|
+
0,
|
|
5
|
+
2.6,
|
|
6
|
+
3.8,
|
|
7
|
+
4.0,
|
|
8
|
+
5.2
|
|
9
|
+
];
|
|
10
|
+
const CEL_MAX_PARTICLES = 900;
|
|
11
|
+
const CEL_SWEEP_START = 1.6;
|
|
12
|
+
const CEL_SWEEP_DUR = 1.2;
|
|
13
|
+
const CEL_WAVE_DUR = 2.4;
|
|
14
|
+
const CEL_WAVE_RISE = 1.5;
|
|
15
|
+
const CEL_WAVE_FADE_AT = 1.4;
|
|
16
|
+
const CEL_WAVE_SIGMA_SQ2 = 4608;
|
|
17
|
+
const CEL_WAVE_STRENGTH = 0.95;
|
|
18
|
+
const CEL_BLAST_WAVE_DUR = 1.6;
|
|
19
|
+
const CEL_BLAST_WAVE_RISE = 0.9;
|
|
20
|
+
const CEL_BLAST_WAVE_FADE_AT = 0.8;
|
|
21
|
+
const CEL_BLAST_WAVE_SIGMA_SQ2 = 5408;
|
|
22
|
+
const CEL_BLAST_WAVE_STRENGTH = 0.9;
|
|
23
|
+
const CEL_BURST_DUR = 0.6;
|
|
24
|
+
const CEL_BURST_RADIUS = 70;
|
|
25
|
+
const CEL_BURST_SIGMA_SQ2 = 1152;
|
|
26
|
+
const CEL_BURST_STRENGTH = 0.9;
|
|
27
|
+
const CEL_LIFT_AT = 0.5;
|
|
28
|
+
const CEL_LIFT_DUR = 1.2;
|
|
29
|
+
const CEL_CANNON_AWAY_AT = 1.7;
|
|
30
|
+
const CEL_LABEL_AT = [
|
|
31
|
+
0,
|
|
32
|
+
0.7,
|
|
33
|
+
2.8,
|
|
34
|
+
2.4,
|
|
35
|
+
2.6
|
|
36
|
+
];
|
|
37
|
+
const CEL_LABEL_RISE_DUR = 0.5;
|
|
38
|
+
const CEL_RAINBOW_START = 3.0;
|
|
39
|
+
const CEL_RAINBOW_END = 4.6;
|
|
40
|
+
const CEL_RAIN_START = 2.0;
|
|
41
|
+
const CEL_RAIN_END = 4.6;
|
|
42
|
+
const CEL_RAIN_RATE = 360;
|
|
43
|
+
const CEL_GLYPHS = {
|
|
44
|
+
0: [
|
|
45
|
+
14,
|
|
46
|
+
17,
|
|
47
|
+
19,
|
|
48
|
+
21,
|
|
49
|
+
25,
|
|
50
|
+
17,
|
|
51
|
+
14
|
|
52
|
+
],
|
|
53
|
+
1: [
|
|
54
|
+
4,
|
|
55
|
+
12,
|
|
56
|
+
4,
|
|
57
|
+
4,
|
|
58
|
+
4,
|
|
59
|
+
4,
|
|
60
|
+
14
|
|
61
|
+
],
|
|
62
|
+
2: [
|
|
63
|
+
14,
|
|
64
|
+
17,
|
|
65
|
+
1,
|
|
66
|
+
6,
|
|
67
|
+
8,
|
|
68
|
+
16,
|
|
69
|
+
31
|
|
70
|
+
],
|
|
71
|
+
3: [
|
|
72
|
+
14,
|
|
73
|
+
17,
|
|
74
|
+
1,
|
|
75
|
+
6,
|
|
76
|
+
1,
|
|
77
|
+
17,
|
|
78
|
+
14
|
|
79
|
+
],
|
|
80
|
+
4: [
|
|
81
|
+
2,
|
|
82
|
+
6,
|
|
83
|
+
10,
|
|
84
|
+
18,
|
|
85
|
+
31,
|
|
86
|
+
2,
|
|
87
|
+
2
|
|
88
|
+
],
|
|
89
|
+
5: [
|
|
90
|
+
31,
|
|
91
|
+
16,
|
|
92
|
+
30,
|
|
93
|
+
1,
|
|
94
|
+
1,
|
|
95
|
+
17,
|
|
96
|
+
14
|
|
97
|
+
],
|
|
98
|
+
6: [
|
|
99
|
+
6,
|
|
100
|
+
8,
|
|
101
|
+
16,
|
|
102
|
+
30,
|
|
103
|
+
17,
|
|
104
|
+
17,
|
|
105
|
+
14
|
|
106
|
+
],
|
|
107
|
+
7: [
|
|
108
|
+
31,
|
|
109
|
+
1,
|
|
110
|
+
2,
|
|
111
|
+
4,
|
|
112
|
+
8,
|
|
113
|
+
8,
|
|
114
|
+
8
|
|
115
|
+
],
|
|
116
|
+
8: [
|
|
117
|
+
14,
|
|
118
|
+
17,
|
|
119
|
+
17,
|
|
120
|
+
14,
|
|
121
|
+
17,
|
|
122
|
+
17,
|
|
123
|
+
14
|
|
124
|
+
],
|
|
125
|
+
9: [
|
|
126
|
+
14,
|
|
127
|
+
17,
|
|
128
|
+
17,
|
|
129
|
+
15,
|
|
130
|
+
1,
|
|
131
|
+
2,
|
|
132
|
+
12
|
|
133
|
+
],
|
|
134
|
+
'%': [
|
|
135
|
+
25,
|
|
136
|
+
26,
|
|
137
|
+
4,
|
|
138
|
+
4,
|
|
139
|
+
4,
|
|
140
|
+
11,
|
|
141
|
+
19
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
const CEL_PAL = [
|
|
145
|
+
'rgb(255,60,60)',
|
|
146
|
+
'rgb(255,180,40)',
|
|
147
|
+
'rgb(60,220,80)',
|
|
148
|
+
'rgb(60,200,220)',
|
|
149
|
+
'rgb(80,100,255)',
|
|
150
|
+
'rgb(180,80,220)',
|
|
151
|
+
'rgb(255,100,180)'
|
|
152
|
+
];
|
|
153
|
+
const CEL_CAUGHT_COL = '__caught__';
|
|
154
|
+
function tierForScore(score) {
|
|
155
|
+
if (score < 20) return 0;
|
|
156
|
+
if (score < 35) return 1;
|
|
157
|
+
if (score < 90) return 2;
|
|
158
|
+
if (score < 100) return 3;
|
|
159
|
+
return 4;
|
|
160
|
+
}
|
|
161
|
+
function buildScoreCells(score, gridCols, gridSp, w, h, exclusionBox) {
|
|
162
|
+
const str = `${Math.round(score)}%`;
|
|
163
|
+
const glyphW = 5;
|
|
164
|
+
const glyphH = 7;
|
|
165
|
+
const gap = 1;
|
|
166
|
+
const totalCols = str.length * (glyphW + gap) - gap;
|
|
167
|
+
const totalRows = glyphH;
|
|
168
|
+
const blockW = totalCols * gridSp;
|
|
169
|
+
const blockH = totalRows * gridSp;
|
|
170
|
+
const totalGridRows = gridCols > 0 ? Math.ceil(h / gridSp) : 0;
|
|
171
|
+
if (0 === gridCols || totalGridRows < glyphH) return {
|
|
172
|
+
cells: new Map(),
|
|
173
|
+
colCount: totalCols
|
|
174
|
+
};
|
|
175
|
+
let startCol;
|
|
176
|
+
let startRow;
|
|
177
|
+
if (exclusionBox) {
|
|
178
|
+
const cardTop = (h - exclusionBox.height) / 2;
|
|
179
|
+
const cardLeft = (w - exclusionBox.width) / 2;
|
|
180
|
+
if (cardTop >= blockH + 8) {
|
|
181
|
+
const stripCenterY = cardTop / 2;
|
|
182
|
+
startRow = Math.max(0, Math.round((stripCenterY - blockH / 2) / gridSp));
|
|
183
|
+
startCol = Math.max(0, Math.round((w / 2 - blockW / 2) / gridSp));
|
|
184
|
+
} else {
|
|
185
|
+
if (!(cardLeft >= blockW + 32)) return {
|
|
186
|
+
cells: new Map(),
|
|
187
|
+
colCount: totalCols
|
|
188
|
+
};
|
|
189
|
+
const gutterCenterX = cardLeft / 2;
|
|
190
|
+
startCol = Math.max(0, Math.round((gutterCenterX - blockW / 2) / gridSp));
|
|
191
|
+
startRow = Math.max(0, Math.round((h / 2 - blockH / 2) / gridSp));
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
const topY = Math.max(gridSp, 0.16 * h);
|
|
195
|
+
startRow = Math.max(0, Math.round(topY / gridSp));
|
|
196
|
+
startCol = Math.max(0, Math.round((w / 2 - blockW / 2) / gridSp));
|
|
197
|
+
}
|
|
198
|
+
if (startCol + totalCols > gridCols) startCol = Math.max(0, gridCols - totalCols);
|
|
199
|
+
if (startRow + totalRows > totalGridRows) startRow = Math.max(0, totalGridRows - totalRows);
|
|
200
|
+
const cells = new Map();
|
|
201
|
+
let colOffset = 0;
|
|
202
|
+
for (const ch of str){
|
|
203
|
+
const glyph = CEL_GLYPHS[ch];
|
|
204
|
+
if (!glyph) {
|
|
205
|
+
colOffset += glyphW + gap;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
for(let row = 0; row < glyphH; row++){
|
|
209
|
+
const bits = glyph[row];
|
|
210
|
+
for(let gc = 0; gc < glyphW; gc++)if (bits & 1 << glyphW - 1 - gc) {
|
|
211
|
+
const c = startCol + colOffset + gc;
|
|
212
|
+
const r = startRow + row;
|
|
213
|
+
if (c >= 0 && c < gridCols && r >= 0) {
|
|
214
|
+
const idx = r * gridCols + c;
|
|
215
|
+
cells.set(idx, true);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
colOffset += glyphW + gap;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
cells,
|
|
223
|
+
colCount: totalCols
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function spawnConfetti(particles, count, w, spread) {
|
|
227
|
+
for(let i = 0; i < count; i++){
|
|
228
|
+
if (particles.length >= CEL_MAX_PARTICLES) break;
|
|
229
|
+
particles.push({
|
|
230
|
+
x: Math.random() * w,
|
|
231
|
+
y: -8 - Math.random() * spread,
|
|
232
|
+
vx: (Math.random() - 0.5) * 30,
|
|
233
|
+
vy: 110 + 130 * Math.random(),
|
|
234
|
+
age: 0,
|
|
235
|
+
life: 7,
|
|
236
|
+
half: 4,
|
|
237
|
+
color: CEL_PAL[Math.random() * CEL_PAL.length | 0],
|
|
238
|
+
conf: true,
|
|
239
|
+
drag: 0,
|
|
240
|
+
gravity: 0
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function spawnBurst(particles, x, y, count, caughtColor, dotColor) {
|
|
245
|
+
for(let i = 0; i < count; i++){
|
|
246
|
+
if (particles.length >= CEL_MAX_PARTICLES) break;
|
|
247
|
+
const angle = Math.random() * Math.PI * 2;
|
|
248
|
+
const speed = 80 + 120 * Math.random();
|
|
249
|
+
particles.push({
|
|
250
|
+
x,
|
|
251
|
+
y,
|
|
252
|
+
vx: Math.cos(angle) * speed,
|
|
253
|
+
vy: Math.sin(angle) * speed,
|
|
254
|
+
age: 0,
|
|
255
|
+
life: 0.9 + 0.4 * Math.random(),
|
|
256
|
+
half: 4,
|
|
257
|
+
color: Math.random() < 0.7 ? caughtColor : dotColor,
|
|
258
|
+
conf: false,
|
|
259
|
+
drag: 2.5,
|
|
260
|
+
gravity: 30
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function startCelebration(score, t, host) {
|
|
265
|
+
const tier = tierForScore(score);
|
|
266
|
+
if (0 === tier) return null;
|
|
267
|
+
const { gridCols, gridSp } = host;
|
|
268
|
+
const { cells, colCount } = buildScoreCells(score, gridCols, gridSp, host.w, host.h, host.exclusionBox);
|
|
269
|
+
const headlines = {
|
|
270
|
+
1: 'THREAT CONTAINED',
|
|
271
|
+
2: 'PERIMETER HELD',
|
|
272
|
+
3: 'ZERO BREACH',
|
|
273
|
+
4: 'AIRTIGHT \u2014 100%'
|
|
274
|
+
};
|
|
275
|
+
const sublines = {
|
|
276
|
+
1: 'well done \u2014 share it with your mates',
|
|
277
|
+
2: 'you nailed it',
|
|
278
|
+
3: 'you rock \u2014 worth a screenshot',
|
|
279
|
+
4: 'flawless \u00B7 the field salutes you'
|
|
280
|
+
};
|
|
281
|
+
const cel = {
|
|
282
|
+
t0: t,
|
|
283
|
+
tier,
|
|
284
|
+
score,
|
|
285
|
+
settled: false,
|
|
286
|
+
particles: [],
|
|
287
|
+
rockets: [],
|
|
288
|
+
pulses: [],
|
|
289
|
+
headline: headlines[tier] ?? '',
|
|
290
|
+
subline: sublines[tier] ?? '',
|
|
291
|
+
scoreCells: cells,
|
|
292
|
+
scoreColCount: colCount,
|
|
293
|
+
liftStarted: false,
|
|
294
|
+
confettiSpawned2: 0,
|
|
295
|
+
confettiVolley1: false,
|
|
296
|
+
confettiVolley2: false,
|
|
297
|
+
burstSpawned: false,
|
|
298
|
+
rainAccum: 0
|
|
299
|
+
};
|
|
300
|
+
if (2 === tier) {
|
|
301
|
+
const cannonX = host.w / 2;
|
|
302
|
+
const cannonY = host.h - 40;
|
|
303
|
+
const targets = computeRocketTargets(host);
|
|
304
|
+
for(let i = 0; i < 3 && i < targets.length; i++)cel.rockets.push({
|
|
305
|
+
t0: t + 0.4 * i,
|
|
306
|
+
sx: cannonX,
|
|
307
|
+
sy: cannonY,
|
|
308
|
+
tx: targets[i].x,
|
|
309
|
+
ty: targets[i].y,
|
|
310
|
+
dur: 0.6,
|
|
311
|
+
burst: false
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return cel;
|
|
315
|
+
}
|
|
316
|
+
function computeRocketTargets(host) {
|
|
317
|
+
const { w, h, exclusionBox } = host;
|
|
318
|
+
const targets = [];
|
|
319
|
+
const clamp = (v, lo, hi)=>Math.max(lo, Math.min(hi, v));
|
|
320
|
+
if (exclusionBox) {
|
|
321
|
+
const cardLeft = (w - exclusionBox.width) / 2;
|
|
322
|
+
const cardRight = (w + exclusionBox.width) / 2;
|
|
323
|
+
const cardTop = (h - exclusionBox.height) / 2;
|
|
324
|
+
targets.push({
|
|
325
|
+
x: clamp(cardLeft / 2, SPAWN_EDGE, w - SPAWN_EDGE),
|
|
326
|
+
y: 0.42 * h
|
|
327
|
+
});
|
|
328
|
+
targets.push({
|
|
329
|
+
x: clamp((cardRight + w) / 2, SPAWN_EDGE, w - SPAWN_EDGE),
|
|
330
|
+
y: 0.42 * h
|
|
331
|
+
});
|
|
332
|
+
targets.push({
|
|
333
|
+
x: w / 2,
|
|
334
|
+
y: Math.max(40, 0.45 * cardTop)
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
targets.push({
|
|
338
|
+
x: 0.2 * w,
|
|
339
|
+
y: 0.3 * h
|
|
340
|
+
});
|
|
341
|
+
targets.push({
|
|
342
|
+
x: 0.8 * w,
|
|
343
|
+
y: 0.3 * h
|
|
344
|
+
});
|
|
345
|
+
targets.push({
|
|
346
|
+
x: 0.5 * w,
|
|
347
|
+
y: 0.15 * h
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return targets;
|
|
351
|
+
}
|
|
352
|
+
function stepCelebration(cel, t, dt, host, caughtColor, dotColor) {
|
|
353
|
+
const elapsed = t - cel.t0;
|
|
354
|
+
const { tier, particles, rockets } = cel;
|
|
355
|
+
const { w, h } = host;
|
|
356
|
+
let kept = 0;
|
|
357
|
+
for (const p of particles){
|
|
358
|
+
p.age += dt;
|
|
359
|
+
if (!(p.age >= p.life)) {
|
|
360
|
+
if (p.conf) {
|
|
361
|
+
p.x += p.vx * dt;
|
|
362
|
+
p.y += p.vy * dt;
|
|
363
|
+
} else {
|
|
364
|
+
const dragFactor = Math.exp(-p.drag * dt);
|
|
365
|
+
p.vx *= dragFactor;
|
|
366
|
+
p.vy *= dragFactor;
|
|
367
|
+
p.vy += p.gravity * dt;
|
|
368
|
+
p.x += p.vx * dt;
|
|
369
|
+
p.y += p.vy * dt;
|
|
370
|
+
}
|
|
371
|
+
if (!(p.y > h + 12) && !(p.x < -20) && !(p.x > w + 20)) particles[kept++] = p;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
particles.length = kept;
|
|
375
|
+
if (2 === tier) {
|
|
376
|
+
const confettiTarget = cel.score >= 60 ? 56 : 32;
|
|
377
|
+
if (elapsed > 1.0 && cel.confettiSpawned2 < confettiTarget) {
|
|
378
|
+
const batch = Math.min(confettiTarget - cel.confettiSpawned2, confettiTarget);
|
|
379
|
+
spawnConfetti(particles, batch, w, 40);
|
|
380
|
+
cel.confettiSpawned2 += batch;
|
|
381
|
+
}
|
|
382
|
+
for (const rocket of rockets){
|
|
383
|
+
if (rocket.burst) continue;
|
|
384
|
+
const re = t - rocket.t0;
|
|
385
|
+
if (re >= rocket.dur) {
|
|
386
|
+
rocket.burst = true;
|
|
387
|
+
spawnBurst(particles, rocket.tx, rocket.ty, 16, caughtColor, dotColor);
|
|
388
|
+
cel.pulses.push({
|
|
389
|
+
t0: t,
|
|
390
|
+
x: rocket.tx,
|
|
391
|
+
y: rocket.ty,
|
|
392
|
+
dur: CEL_BURST_DUR
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (3 === tier) {
|
|
398
|
+
if (elapsed > 1.7 && !cel.confettiVolley1) {
|
|
399
|
+
cel.confettiVolley1 = true;
|
|
400
|
+
spawnConfetti(particles, 60, w, 60);
|
|
401
|
+
}
|
|
402
|
+
if (elapsed > 2.6 && !cel.confettiVolley2) {
|
|
403
|
+
cel.confettiVolley2 = true;
|
|
404
|
+
spawnConfetti(particles, 40, w, 40);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (4 === tier) {
|
|
408
|
+
if (elapsed > 0.8 && !cel.burstSpawned) {
|
|
409
|
+
cel.burstSpawned = true;
|
|
410
|
+
spawnConfetti(particles, 80, w, 80);
|
|
411
|
+
}
|
|
412
|
+
if (elapsed >= CEL_RAIN_START && elapsed < CEL_RAIN_END) {
|
|
413
|
+
cel.rainAccum += CEL_RAIN_RATE * dt;
|
|
414
|
+
const toSpawn = Math.floor(cel.rainAccum);
|
|
415
|
+
if (toSpawn > 0) {
|
|
416
|
+
cel.rainAccum -= toSpawn;
|
|
417
|
+
spawnConfetti(particles, toSpawn, w, 20);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (tier >= 3 && !cel.settled) {
|
|
422
|
+
const liftE = elapsed - CEL_LIFT_AT;
|
|
423
|
+
if (liftE > 0 && liftE < CEL_LIFT_DUR) {
|
|
424
|
+
const rise = easeIn(clamp01(liftE / CEL_LIFT_DUR));
|
|
425
|
+
const cannonX = host.w / 2;
|
|
426
|
+
const baseY = h - 22 - rise * (h + 120);
|
|
427
|
+
for(let i = 0; i < 3; i++){
|
|
428
|
+
if (particles.length >= CEL_MAX_PARTICLES) break;
|
|
429
|
+
particles.push({
|
|
430
|
+
x: cannonX + (Math.random() - 0.5) * 10,
|
|
431
|
+
y: baseY,
|
|
432
|
+
vx: (Math.random() - 0.5) * 20,
|
|
433
|
+
vy: 80 + 80 * Math.random(),
|
|
434
|
+
age: 0,
|
|
435
|
+
life: 0.45,
|
|
436
|
+
half: 3,
|
|
437
|
+
color: Math.random() < 0.5 ? caughtColor : dotColor,
|
|
438
|
+
conf: false,
|
|
439
|
+
drag: 0,
|
|
440
|
+
gravity: 0
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
let pKept = 0;
|
|
446
|
+
for (const pulse of cel.pulses)if (t - pulse.t0 < pulse.dur) cel.pulses[pKept++] = pulse;
|
|
447
|
+
cel.pulses.length = pKept;
|
|
448
|
+
for (const rocket of rockets){
|
|
449
|
+
if (rocket.burst) continue;
|
|
450
|
+
const re = t - rocket.t0;
|
|
451
|
+
if (re < 0 || re >= rocket.dur) continue;
|
|
452
|
+
const p = easeOut(re / rocket.dur);
|
|
453
|
+
const rx = rocket.sx + (rocket.tx - rocket.sx) * p;
|
|
454
|
+
const ry = rocket.sy + (rocket.ty - rocket.sy) * p;
|
|
455
|
+
if (particles.length < CEL_MAX_PARTICLES) particles.push({
|
|
456
|
+
x: rx,
|
|
457
|
+
y: ry,
|
|
458
|
+
vx: (Math.random() - 0.5) * 8,
|
|
459
|
+
vy: 20 + 20 * Math.random(),
|
|
460
|
+
age: 0,
|
|
461
|
+
life: 0.3,
|
|
462
|
+
half: 3,
|
|
463
|
+
color: dotColor,
|
|
464
|
+
conf: false,
|
|
465
|
+
drag: 0,
|
|
466
|
+
gravity: 0
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (tier >= 3 && elapsed > CEL_CANNON_AWAY_AT) cel.liftStarted = true;
|
|
470
|
+
const dur = CEL_DUR[tier] ?? 0;
|
|
471
|
+
if (!cel.settled && elapsed >= dur) cel.settled = true;
|
|
472
|
+
}
|
|
473
|
+
function adjustCelebrationTimeMarkers(cel, skip) {
|
|
474
|
+
cel.t0 += skip;
|
|
475
|
+
for (const rocket of cel.rockets)rocket.t0 += skip;
|
|
476
|
+
for (const pulse of cel.pulses)pulse.t0 += skip;
|
|
477
|
+
}
|
|
478
|
+
function computeCelFrameParams(cel, t, w, h) {
|
|
479
|
+
const elapsed = t - cel.t0;
|
|
480
|
+
const tier = cel.tier;
|
|
481
|
+
const diag = Math.hypot(w / 2, h);
|
|
482
|
+
let waveRadius = 0;
|
|
483
|
+
let waveStrength = 0;
|
|
484
|
+
const waveOriginX = w / 2;
|
|
485
|
+
const waveOriginY = h - 30;
|
|
486
|
+
if (1 === tier && elapsed < CEL_WAVE_DUR) {
|
|
487
|
+
waveRadius = easeOut(clamp01(elapsed / CEL_WAVE_RISE)) * diag * 0.9;
|
|
488
|
+
waveStrength = CEL_WAVE_STRENGTH * (1 - clamp01((elapsed - CEL_WAVE_FADE_AT) / 1));
|
|
489
|
+
}
|
|
490
|
+
let blastRadius = 0;
|
|
491
|
+
let blastStrength = 0;
|
|
492
|
+
const blastOriginX = w / 2;
|
|
493
|
+
const blastOriginY = h - 30;
|
|
494
|
+
if (tier >= 3 && elapsed < CEL_BLAST_WAVE_DUR) {
|
|
495
|
+
blastRadius = easeOut(clamp01(elapsed / CEL_BLAST_WAVE_RISE)) * diag;
|
|
496
|
+
blastStrength = CEL_BLAST_WAVE_STRENGTH * (1 - clamp01((elapsed - CEL_BLAST_WAVE_FADE_AT) / 0.8));
|
|
497
|
+
}
|
|
498
|
+
const sweepStart = cel.t0 + CEL_SWEEP_START;
|
|
499
|
+
const sweepEnd = sweepStart + CEL_SWEEP_DUR;
|
|
500
|
+
const sweepActive = tier >= 2 && t >= sweepStart && t <= sweepEnd + 0.5;
|
|
501
|
+
let sweepXPos = -9999;
|
|
502
|
+
if (sweepActive && cel.scoreColCount > 0) {
|
|
503
|
+
const sweepProgress = clamp01((t - sweepStart) / CEL_SWEEP_DUR);
|
|
504
|
+
sweepXPos = sweepProgress * cel.scoreColCount;
|
|
505
|
+
}
|
|
506
|
+
const rainbowActive = 4 === tier && elapsed >= CEL_RAINBOW_START && elapsed < CEL_RAINBOW_END;
|
|
507
|
+
let rainbowQ = 0;
|
|
508
|
+
if (rainbowActive) rainbowQ = 0.5 * Math.sin((elapsed - CEL_RAINBOW_START) / (CEL_RAINBOW_END - CEL_RAINBOW_START) * Math.PI);
|
|
509
|
+
return {
|
|
510
|
+
elapsed,
|
|
511
|
+
tier,
|
|
512
|
+
waveRadius,
|
|
513
|
+
waveStrength,
|
|
514
|
+
waveOriginX,
|
|
515
|
+
waveOriginY,
|
|
516
|
+
waveSigmaSq2: CEL_WAVE_SIGMA_SQ2,
|
|
517
|
+
blastRadius,
|
|
518
|
+
blastStrength,
|
|
519
|
+
blastOriginX,
|
|
520
|
+
blastOriginY,
|
|
521
|
+
blastSigmaSq2: CEL_BLAST_WAVE_SIGMA_SQ2,
|
|
522
|
+
sweepX: sweepXPos,
|
|
523
|
+
sweepActive,
|
|
524
|
+
rainbowQ,
|
|
525
|
+
rainbowActive,
|
|
526
|
+
t,
|
|
527
|
+
cel
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function celDotEffect(dotIndex, dotX, dotY, p, w, h) {
|
|
531
|
+
let boost = 0;
|
|
532
|
+
let col = null;
|
|
533
|
+
const isDigit = p.cel.scoreCells.has(dotIndex);
|
|
534
|
+
if (isDigit) {
|
|
535
|
+
const scoreColCount = p.cel.scoreColCount;
|
|
536
|
+
if (scoreColCount > 0) {
|
|
537
|
+
const sweepStart = p.cel.t0 + CEL_SWEEP_START;
|
|
538
|
+
const elapsed = p.t - sweepStart;
|
|
539
|
+
if (elapsed >= 0) {
|
|
540
|
+
const revealProgress = clamp01(elapsed / CEL_SWEEP_DUR);
|
|
541
|
+
if (revealProgress > 0) {
|
|
542
|
+
boost = 1;
|
|
543
|
+
col = CEL_CAUGHT_COL;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (p.waveStrength > 0) {
|
|
549
|
+
const dx = dotX - p.waveOriginX;
|
|
550
|
+
const dy = dotY - p.waveOriginY;
|
|
551
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
552
|
+
const dd = dist - p.waveRadius;
|
|
553
|
+
const gauss = Math.exp(-(dd * dd) / p.waveSigmaSq2);
|
|
554
|
+
const waveVal = gauss * p.waveStrength;
|
|
555
|
+
if (waveVal > boost) {
|
|
556
|
+
boost = waveVal;
|
|
557
|
+
if (!isDigit) col = CEL_CAUGHT_COL;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (p.blastStrength > 0) {
|
|
561
|
+
const dx = dotX - p.blastOriginX;
|
|
562
|
+
const dy = dotY - p.blastOriginY;
|
|
563
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
564
|
+
const dd = dist - p.blastRadius;
|
|
565
|
+
const gauss = Math.exp(-(dd * dd) / p.blastSigmaSq2);
|
|
566
|
+
const waveVal = gauss * p.blastStrength;
|
|
567
|
+
if (waveVal > boost) {
|
|
568
|
+
boost = waveVal;
|
|
569
|
+
if (!isDigit) col = CEL_CAUGHT_COL;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
for (const pulse of p.cel.pulses){
|
|
573
|
+
const pe = p.t - pulse.t0;
|
|
574
|
+
if (pe < 0 || pe >= pulse.dur) continue;
|
|
575
|
+
const dx = dotX - pulse.x;
|
|
576
|
+
const dy = dotY - pulse.y;
|
|
577
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
578
|
+
const pRadius = easeOut(pe / pulse.dur) * CEL_BURST_RADIUS;
|
|
579
|
+
const dd = dist - pRadius;
|
|
580
|
+
const gauss = Math.exp(-(dd * dd) / CEL_BURST_SIGMA_SQ2);
|
|
581
|
+
const pulseVal = gauss * CEL_BURST_STRENGTH * (1 - pe / pulse.dur);
|
|
582
|
+
if (pulseVal > boost) {
|
|
583
|
+
boost = pulseVal;
|
|
584
|
+
if (!isDigit) col = CEL_CAUGHT_COL;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (p.rainbowActive && p.rainbowQ > 0) {
|
|
588
|
+
const palIdx = Math.floor(0.03 * dotX + 0.02 * dotY + 3 * p.elapsed) % CEL_PAL.length;
|
|
589
|
+
const idx = palIdx < 0 ? palIdx + CEL_PAL.length : palIdx;
|
|
590
|
+
const rainbowBoost = p.rainbowQ;
|
|
591
|
+
if (rainbowBoost > boost || rainbowBoost > 0.05 && !isDigit) {
|
|
592
|
+
boost = Math.max(boost, rainbowBoost);
|
|
593
|
+
if (!isDigit) col = CEL_PAL[idx];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (boost < 0.02 && !isDigit) return null;
|
|
597
|
+
return {
|
|
598
|
+
celBoost: boost,
|
|
599
|
+
celCol: col
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function computeHeadlineY(h, exclusionBox) {
|
|
603
|
+
const cannonClear = 72;
|
|
604
|
+
if (exclusionBox) {
|
|
605
|
+
const cardBottom = (h + exclusionBox.height) / 2;
|
|
606
|
+
return cardBottom + (h - cannonClear - cardBottom) * 0.45;
|
|
607
|
+
}
|
|
608
|
+
return 0.75 * h;
|
|
609
|
+
}
|
|
610
|
+
function headlineAlpha(cel, t) {
|
|
611
|
+
const labelAt = CEL_LABEL_AT[cel.tier] ?? 0;
|
|
612
|
+
const elapsed = t - cel.t0 - labelAt;
|
|
613
|
+
if (elapsed < 0) return 0;
|
|
614
|
+
return clamp01(elapsed / CEL_LABEL_RISE_DUR);
|
|
615
|
+
}
|
|
616
|
+
function headlineRise(cel, t) {
|
|
617
|
+
const labelAt = CEL_LABEL_AT[cel.tier] ?? 0;
|
|
618
|
+
const elapsed = t - cel.t0 - labelAt;
|
|
619
|
+
if (elapsed < 0) return 10;
|
|
620
|
+
return 10 * (1 - easeOut(clamp01(elapsed / CEL_LABEL_RISE_DUR)));
|
|
621
|
+
}
|
|
622
|
+
function cannonLiftOffset(cel, t, h) {
|
|
623
|
+
const elapsed = t - cel.t0;
|
|
624
|
+
const liftE = elapsed - CEL_LIFT_AT;
|
|
625
|
+
if (liftE <= 0) return 0;
|
|
626
|
+
return easeIn(clamp01(liftE / CEL_LIFT_DUR)) * (h + 120);
|
|
627
|
+
}
|
|
628
|
+
export { CEL_CAUGHT_COL, CEL_DUR, CEL_LIFT_AT, CEL_LIFT_DUR, CEL_MAX_PARTICLES, CEL_PAL, adjustCelebrationTimeMarkers, buildScoreCells, cannonLiftOffset, celDotEffect, computeCelFrameParams, computeHeadlineY, headlineAlpha, headlineRise, startCelebration, stepCelebration, tierForScore };
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export interface Dot {
|
|
2
2
|
x: number;
|
|
3
3
|
y: number;
|
|
4
|
+
/** Pre-computed spatial noise phase: x * NOISE_FREQ_X + y * NOISE_FREQ_Y */
|
|
5
|
+
noiseSpatial: number;
|
|
4
6
|
}
|
|
5
|
-
export
|
|
7
|
+
export interface GridResult {
|
|
8
|
+
dots: Dot[];
|
|
9
|
+
cols: number;
|
|
10
|
+
sp: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildGrid(w: number, h: number, sp: number): GridResult;
|
|
6
13
|
export declare function sweepX(t: number, w: number, period: number): number;
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
+
import { NOISE_FREQ_X, NOISE_FREQ_Y } from "./constants.js";
|
|
1
2
|
function buildGrid(w, h, sp) {
|
|
2
3
|
const cap = 20000;
|
|
3
4
|
const safeSp = Math.max(sp, Math.sqrt(w * h / cap));
|
|
4
5
|
const a = [];
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
let cols = 0;
|
|
7
|
+
for(let y = safeSp / 2; y < h; y += safeSp){
|
|
8
|
+
let rowCols = 0;
|
|
9
|
+
for(let x = safeSp / 2; x < w; x += safeSp){
|
|
10
|
+
a.push({
|
|
11
|
+
x,
|
|
12
|
+
y,
|
|
13
|
+
noiseSpatial: x * NOISE_FREQ_X + y * NOISE_FREQ_Y
|
|
14
|
+
});
|
|
15
|
+
rowCols++;
|
|
16
|
+
}
|
|
17
|
+
if (0 === cols) cols = rowCols;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
dots: a,
|
|
21
|
+
cols,
|
|
22
|
+
sp: safeSp
|
|
23
|
+
};
|
|
10
24
|
}
|
|
11
25
|
function sweepX(t, w, period) {
|
|
12
26
|
return t % period / period * (w + 140) - 70;
|
|
@@ -29,5 +29,7 @@ export interface SweepEngine {
|
|
|
29
29
|
width: number;
|
|
30
30
|
height: number;
|
|
31
31
|
} | null): void;
|
|
32
|
+
celebrate(score: number): void;
|
|
33
|
+
setSound(on: boolean): void;
|
|
32
34
|
}
|
|
33
35
|
export declare function createSweepEngine(canvas: HTMLCanvasElement, options: EngineOptions): SweepEngine;
|