@zakkster/lite-scratch-fx 1.0.0 → 1.2.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/CHANGELOG.md +52 -0
- package/README.md +107 -5
- package/index.d.ts +118 -6
- package/index.js +102 -49
- package/llms.txt +55 -21
- package/package.json +7 -4
- package/src/PixelScan.js +74 -0
- package/src/ScratchController.js +83 -74
- package/src/ScratchRecipes.js +679 -130
- package/src/ScratchStage.js +192 -0
- package/src/ScratchRecipes2.js +0 -642
package/src/ScratchRecipes.js
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scratch Card Reveal Recipes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* All 21 one-shot reveal effects, each a standalone factory function passed to
|
|
5
|
+
* ScratchController.reveal(recipe, onDone). Grouped by family below.
|
|
6
6
|
*
|
|
7
7
|
* Recipe interface:
|
|
8
8
|
* { count, init(ctx, capacity, w, h),
|
|
9
9
|
* spawn(idx, rng, w, h, spot),
|
|
10
|
-
* tick(dt, elapsedMs, engine, ctx, sourceCanvas, w, h)
|
|
10
|
+
* tick(dt, elapsedMs, engine, ctx, sourceCanvas, w, h) -> boolean,
|
|
11
11
|
* destroy() }
|
|
12
12
|
*
|
|
13
|
-
* tick() returns
|
|
13
|
+
* tick() returns true when the effect is complete. `spot` is a shared, reused object:
|
|
14
|
+
* read spot.x / spot.y synchronously and never retain it.
|
|
14
15
|
*
|
|
15
16
|
* Uses:
|
|
16
|
-
* @zakkster/lite-lerp
|
|
17
|
-
* ./Palette.js
|
|
17
|
+
* @zakkster/lite-lerp -- easeIn, easeOut, easeInOut, clamp, lerp
|
|
18
|
+
* ./Palette.js -- resolvePalette (colors / theme option handling)
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import {easeIn, easeOut, easeInOut, clamp, lerp} from '@zakkster/lite-lerp';
|
|
21
22
|
import {resolvePalette} from './Palette.js';
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// ===========================================================================
|
|
26
|
+
// PARTICLE REVEALS
|
|
27
|
+
// ===========================================================================
|
|
28
|
+
// Spawn particles from the scratch layer's still-covered pixels and run their own physics.
|
|
29
|
+
// ===========================================================================
|
|
27
30
|
|
|
28
31
|
export function BurnRecipe({count = 150, duration = 1500, colors, theme} = {}) {
|
|
29
32
|
let pSize, pDecay, pColorIdx;
|
|
@@ -78,76 +81,6 @@ export function BurnRecipe({count = 150, duration = 1500, colors, theme} = {}) {
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
|
|
81
|
-
// ═══════════════════════════════════════════════════════════
|
|
82
|
-
// 2. SHATTER — Glass pieces with gravity + rotation
|
|
83
|
-
// ═══════════════════════════════════════════════════════════
|
|
84
|
-
|
|
85
|
-
export function ShatterRecipe({count = 30, duration = 1200, gravity = 0.5} = {}) {
|
|
86
|
-
let pRot, pRotSpd, pSz, pOpacity;
|
|
87
|
-
let img = null;
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
count,
|
|
91
|
-
init(ctx, capacity, w, h) {
|
|
92
|
-
pRot = new Float32Array(capacity);
|
|
93
|
-
pRotSpd = new Float32Array(capacity);
|
|
94
|
-
pSz = new Float32Array(capacity);
|
|
95
|
-
pOpacity = new Float32Array(capacity);
|
|
96
|
-
},
|
|
97
|
-
spawn(idx, rng, w, h, spot) {
|
|
98
|
-
pRot[idx] = rng.range(0, Math.PI * 2);
|
|
99
|
-
pRotSpd[idx] = rng.range(-0.2, 0.2);
|
|
100
|
-
pSz[idx] = Math.sqrt((w * h) / count);
|
|
101
|
-
pOpacity[idx] = 1;
|
|
102
|
-
return {x: spot.x * w, y: spot.y * h, vx: rng.range(-15, 15), vy: rng.range(-10, -2), life: 1.0};
|
|
103
|
-
},
|
|
104
|
-
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
105
|
-
// Lazy-capture source image on first frame
|
|
106
|
-
if (!img) {
|
|
107
|
-
img = new Image();
|
|
108
|
-
img.src = src.toDataURL();
|
|
109
|
-
src.style.opacity = '0';
|
|
110
|
-
}
|
|
111
|
-
if (!img.complete) return false;
|
|
112
|
-
|
|
113
|
-
const {x, y, vx, vy, life, data, max} = engine;
|
|
114
|
-
const progress = clamp(elapsed / duration, 0, 1);
|
|
115
|
-
let alive = 0;
|
|
116
|
-
|
|
117
|
-
for (let i = 0; i < max; i++) {
|
|
118
|
-
if (life[i] <= 0) continue;
|
|
119
|
-
const p = data[i];
|
|
120
|
-
x[i] += vx[i] * dt * 60;
|
|
121
|
-
y[i] += vy[i] * dt * 60;
|
|
122
|
-
vy[i] += gravity;
|
|
123
|
-
pRot[p] += pRotSpd[p];
|
|
124
|
-
pOpacity[p] = 1 - progress;
|
|
125
|
-
if (pOpacity[p] <= 0.01) {
|
|
126
|
-
life[i] = 0;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
alive++;
|
|
130
|
-
ctx.save();
|
|
131
|
-
ctx.globalAlpha = pOpacity[p];
|
|
132
|
-
ctx.translate(x[i], y[i]);
|
|
133
|
-
ctx.rotate(pRot[p]);
|
|
134
|
-
ctx.drawImage(img, -pSz[p] / 2, -pSz[p] / 2, pSz[p], pSz[p]);
|
|
135
|
-
ctx.restore();
|
|
136
|
-
}
|
|
137
|
-
return alive === 0;
|
|
138
|
-
},
|
|
139
|
-
destroy() {
|
|
140
|
-
pRot = pRotSpd = pSz = pOpacity = null;
|
|
141
|
-
img = null;
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// ═══════════════════════════════════════════════════════════
|
|
148
|
-
// 3. DISSOLVE — Particles drift upward (Thanos snap)
|
|
149
|
-
// ═══════════════════════════════════════════════════════════
|
|
150
|
-
|
|
151
84
|
export function DissolveRecipe({count = 150, duration = 1000, colors, theme} = {}) {
|
|
152
85
|
let pSize;
|
|
153
86
|
const palette = resolvePalette(colors, theme, ['#DC143C']);
|
|
@@ -189,10 +122,6 @@ export function DissolveRecipe({count = 150, duration = 1000, colors, theme} = {
|
|
|
189
122
|
}
|
|
190
123
|
|
|
191
124
|
|
|
192
|
-
// ═══════════════════════════════════════════════════════════
|
|
193
|
-
// 4. EXPLODE — Radial burst from centroid
|
|
194
|
-
// ═══════════════════════════════════════════════════════════
|
|
195
|
-
|
|
196
125
|
export function ExplodeRecipe({count = 80, duration = 750, force = 15, colors, theme} = {}) {
|
|
197
126
|
let pSize, pColorIdx;
|
|
198
127
|
const palette = resolvePalette(colors, theme, ['#DC143C', '#8B0000', '#5C0000']);
|
|
@@ -240,10 +169,6 @@ export function ExplodeRecipe({count = 80, duration = 750, force = 15, colors, t
|
|
|
240
169
|
}
|
|
241
170
|
|
|
242
171
|
|
|
243
|
-
// ═══════════════════════════════════════════════════════════
|
|
244
|
-
// 5. DRAGON BREATH — Fire cone from below, additive blend
|
|
245
|
-
// ═══════════════════════════════════════════════════════════
|
|
246
|
-
|
|
247
172
|
export function DragonBreathRecipe({count = 200, duration = 1200, colors, theme} = {}) {
|
|
248
173
|
let pSize, pDecay, pColorIdx;
|
|
249
174
|
const palette = resolvePalette(colors, theme, ['#FFF', '#FFD700', '#FF4500', '#8B0000', '#2F2F2F']);
|
|
@@ -308,10 +233,6 @@ export function DragonBreathRecipe({count = 200, duration = 1200, colors, theme}
|
|
|
308
233
|
}
|
|
309
234
|
|
|
310
235
|
|
|
311
|
-
// ═══════════════════════════════════════════════════════════
|
|
312
|
-
// 6. ICE BREATH — Crystal shards, screen blend, friction
|
|
313
|
-
// ═══════════════════════════════════════════════════════════
|
|
314
|
-
|
|
315
236
|
export function IceBreathRecipe({count = 300, duration = 1500, colors, theme} = {}) {
|
|
316
237
|
let pSize, pDecay, pRot, pRotSpd, pColorIdx;
|
|
317
238
|
const palette = resolvePalette(colors, theme, ['#FFF', '#E0FFFF', '#00FFFF', '#1E90FF', '#4682B4']);
|
|
@@ -390,9 +311,551 @@ export function IceBreathRecipe({count = 300, duration = 1500, colors, theme} =
|
|
|
390
311
|
}
|
|
391
312
|
|
|
392
313
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
314
|
+
export function GoldDustRecipe({count = 150, duration = 2000, colors, theme} = {}) {
|
|
315
|
+
let pSize, pPhase; // pPhase drives twinkle, not rotation
|
|
316
|
+
const palette = resolvePalette(colors, theme, ['#FFD700']);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
count,
|
|
320
|
+
init(ctx, capacity) {
|
|
321
|
+
pSize = new Float32Array(capacity);
|
|
322
|
+
pPhase = new Float32Array(capacity);
|
|
323
|
+
},
|
|
324
|
+
spawn(idx, rng, w, h, spot) {
|
|
325
|
+
pSize[idx] = rng.range(1, 4);
|
|
326
|
+
pPhase[idx] = rng.range(0, Math.PI * 2);
|
|
327
|
+
return {
|
|
328
|
+
x: spot.x * w,
|
|
329
|
+
y: spot.y * h,
|
|
330
|
+
vx: rng.range(-1, 1),
|
|
331
|
+
vy: rng.range(-0.5, -2.5),
|
|
332
|
+
life: 1.0,
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
336
|
+
const {x, y, vx, vy, life, data, max} = engine;
|
|
337
|
+
const ds = dt * 60;
|
|
338
|
+
let alive = 0;
|
|
339
|
+
|
|
340
|
+
src.style.opacity = 1 - easeOut(clamp(elapsed / (duration * 0.5), 0, 1));
|
|
341
|
+
|
|
342
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < max; i++) {
|
|
345
|
+
if (life[i] <= 0) continue;
|
|
346
|
+
const p = data[i];
|
|
347
|
+
x[i] += vx[i] * ds;
|
|
348
|
+
y[i] += vy[i] * ds;
|
|
349
|
+
pPhase[p] += 0.1 * ds;
|
|
350
|
+
life[i] -= 0.008 * ds;
|
|
351
|
+
if (life[i] <= 0) continue;
|
|
352
|
+
alive++;
|
|
353
|
+
|
|
354
|
+
const twinkle = (Math.sin(pPhase[p]) + 1) / 2;
|
|
355
|
+
ctx.globalAlpha = life[i] * twinkle;
|
|
356
|
+
ctx.fillStyle = palette[0];
|
|
357
|
+
ctx.beginPath();
|
|
358
|
+
ctx.arc(x[i], y[i], pSize[p], 0, Math.PI * 2);
|
|
359
|
+
ctx.fill();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ← FIX: Reset composite operation every frame
|
|
363
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
364
|
+
ctx.globalAlpha = 1;
|
|
365
|
+
return alive === 0 && elapsed >= duration;
|
|
366
|
+
},
|
|
367
|
+
destroy() {
|
|
368
|
+
pSize = pPhase = null;
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
export function ConfettiBlastRecipe({count = 80, duration = 1500, colors, theme} = {}) {
|
|
375
|
+
// ← FIX: Dedicated parallel arrays instead of repurposing size/decay
|
|
376
|
+
let pWobblePhase, pWobbleSpeed, pColorIdx;
|
|
377
|
+
const palette = resolvePalette(colors, theme, ['#ff0055', '#00ffcc', '#ffcc00', '#aa00ff', '#ff6600', '#00aaff']);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
count,
|
|
381
|
+
init(ctx, capacity) {
|
|
382
|
+
pWobblePhase = new Float32Array(capacity);
|
|
383
|
+
pWobbleSpeed = new Float32Array(capacity);
|
|
384
|
+
pColorIdx = new Uint8Array(capacity);
|
|
385
|
+
},
|
|
386
|
+
spawn(idx, rng, w, h, spot) {
|
|
387
|
+
pWobblePhase[idx] = rng.range(0, Math.PI * 2);
|
|
388
|
+
pWobbleSpeed[idx] = rng.range(0.1, 0.3);
|
|
389
|
+
pColorIdx[idx] = rng.int(0, palette.length - 1);
|
|
390
|
+
|
|
391
|
+
const angle = rng.range(0, Math.PI * 2);
|
|
392
|
+
const spd = rng.range(5, 20);
|
|
393
|
+
return {x: w / 2, y: h / 2, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd, life: 1.0};
|
|
394
|
+
},
|
|
395
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
396
|
+
const {x, y, vx, vy, life, data, max} = engine;
|
|
397
|
+
const ds = dt * 60;
|
|
398
|
+
let alive = 0;
|
|
399
|
+
|
|
400
|
+
src.style.opacity = 1 - easeOut(clamp(elapsed / (duration * 0.3), 0, 1));
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < max; i++) {
|
|
403
|
+
if (life[i] <= 0) continue;
|
|
404
|
+
const p = data[i];
|
|
405
|
+
|
|
406
|
+
vy[i] += 0.5; // Gravity
|
|
407
|
+
x[i] += vx[i] * ds;
|
|
408
|
+
y[i] += vy[i] * ds;
|
|
409
|
+
pWobblePhase[p] += pWobbleSpeed[p] * ds;
|
|
410
|
+
life[i] -= 0.015 * ds;
|
|
411
|
+
if (life[i] <= 0) continue;
|
|
412
|
+
alive++;
|
|
413
|
+
|
|
414
|
+
ctx.save();
|
|
415
|
+
ctx.globalAlpha = life[i];
|
|
416
|
+
ctx.translate(x[i], y[i]);
|
|
417
|
+
ctx.rotate(pWobblePhase[p]); // Spin
|
|
418
|
+
ctx.scale(1, Math.cos(pWobblePhase[p] * 1.7)); // 3D tumble
|
|
419
|
+
ctx.fillStyle = palette[pColorIdx[p]];
|
|
420
|
+
ctx.fillRect(-6, -4, 12, 8);
|
|
421
|
+
ctx.restore();
|
|
422
|
+
}
|
|
423
|
+
return alive === 0 && elapsed >= duration;
|
|
424
|
+
},
|
|
425
|
+
destroy() {
|
|
426
|
+
pWobblePhase = pWobbleSpeed = pColorIdx = null;
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
export function CosmicDustRecipe({count = 200, duration = 1800, colors, theme} = {}) {
|
|
433
|
+
let pAngle, pRadius, pColorIdx;
|
|
434
|
+
const palette = resolvePalette(colors, theme, ['#a78bfa', '#38bdf8', '#c084fc', '#6ee7b6']);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
count,
|
|
438
|
+
init(ctx, capacity) {
|
|
439
|
+
pAngle = new Float32Array(capacity);
|
|
440
|
+
pRadius = new Float32Array(capacity);
|
|
441
|
+
pColorIdx = new Uint8Array(capacity);
|
|
442
|
+
},
|
|
443
|
+
spawn(idx, rng, w, h, spot) {
|
|
444
|
+
const cx = w / 2, cy = h / 2;
|
|
445
|
+
const sx = spot.x * w, sy = spot.y * h;
|
|
446
|
+
pAngle[idx] = Math.atan2(sy - cy, sx - cx);
|
|
447
|
+
pRadius[idx] = Math.hypot(sx - cx, sy - cy);
|
|
448
|
+
pColorIdx[idx] = rng.int(0, palette.length - 1);
|
|
449
|
+
return {x: sx, y: sy, vx: 0, vy: 0, life: 1.0};
|
|
450
|
+
},
|
|
451
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
452
|
+
const {x, y, life, data, max} = engine;
|
|
453
|
+
const ds = dt * 60;
|
|
454
|
+
let alive = 0;
|
|
455
|
+
const cx = w / 2, cy = h / 2;
|
|
456
|
+
|
|
457
|
+
src.style.opacity = 1 - clamp(elapsed / (duration * 0.15), 0, 1);
|
|
458
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
459
|
+
|
|
460
|
+
for (let i = 0; i < max; i++) {
|
|
461
|
+
if (life[i] <= 0) continue;
|
|
462
|
+
const p = data[i];
|
|
463
|
+
|
|
464
|
+
pRadius[p] *= 0.95; // Spiral inward
|
|
465
|
+
pAngle[p] += 0.2 * ds; // Spin
|
|
466
|
+
x[i] = cx + Math.cos(pAngle[p]) * pRadius[p];
|
|
467
|
+
y[i] = cy + Math.sin(pAngle[p]) * pRadius[p];
|
|
468
|
+
life[i] -= 0.01 * ds;
|
|
469
|
+
|
|
470
|
+
if (life[i] <= 0 || pRadius[p] < 2) {
|
|
471
|
+
life[i] = 0;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
alive++;
|
|
475
|
+
|
|
476
|
+
ctx.globalAlpha = life[i];
|
|
477
|
+
ctx.fillStyle = palette[pColorIdx[p]];
|
|
478
|
+
ctx.beginPath();
|
|
479
|
+
ctx.arc(x[i], y[i], 2 + life[i] * 2, 0, Math.PI * 2);
|
|
480
|
+
ctx.fill();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ← FIX: Reset composite operation every frame
|
|
484
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
485
|
+
ctx.globalAlpha = 1;
|
|
486
|
+
return alive === 0 && elapsed >= duration;
|
|
487
|
+
},
|
|
488
|
+
destroy() {
|
|
489
|
+
pAngle = pRadius = pColorIdx = null;
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
export function MatrixDecayRecipe({count = 200, duration = 1500, colors, theme} = {}) {
|
|
496
|
+
let pCharSeed; // Pre-computed char index per particle
|
|
497
|
+
const palette = resolvePalette(colors, theme, ['#00ff41']);
|
|
498
|
+
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$+-*/=%#&_(),.;:?!\\|{}<>[]^~';
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
count,
|
|
502
|
+
init(ctx, capacity) {
|
|
503
|
+
pCharSeed = new Float32Array(capacity);
|
|
504
|
+
},
|
|
505
|
+
spawn(idx, rng, w, h, spot) {
|
|
506
|
+
pCharSeed[idx] = rng.next(); // Deterministic char offset
|
|
507
|
+
return {
|
|
508
|
+
x: spot.x * w,
|
|
509
|
+
y: spot.y * h,
|
|
510
|
+
vx: 0,
|
|
511
|
+
vy: rng.range(5, 10),
|
|
512
|
+
life: 1.0,
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
516
|
+
const {x, y, vx, vy, life, data, max} = engine;
|
|
517
|
+
const ds = dt * 60;
|
|
518
|
+
let alive = 0;
|
|
519
|
+
|
|
520
|
+
// Fade source
|
|
521
|
+
src.style.opacity = 1 - clamp(elapsed / (duration * 0.4), 0, 1);
|
|
522
|
+
|
|
523
|
+
// NOTE: No trail fillRect here. The ScratchController clears the canvas
|
|
524
|
+
// every frame before calling tick(). Crisp falling text over the fading
|
|
525
|
+
// scratch layer is cleaner for a reveal effect — no persistent black box
|
|
526
|
+
// obscuring the prize underneath.
|
|
527
|
+
|
|
528
|
+
ctx.font = '14px monospace';
|
|
529
|
+
ctx.textAlign = 'center';
|
|
530
|
+
|
|
531
|
+
for (let i = 0; i < max; i++) {
|
|
532
|
+
if (life[i] <= 0) continue;
|
|
533
|
+
const p = data[i];
|
|
534
|
+
y[i] += vy[i] * ds;
|
|
535
|
+
life[i] -= 0.015 * ds;
|
|
536
|
+
if (life[i] <= 0 || y[i] > h + 20) {
|
|
537
|
+
life[i] = 0;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
alive++;
|
|
541
|
+
|
|
542
|
+
// Cycle through chars using seed + elapsed time
|
|
543
|
+
const charIdx = ((pCharSeed[p] * CHARS.length + elapsed * 0.03) | 0) % CHARS.length;
|
|
544
|
+
ctx.globalAlpha = life[i];
|
|
545
|
+
ctx.fillStyle = palette[0];
|
|
546
|
+
ctx.fillText(CHARS[charIdx], x[i], y[i]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
ctx.globalAlpha = 1;
|
|
550
|
+
ctx.textAlign = 'start';
|
|
551
|
+
return alive === 0 && elapsed >= duration;
|
|
552
|
+
},
|
|
553
|
+
destroy() {
|
|
554
|
+
pCharSeed = null;
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
export function LiquidMeltRecipe({count = 100, duration = 1200, colors, theme} = {}) {
|
|
561
|
+
let pSize, pOriginY; // pOriginY = where the drip started (top of the streak)
|
|
562
|
+
const palette = resolvePalette(colors, theme, ['#1e293b']);
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
count,
|
|
566
|
+
init(ctx, capacity) {
|
|
567
|
+
pSize = new Float32Array(capacity);
|
|
568
|
+
pOriginY = new Float32Array(capacity);
|
|
569
|
+
},
|
|
570
|
+
spawn(idx, rng, w, h, spot) {
|
|
571
|
+
const sy = spot.y * h;
|
|
572
|
+
pSize[idx] = rng.range(2, 8);
|
|
573
|
+
pOriginY[idx] = sy;
|
|
574
|
+
return {x: spot.x * w, y: sy, vx: 0, vy: rng.range(0, 2), life: 1.0};
|
|
575
|
+
},
|
|
576
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
577
|
+
const {x, y, vy, life, data, max} = engine;
|
|
578
|
+
const ds = dt * 60;
|
|
579
|
+
let alive = 0;
|
|
580
|
+
|
|
581
|
+
src.style.opacity = 1 - clamp(elapsed / (duration * 0.2), 0, 1);
|
|
582
|
+
|
|
583
|
+
for (let i = 0; i < max; i++) {
|
|
584
|
+
if (life[i] <= 0) continue;
|
|
585
|
+
const p = data[i];
|
|
586
|
+
|
|
587
|
+
vy[i] += 0.4; // Heavy gravity
|
|
588
|
+
y[i] += vy[i] * ds;
|
|
589
|
+
life[i] -= 0.015 * ds;
|
|
590
|
+
if (life[i] <= 0 || y[i] > h + 30) {
|
|
591
|
+
life[i] = 0;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
alive++;
|
|
595
|
+
|
|
596
|
+
const sz = pSize[p];
|
|
597
|
+
const streakHeight = y[i] - pOriginY[p] + sz;
|
|
598
|
+
|
|
599
|
+
ctx.globalAlpha = life[i];
|
|
600
|
+
ctx.fillStyle = palette[0];
|
|
601
|
+
// ← FIX: Use fillRect instead of roundRect for Safari < 17.4 compat
|
|
602
|
+
ctx.fillRect(x[i] - sz / 2, pOriginY[p], sz, streakHeight);
|
|
603
|
+
// Rounded tip at bottom
|
|
604
|
+
ctx.beginPath();
|
|
605
|
+
ctx.arc(x[i], y[i], sz / 2, 0, Math.PI * 2);
|
|
606
|
+
ctx.fill();
|
|
607
|
+
}
|
|
608
|
+
ctx.globalAlpha = 1;
|
|
609
|
+
return alive === 0 && elapsed >= duration;
|
|
610
|
+
},
|
|
611
|
+
destroy() {
|
|
612
|
+
pSize = pOriginY = null;
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
// ===========================================================================
|
|
620
|
+
// IMAGE REVEALS
|
|
621
|
+
// ===========================================================================
|
|
622
|
+
// Snapshot the scratch layer via toDataURL() + Image and animate the pixels. Same-origin only.
|
|
623
|
+
// ===========================================================================
|
|
624
|
+
|
|
625
|
+
export function ShatterRecipe({count = 30, duration = 1200, gravity = 0.5} = {}) {
|
|
626
|
+
let pRot, pRotSpd, pSz, pOpacity;
|
|
627
|
+
let img = null;
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
count,
|
|
631
|
+
init(ctx, capacity, w, h) {
|
|
632
|
+
pRot = new Float32Array(capacity);
|
|
633
|
+
pRotSpd = new Float32Array(capacity);
|
|
634
|
+
pSz = new Float32Array(capacity);
|
|
635
|
+
pOpacity = new Float32Array(capacity);
|
|
636
|
+
},
|
|
637
|
+
spawn(idx, rng, w, h, spot) {
|
|
638
|
+
pRot[idx] = rng.range(0, Math.PI * 2);
|
|
639
|
+
pRotSpd[idx] = rng.range(-0.2, 0.2);
|
|
640
|
+
pSz[idx] = Math.sqrt((w * h) / count);
|
|
641
|
+
pOpacity[idx] = 1;
|
|
642
|
+
return {x: spot.x * w, y: spot.y * h, vx: rng.range(-15, 15), vy: rng.range(-10, -2), life: 1.0};
|
|
643
|
+
},
|
|
644
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
645
|
+
// Lazy-capture source image on first frame
|
|
646
|
+
if (!img) {
|
|
647
|
+
img = new Image();
|
|
648
|
+
img.src = src.toDataURL();
|
|
649
|
+
src.style.opacity = '0';
|
|
650
|
+
}
|
|
651
|
+
if (!img.complete) return false;
|
|
652
|
+
|
|
653
|
+
const {x, y, vx, vy, life, data, max} = engine;
|
|
654
|
+
const progress = clamp(elapsed / duration, 0, 1);
|
|
655
|
+
let alive = 0;
|
|
656
|
+
|
|
657
|
+
for (let i = 0; i < max; i++) {
|
|
658
|
+
if (life[i] <= 0) continue;
|
|
659
|
+
const p = data[i];
|
|
660
|
+
x[i] += vx[i] * dt * 60;
|
|
661
|
+
y[i] += vy[i] * dt * 60;
|
|
662
|
+
vy[i] += gravity;
|
|
663
|
+
pRot[p] += pRotSpd[p];
|
|
664
|
+
pOpacity[p] = 1 - progress;
|
|
665
|
+
if (pOpacity[p] <= 0.01) {
|
|
666
|
+
life[i] = 0;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
alive++;
|
|
670
|
+
ctx.save();
|
|
671
|
+
ctx.globalAlpha = pOpacity[p];
|
|
672
|
+
ctx.translate(x[i], y[i]);
|
|
673
|
+
ctx.rotate(pRot[p]);
|
|
674
|
+
ctx.drawImage(img, -pSz[p] / 2, -pSz[p] / 2, pSz[p], pSz[p]);
|
|
675
|
+
ctx.restore();
|
|
676
|
+
}
|
|
677
|
+
return alive === 0;
|
|
678
|
+
},
|
|
679
|
+
destroy() {
|
|
680
|
+
pRot = pRotSpd = pSz = pOpacity = null;
|
|
681
|
+
img = null;
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
export function PixelShatterRecipe({count = 60, duration = 1200} = {}) {
|
|
688
|
+
let pRot, pRotSpd, pSz, pOrigX, pOrigY; // ← FIX: Store ORIGINAL positions
|
|
689
|
+
let img = null, imgReady = false;
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
count,
|
|
693
|
+
init(ctx, capacity) {
|
|
694
|
+
pRot = new Float32Array(capacity);
|
|
695
|
+
pRotSpd = new Float32Array(capacity);
|
|
696
|
+
pSz = new Float32Array(capacity);
|
|
697
|
+
pOrigX = new Float32Array(capacity);
|
|
698
|
+
pOrigY = new Float32Array(capacity);
|
|
699
|
+
},
|
|
700
|
+
spawn(idx, rng, w, h, spot) {
|
|
701
|
+
const sx = spot.x * w, sy = spot.y * h;
|
|
702
|
+
pRot[idx] = rng.range(0, Math.PI);
|
|
703
|
+
pRotSpd[idx] = rng.range(-0.3, 0.3);
|
|
704
|
+
pSz[idx] = rng.range(5, 15);
|
|
705
|
+
pOrigX[idx] = sx; // ← FIX: Store spawn position for source crop
|
|
706
|
+
pOrigY[idx] = sy;
|
|
707
|
+
|
|
708
|
+
const angle = rng.range(0, Math.PI * 2);
|
|
709
|
+
const spd = rng.range(5, 15);
|
|
710
|
+
return {x: sx, y: sy, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd, life: 1.0};
|
|
711
|
+
},
|
|
712
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
713
|
+
if (!img) {
|
|
714
|
+
img = new Image();
|
|
715
|
+
img.onload = () => {
|
|
716
|
+
imgReady = true;
|
|
717
|
+
};
|
|
718
|
+
img.src = src.toDataURL();
|
|
719
|
+
src.style.opacity = '0';
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
if (!imgReady) return false;
|
|
723
|
+
|
|
724
|
+
const {x, y, vx, vy, life, data, max} = engine;
|
|
725
|
+
const ds = dt * 60;
|
|
726
|
+
const progress = clamp(elapsed / duration, 0, 1);
|
|
727
|
+
let alive = 0;
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
for (let i = 0; i < max; i++) {
|
|
731
|
+
if (life[i] <= 0) continue;
|
|
732
|
+
const p = data[i];
|
|
733
|
+
x[i] += vx[i] * ds;
|
|
734
|
+
y[i] += vy[i] * ds;
|
|
735
|
+
vx[i] *= 0.95;
|
|
736
|
+
vy[i] *= 0.95; // Drag
|
|
737
|
+
pRot[p] += pRotSpd[p];
|
|
738
|
+
life[i] -= 0.02 * ds;
|
|
739
|
+
if (life[i] <= 0) continue;
|
|
740
|
+
alive++;
|
|
741
|
+
|
|
742
|
+
const sz = pSz[p];
|
|
743
|
+
ctx.save();
|
|
744
|
+
ctx.globalAlpha = life[i];
|
|
745
|
+
ctx.translate(x[i], y[i]);
|
|
746
|
+
ctx.rotate(pRot[p]);
|
|
747
|
+
// ← FIX: Source crop uses ORIGINAL position, not current animated position
|
|
748
|
+
ctx.drawImage(img, pOrigX[p], pOrigY[p], sz, sz, -sz / 2, -sz / 2, sz, sz);
|
|
749
|
+
ctx.restore();
|
|
750
|
+
}
|
|
751
|
+
return alive === 0;
|
|
752
|
+
},
|
|
753
|
+
destroy() {
|
|
754
|
+
pRot = pRotSpd = pSz = pOrigX = pOrigY = null;
|
|
755
|
+
img = null;
|
|
756
|
+
imgReady = false;
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
export function GlitchRevealRecipe({duration = 800} = {}) {
|
|
763
|
+
let img = null;
|
|
764
|
+
let imgReady = false;
|
|
765
|
+
let sliceSeeds = null; // Pre-generated random values for deterministic slicing
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
count: 0,
|
|
769
|
+
init(ctx, capacity, w, h) {
|
|
770
|
+
// Pre-generate slice randomness so tick() is deterministic
|
|
771
|
+
sliceSeeds = new Float32Array(200); // 20 slices × 10 values per slice
|
|
772
|
+
},
|
|
773
|
+
spawn(idx, rng, w, h, spot) {
|
|
774
|
+
// Populate slice seeds during spawn phase (has rng access)
|
|
775
|
+
for (let i = 0; i < sliceSeeds.length; i++) sliceSeeds[i] = rng.next();
|
|
776
|
+
return {x: 0, y: 0, vx: 0, vy: 0, life: 0};
|
|
777
|
+
},
|
|
778
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
779
|
+
// Lazy capture with onload guard
|
|
780
|
+
if (!img) {
|
|
781
|
+
img = new Image();
|
|
782
|
+
img.onload = () => {
|
|
783
|
+
imgReady = true;
|
|
784
|
+
};
|
|
785
|
+
img.src = src.toDataURL();
|
|
786
|
+
src.style.opacity = '0';
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
if (!imgReady) return false;
|
|
790
|
+
|
|
791
|
+
const raw = clamp(elapsed / duration, 0, 1);
|
|
792
|
+
|
|
793
|
+
if (raw > 0.9) return raw >= 1;
|
|
794
|
+
|
|
795
|
+
const intensity = 1 - easeIn(raw);
|
|
796
|
+
const sliceCount = 10 + (sliceSeeds[0] * 20) | 0;
|
|
797
|
+
|
|
798
|
+
ctx.globalAlpha = 1 - raw;
|
|
799
|
+
for (let i = 0; i < sliceCount; i++) {
|
|
800
|
+
const si = (i * 4) % sliceSeeds.length;
|
|
801
|
+
const sliceY = sliceSeeds[si] * h;
|
|
802
|
+
const sliceH = sliceSeeds[si + 1] * (h / 4);
|
|
803
|
+
const offsetX = (sliceSeeds[si + 2] - 0.5) * 100 * intensity;
|
|
804
|
+
const rgbShift = 5 * intensity;
|
|
805
|
+
|
|
806
|
+
// Red channel
|
|
807
|
+
ctx.globalCompositeOperation = 'screen';
|
|
808
|
+
ctx.drawImage(img, 0, sliceY, w, sliceH, offsetX - rgbShift, sliceY, w, sliceH);
|
|
809
|
+
// Cyan channel
|
|
810
|
+
ctx.drawImage(img, 0, sliceY, w, sliceH, offsetX + rgbShift, sliceY, w, sliceH);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
814
|
+
ctx.globalAlpha = 1;
|
|
815
|
+
return raw >= 1;
|
|
816
|
+
},
|
|
817
|
+
destroy() {
|
|
818
|
+
img = null;
|
|
819
|
+
imgReady = false;
|
|
820
|
+
sliceSeeds = null;
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
// ===========================================================================
|
|
828
|
+
// BEAM / CANVAS REVEALS
|
|
829
|
+
// ===========================================================================
|
|
830
|
+
// Draw light, beams, or lines straight onto the overlay (mostly count: 0).
|
|
831
|
+
// ===========================================================================
|
|
832
|
+
|
|
833
|
+
export function ShineRecipe({duration = 500, width = 250} = {}) {
|
|
834
|
+
return {
|
|
835
|
+
count: 0,
|
|
836
|
+
init() {
|
|
837
|
+
},
|
|
838
|
+
spawn() {
|
|
839
|
+
return {x: 0, y: 0, vx: 0, vy: 0, life: 0};
|
|
840
|
+
},
|
|
841
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
842
|
+
const raw = clamp(elapsed / duration, 0, 1);
|
|
843
|
+
const t = easeInOut(raw);
|
|
844
|
+
const posX = lerp(-width, w + width, t);
|
|
845
|
+
const g = ctx.createLinearGradient(posX - width / 2, 0, posX + width / 2, 0);
|
|
846
|
+
g.addColorStop(0, 'rgba(255,255,255,0)');
|
|
847
|
+
g.addColorStop(0.5, `rgba(255,255,255,${0.8 * (1 - Math.abs(t - 0.5) * 2)})`);
|
|
848
|
+
g.addColorStop(1, 'rgba(255,255,255,0)');
|
|
849
|
+
ctx.fillStyle = g;
|
|
850
|
+
ctx.fillRect(0, 0, w, h);
|
|
851
|
+
if (t > 0.4) src.style.opacity = 1 - (t - 0.4) / 0.5;
|
|
852
|
+
return raw >= 1;
|
|
853
|
+
},
|
|
854
|
+
destroy() {
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
396
859
|
|
|
397
860
|
export function ShineWaveRecipe({duration = 600, beamWidth = 80, particleCount = 60, colors, theme} = {}) {
|
|
398
861
|
const palette = resolvePalette(colors, theme, ['#FFF', '#E0E8FF', '#B0C4FF', '#88AAFF']);
|
|
@@ -462,9 +925,66 @@ export function ShineWaveRecipe({duration = 600, beamWidth = 80, particleCount =
|
|
|
462
925
|
}
|
|
463
926
|
|
|
464
927
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
928
|
+
export function LaserScanRecipe({duration = 1000, colors, theme} = {}) {
|
|
929
|
+
let sparkSeeds = null;
|
|
930
|
+
const palette = resolvePalette(colors, theme, ['#00ffff']);
|
|
931
|
+
|
|
932
|
+
return {
|
|
933
|
+
count: 0,
|
|
934
|
+
init(ctx, capacity, w, h) {
|
|
935
|
+
sparkSeeds = new Float32Array(20); // Pre-computed spark X positions
|
|
936
|
+
},
|
|
937
|
+
spawn(idx, rng) {
|
|
938
|
+
for (let i = 0; i < sparkSeeds.length; i++) sparkSeeds[i] = rng.next();
|
|
939
|
+
return {x: 0, y: 0, vx: 0, vy: 0, life: 0};
|
|
940
|
+
},
|
|
941
|
+
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
942
|
+
const raw = clamp(elapsed / duration, 0, 1);
|
|
943
|
+
const scanY = raw * h;
|
|
944
|
+
|
|
945
|
+
// Clip source canvas to only show below the scan line
|
|
946
|
+
src.style.clipPath = `polygon(0 ${scanY}px, 100% ${scanY}px, 100% 100%, 0 100%)`;
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
if (raw < 0.98) {
|
|
950
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
951
|
+
|
|
952
|
+
// Laser line core (themed) with a hot white centre
|
|
953
|
+
ctx.fillStyle = palette[0];
|
|
954
|
+
ctx.fillRect(0, scanY - 2, w, 4);
|
|
955
|
+
ctx.fillStyle = '#ffffff';
|
|
956
|
+
ctx.fillRect(0, scanY - 1, w, 2);
|
|
957
|
+
|
|
958
|
+
// Glow
|
|
959
|
+
ctx.shadowBlur = 15;
|
|
960
|
+
ctx.shadowColor = palette[0];
|
|
961
|
+
|
|
962
|
+
// Sparks (deterministic positions)
|
|
963
|
+
for (let i = 0; i < 5; i++) {
|
|
964
|
+
ctx.beginPath();
|
|
965
|
+
ctx.arc(sparkSeeds[i * 2] * w, scanY - sparkSeeds[i * 2 + 1] * 20, sparkSeeds[i * 2] * 2, 0, Math.PI * 2);
|
|
966
|
+
ctx.fill();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ← FIX: Reset shadow + composite every frame
|
|
970
|
+
ctx.shadowBlur = 0;
|
|
971
|
+
ctx.shadowColor = 'transparent';
|
|
972
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (raw >= 1) {
|
|
976
|
+
// ← FIX: Clean up clipPath on completion
|
|
977
|
+
src.style.clipPath = '';
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
return false;
|
|
981
|
+
},
|
|
982
|
+
destroy() {
|
|
983
|
+
sparkSeeds = null;
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
468
988
|
|
|
469
989
|
export function LightningCrawlRecipe({boltCount = 5, duration = 600, colors, theme} = {}) {
|
|
470
990
|
const palette = resolvePalette(colors, theme, ['#FFF', '#E0E8FF', '#88CCFF', '#4488FF']);
|
|
@@ -575,11 +1095,8 @@ export function LightningCrawlRecipe({boltCount = 5, duration = 600, colors, the
|
|
|
575
1095
|
}
|
|
576
1096
|
|
|
577
1097
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
// ═══════════════════════════════════════════════════════════
|
|
581
|
-
|
|
582
|
-
export function ShineRecipe({duration = 500, width = 250} = {}) {
|
|
1098
|
+
export function NeonPulseRecipe({duration = 800, colors, theme} = {}) {
|
|
1099
|
+
const palette = resolvePalette(colors, theme, ['#00ffcc']);
|
|
583
1100
|
return {
|
|
584
1101
|
count: 0,
|
|
585
1102
|
init() {
|
|
@@ -589,16 +1106,36 @@ export function ShineRecipe({duration = 500, width = 250} = {}) {
|
|
|
589
1106
|
},
|
|
590
1107
|
tick(dt, elapsed, engine, ctx, src, w, h) {
|
|
591
1108
|
const raw = clamp(elapsed / duration, 0, 1);
|
|
592
|
-
const t =
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1109
|
+
const t = easeOut(raw);
|
|
1110
|
+
const cx = w / 2, cy = h / 2;
|
|
1111
|
+
|
|
1112
|
+
src.style.opacity = 1 - t;
|
|
1113
|
+
// ← FIX: Clamp scale to prevent overflow; clean up on completion
|
|
1114
|
+
src.style.transform = `scale(${1 + t * 0.3})`;
|
|
1115
|
+
src.style.transformOrigin = 'center';
|
|
1116
|
+
|
|
1117
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
1118
|
+
|
|
1119
|
+
for (let i = 0; i < 3; i++) {
|
|
1120
|
+
const r = t * Math.max(w, h) * 0.8 * (1 - i * 0.2);
|
|
1121
|
+
if (r < 1) continue;
|
|
1122
|
+
ctx.beginPath();
|
|
1123
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
1124
|
+
ctx.globalAlpha = (1 - t) * (0.8 - i * 0.2);
|
|
1125
|
+
ctx.strokeStyle = palette[0];
|
|
1126
|
+
ctx.lineWidth = 10 - i * 3;
|
|
1127
|
+
ctx.stroke();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// ← FIX: Reset composite operation
|
|
1131
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1132
|
+
ctx.globalAlpha = 1;
|
|
1133
|
+
|
|
1134
|
+
if (raw >= 1) {
|
|
1135
|
+
src.style.transform = '';
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
602
1139
|
},
|
|
603
1140
|
destroy() {
|
|
604
1141
|
},
|
|
@@ -606,9 +1143,12 @@ export function ShineRecipe({duration = 500, width = 250} = {}) {
|
|
|
606
1143
|
}
|
|
607
1144
|
|
|
608
1145
|
|
|
609
|
-
|
|
610
|
-
//
|
|
611
|
-
//
|
|
1146
|
+
|
|
1147
|
+
// ===========================================================================
|
|
1148
|
+
// CSS REVEALS
|
|
1149
|
+
// ===========================================================================
|
|
1150
|
+
// Drive the scratch layer's own transform/opacity; little or no particle work.
|
|
1151
|
+
// ===========================================================================
|
|
612
1152
|
|
|
613
1153
|
export function PeelRecipe({duration = 1000} = {}) {
|
|
614
1154
|
return {
|
|
@@ -636,10 +1176,6 @@ export function PeelRecipe({duration = 1000} = {}) {
|
|
|
636
1176
|
}
|
|
637
1177
|
|
|
638
1178
|
|
|
639
|
-
// ═══════════════════════════════════════════════════════════
|
|
640
|
-
// 11. FADE — Simple opacity tween
|
|
641
|
-
// ═══════════════════════════════════════════════════════════
|
|
642
|
-
|
|
643
1179
|
export function FadeRecipe({duration = 400} = {}) {
|
|
644
1180
|
return {
|
|
645
1181
|
count: 0,
|
|
@@ -659,10 +1195,6 @@ export function FadeRecipe({duration = 400} = {}) {
|
|
|
659
1195
|
}
|
|
660
1196
|
|
|
661
1197
|
|
|
662
|
-
// ═══════════════════════════════════════════════════════════
|
|
663
|
-
// 12. IMPLOSION — Spin + shrink + speed lines (zero particles)
|
|
664
|
-
// ═══════════════════════════════════════════════════════════
|
|
665
|
-
|
|
666
1198
|
export function ImplosionRecipe({duration = 800} = {}) {
|
|
667
1199
|
return {
|
|
668
1200
|
count: 0,
|
|
@@ -705,15 +1237,32 @@ export function ImplosionRecipe({duration = 800} = {}) {
|
|
|
705
1237
|
}
|
|
706
1238
|
|
|
707
1239
|
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
1240
|
+
// ===========================================================================
|
|
1241
|
+
// RECIPE MAP
|
|
1242
|
+
// ===========================================================================
|
|
711
1243
|
|
|
712
1244
|
export const ScratchRecipes = {
|
|
713
|
-
BurnRecipe,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1245
|
+
BurnRecipe,
|
|
1246
|
+
DissolveRecipe,
|
|
1247
|
+
ExplodeRecipe,
|
|
1248
|
+
DragonBreathRecipe,
|
|
1249
|
+
IceBreathRecipe,
|
|
1250
|
+
GoldDustRecipe,
|
|
1251
|
+
ConfettiBlastRecipe,
|
|
1252
|
+
CosmicDustRecipe,
|
|
1253
|
+
MatrixDecayRecipe,
|
|
1254
|
+
LiquidMeltRecipe,
|
|
1255
|
+
ShatterRecipe,
|
|
1256
|
+
PixelShatterRecipe,
|
|
1257
|
+
GlitchRevealRecipe,
|
|
1258
|
+
ShineRecipe,
|
|
1259
|
+
ShineWaveRecipe,
|
|
1260
|
+
LaserScanRecipe,
|
|
1261
|
+
LightningCrawlRecipe,
|
|
1262
|
+
NeonPulseRecipe,
|
|
1263
|
+
PeelRecipe,
|
|
1264
|
+
FadeRecipe,
|
|
1265
|
+
ImplosionRecipe,
|
|
717
1266
|
};
|
|
718
1267
|
|
|
719
1268
|
export default ScratchRecipes;
|