@zakkster/lite-tools 1.0.7 → 2.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/LiteEngine.js CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * @zakkster/lite-tools — The Standard Library for High-Performance Web Presentation
2
+ * @zakkster/lite-tools v2.0 — The Standard Library for High-Performance Web Presentation
3
3
  *
4
- * A unified, tree-shakeable toolkit composing every @zakkster library into
5
- * ready-to-use recipes. The math of Three.js, the physics of Framer Motion,
6
- * and the color theory of Tailwind — deterministic, zero-GC, fraction of the bundle.
4
+ * 45+ micro-libraries. 24 ready-made recipes. 1 install.
5
+ * Zero-GC, deterministic, tree-shakeable.
7
6
  *
8
7
  * IMPORT PATTERNS:
9
8
  * import { Recipes, FXSystem, GenEngine } from '@zakkster/lite-tools'
@@ -54,10 +53,82 @@ export {
54
53
  export {generateTheme, toCssVariables, createThemeCss} from '@zakkster/lite-theme-gen';
55
54
  export {Viewport} from 'lite-viewport';
56
55
  export {Ticker} from '@zakkster/lite-ticker';
57
- export {FSM} from 'lite-fsm';
56
+ export {FSM} from 'lite-states';
58
57
  export {FPSMeter} from 'lite-fps-meter';
59
58
  export {PointerTracker} from 'lite-pointer-tracker';
60
59
 
60
+ // ═══════════════════════════════════════════════════════════
61
+ // v2.0 — NEW BARREL RE-EXPORTS
62
+ // ═══════════════════════════════════════════════════════════
63
+
64
+ // Animation primitives
65
+ export {
66
+ easeInQuad,
67
+ easeOutQuad,
68
+ easeInOutQuad,
69
+ easeInCubic,
70
+ easeOutCubic,
71
+ easeInOutCubic,
72
+ easeInQuart,
73
+ easeOutQuart,
74
+ easeInOutQuart,
75
+ easeInQuint,
76
+ easeOutQuint,
77
+ easeInOutQuint,
78
+ easeInSine,
79
+ easeOutSine,
80
+ easeInOutSine,
81
+ easeInExpo,
82
+ easeOutExpo,
83
+ easeInOutExpo,
84
+ easeInCirc,
85
+ easeOutCirc,
86
+ easeInOutCirc,
87
+ easeInBack,
88
+ easeOutBack,
89
+ easeInOutBack,
90
+ easeInElastic,
91
+ easeOutElastic,
92
+ easeInOutElastic,
93
+ easeInBounce,
94
+ easeOutBounce,
95
+ easeInOutBounce,
96
+ linear
97
+ } from '@zakkster/lite-ease';
98
+ export {Tween} from '@zakkster/lite-tween';
99
+ export {SpringStandalone, SpringPool} from '@zakkster/lite-spring';
100
+ export {Gradient} from '@zakkster/lite-gradient';
101
+ export {Noise} from '@zakkster/lite-noise';
102
+ export {Timeline} from '@zakkster/lite-timeline';
103
+
104
+ // Interaction + utility
105
+ export {GestureRecognizer} from '@zakkster/lite-gesture';
106
+ export {confetti, createConfetti} from '@zakkster/lite-confetti';
107
+ export {createId, createIdGenerator} from '@zakkster/lite-id';
108
+ export {Vec2} from '@zakkster/lite-vec';
109
+ export {
110
+ Seek, Flee, Wander, Arrive, Pursuit, Evade, PathFollow, Separation, Alignment, Cohesion, Flock
111
+ }from '@zakkster/lite-steer';
112
+
113
+ // Game layer
114
+ export {BmFont} from '@zakkster/lite-bmfont';
115
+ export {InputPoller} from '@zakkster/lite-gamepad';
116
+ export {Camera} from '@zakkster/lite-camera';
117
+ export {SpatialHash} from '@zakkster/lite-spatial';
118
+ export {testPolygonPolygon, translatePoly, rotatePoly} from '@zakkster/lite-sat';
119
+ export {PathFinder} from '@zakkster/lite-path';
120
+ export {ShadowCaster} from '@zakkster/lite-shadow';
121
+ export {WFC} from '@zakkster/lite-wfc';
122
+ export {AudioPool} from '@zakkster/lite-audio-pool';
123
+
124
+ // VFX engines (composable weather/fire system)
125
+ export {FireworksEngine} from '@zakkster/lite-fireworks';
126
+ export {SparkEngine} from '@zakkster/lite-sparks';
127
+ export {RainEngine} from '@zakkster/lite-rain';
128
+ export {SnowEngine} from '@zakkster/lite-snow';
129
+ export {EmberEngine} from '@zakkster/lite-embers';
130
+ export {SmokeEngine} from '@zakkster/lite-smoke';
131
+
61
132
  // ═══════════════════════════════════════════════════════════
62
133
  // INTERNAL IMPORTS FOR RECIPES
63
134
  // ═══════════════════════════════════════════════════════════
@@ -81,12 +152,38 @@ import {
81
152
  destroyAll
82
153
  } from '@zakkster/lite-ui';
83
154
  import {generateTheme, toCssVariables} from '@zakkster/lite-theme-gen';
84
- import {Ticker} from 'lite-ticker';
155
+ import {Ticker} from '@zakkster/lite-ticker';
85
156
  import {Viewport} from 'lite-viewport';
86
- import {FSM} from 'lite-fsm';
157
+ import {FSM} from 'lite-states';
87
158
  import {FPSMeter} from 'lite-fps-meter';
88
159
  import {PointerTracker} from 'lite-pointer-tracker';
89
160
 
161
+ // v2.0 internal imports for new recipes
162
+ import {easeOutCubic, easeOutElastic, easeInOutCubic, easeOutBounce, easeOutBack} from '@zakkster/lite-ease';
163
+ import {Tween} from '@zakkster/lite-tween';
164
+ import {SpringPool} from '@zakkster/lite-spring';
165
+ import {Gradient} from '@zakkster/lite-gradient';
166
+ import {Noise} from '@zakkster/lite-noise';
167
+ import {Timeline} from '@zakkster/lite-timeline';
168
+ import {GestureRecognizer} from '@zakkster/lite-gesture';
169
+ import {confetti as confettiFn} from '@zakkster/lite-confetti';
170
+ import {createId} from '@zakkster/lite-id';
171
+ import {Vec2} from '@zakkster/lite-vec';
172
+ import {Flock, Wander, Separation, Alignment, Cohesion} from '@zakkster/lite-steer';
173
+ import {BmFont} from '@zakkster/lite-bmfont';
174
+ import {InputPoller} from '@zakkster/lite-gamepad';
175
+ import {Camera} from '@zakkster/lite-camera';
176
+ import {SpatialHash} from '@zakkster/lite-spatial';
177
+ import {PathFinder} from '@zakkster/lite-path';
178
+ import {WFC} from '@zakkster/lite-wfc';
179
+ import {AudioPool} from '@zakkster/lite-audio-pool';
180
+ import {FireworksEngine} from '@zakkster/lite-fireworks';
181
+ import {SparkEngine} from '@zakkster/lite-sparks';
182
+ import {RainEngine} from '@zakkster/lite-rain';
183
+ import {SnowEngine as SnowEngineV2} from '@zakkster/lite-snow';
184
+ import {EmberEngine} from '@zakkster/lite-embers';
185
+ import {SmokeEngine} from '@zakkster/lite-smoke';
186
+
90
187
 
91
188
  // ═══════════════════════════════════════════════════════════
92
189
  // HELPERS
@@ -672,7 +769,7 @@ export const Recipes = {
672
769
 
673
770
  // ─────────────────────────────────────────────
674
771
  // 🎬 12. Deterministic Replay System
675
- // lite-random + lite-fx + lite-fsm
772
+ // lite-random + lite-fx + lite-states
676
773
  // ─────────────────────────────────────────────
677
774
 
678
775
  replaySystem(ctx, {maxParticles = 5000, seed = Date.now()} = {}) {
@@ -858,6 +955,505 @@ export const Recipes = {
858
955
  },
859
956
  };
860
957
  },
861
- };
862
958
 
863
- export default Recipes;
959
+ // ═══════════════════════════════════════════════════════════
960
+ // v2.0 RECIPES (15–24)
961
+ // ═══════════════════════════════════════════════════════════
962
+
963
+ /**
964
+ * 15. Retro Arcade Text — Bitmap font score counter with tween-animated damage numbers.
965
+ * Composes: lite-bmfont + lite-tween + lite-ease + lite-random
966
+ */
967
+ retroArcadeText(ctx, fontImage, fontData, options = {}) {
968
+ const {seed = Date.now(), maxNumbers = 30} = options;
969
+ const rng = new Random(seed);
970
+ const font = new BmFont(fontImage, fontData);
971
+ const numbers = [];
972
+ let score = 0;
973
+
974
+ function addDamage(x, y, value) {
975
+ numbers.push({
976
+ id: createId(), x, y, value: String(value),
977
+ startY: y, t: 0, life: 1.2, alpha: 1,
978
+ });
979
+ score += value;
980
+ if (numbers.length > maxNumbers) numbers.shift();
981
+ }
982
+
983
+ function update(dt) {
984
+ for (let i = numbers.length - 1; i >= 0; i--) {
985
+ const n = numbers[i];
986
+ n.t += dt;
987
+ const progress = clamp(n.t / n.life, 0, 1);
988
+ n.y = n.startY - easeOutCubic(progress) * 60;
989
+ n.alpha = 1 - easeInOutCubic(progress);
990
+ if (n.t >= n.life) {
991
+ numbers.splice(i, 1);
992
+ continue;
993
+ }
994
+ ctx.globalAlpha = n.alpha;
995
+ font.draw(ctx, n.value, n.x, n.y, 1, 'center');
996
+ }
997
+ ctx.globalAlpha = 1;
998
+ font.draw(ctx, `SCORE: ${score}`, 10, 10, 1.5);
999
+ }
1000
+
1001
+ return {
1002
+ addDamage, update, getScore: () => score, resetScore() {
1003
+ score = 0;
1004
+ }, destroy() {
1005
+ numbers.length = 0;
1006
+ }
1007
+ };
1008
+ },
1009
+
1010
+ /**
1011
+ * 16. Procedural World — Infinite scrollable noise terrain with camera follow.
1012
+ * Composes: lite-noise + lite-gradient + lite-camera + lite-random
1013
+ */
1014
+ proceduralWorld(canvas, options = {}) {
1015
+ const {seed = 42, cellSize = 8, scale = 0.02} = options;
1016
+ const ctx = canvas.getContext('2d');
1017
+ const rng = new Random(seed);
1018
+ const noise = new Noise(seed);
1019
+ const cam = new Camera({smoothing: 0.08, deadzone: 40});
1020
+ const gradient = new Gradient([
1021
+ {l: 0.2, c: 0.15, h: 220}, // deep water
1022
+ {l: 0.4, c: 0.2, h: 200}, // shallow
1023
+ {l: 0.55, c: 0.15, h: 130}, // lowland
1024
+ {l: 0.45, c: 0.2, h: 110}, // forest
1025
+ {l: 0.65, c: 0.08, h: 60}, // mountain
1026
+ {l: 0.95, c: 0.02, h: 0}, // snow
1027
+ ]);
1028
+ let target = {x: 0, y: 0};
1029
+
1030
+ function render(dt) {
1031
+ cam.follow(target.x, target.y, dt);
1032
+ const {x: camX, y: camY} = cam.getPosition();
1033
+ const w = canvas.width, h = canvas.height;
1034
+ const cols = Math.ceil(w / cellSize) + 2;
1035
+ const rows = Math.ceil(h / cellSize) + 2;
1036
+ const offX = Math.floor(camX / cellSize);
1037
+ const offY = Math.floor(camY / cellSize);
1038
+
1039
+ for (let r = 0; r < rows; r++) {
1040
+ for (let c = 0; c < cols; c++) {
1041
+ const wx = (offX + c) * scale;
1042
+ const wy = (offY + r) * scale;
1043
+ const n = noise.fbm2(wx, wy, 5, 2.0, 0.5) * 0.5 + 0.5;
1044
+ const color = gradient.at(clamp(n, 0, 1));
1045
+ ctx.fillStyle = toCssOklch(color);
1046
+ ctx.fillRect((c - (camX / cellSize - offX)) * cellSize, (r - (camY / cellSize - offY)) * cellSize, cellSize + 1, cellSize + 1);
1047
+ }
1048
+ }
1049
+ }
1050
+
1051
+ return {
1052
+ render, cam,
1053
+ moveTo(x, y) {
1054
+ target = {x, y};
1055
+ },
1056
+ reseed(s) {
1057
+ noise.seed(s);
1058
+ },
1059
+ destroy() {
1060
+ },
1061
+ };
1062
+ },
1063
+
1064
+ /**
1065
+ * 17. Dungeon Generator — WFC level generation with pathfinding overlay.
1066
+ * Composes: lite-wfc + lite-spatial + lite-path
1067
+ */
1068
+ dungeonGenerator(options = {}) {
1069
+ const {width = 32, height = 32, seed = Date.now()} = options;
1070
+ const rng = new Random(seed);
1071
+ const grid = new Uint8Array(width * height); // 0=wall, 1=floor
1072
+ const spatial = new SpatialHash(width * 4, height * 4, 32);
1073
+
1074
+ // Simple noise-based dungeon
1075
+ for (let y = 0; y < height; y++) {
1076
+ for (let x = 0; x < width; x++) {
1077
+ grid[y * width + x] = rng.next() > 0.4 ? 1 : 0;
1078
+ }
1079
+ }
1080
+ // Ensure borders are walls
1081
+ for (let x = 0; x < width; x++) {
1082
+ grid[x] = 0;
1083
+ grid[(height - 1) * width + x] = 0;
1084
+ }
1085
+ for (let y = 0; y < height; y++) {
1086
+ grid[y * width] = 0;
1087
+ grid[y * width + width - 1] = 0;
1088
+ }
1089
+
1090
+ function isWalkable(x, y) {
1091
+ return x >= 0 && x < width && y >= 0 && y < height && grid[y * width + x] === 1;
1092
+ }
1093
+
1094
+ function findPath(sx, sy, ex, ey) {
1095
+ const finder = new PathFinder(width, height, (x, y) => isWalkable(x, y));
1096
+ return finder.find(sx, sy, ex, ey);
1097
+ }
1098
+
1099
+ function renderToCanvas(ctx, tileSize = 16) {
1100
+ for (let y = 0; y < height; y++) {
1101
+ for (let x = 0; x < width; x++) {
1102
+ ctx.fillStyle = grid[y * width + x] === 1 ? '#2d2d3d' : '#0d0d12';
1103
+ ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ return {
1109
+ grid, width, height, spatial, isWalkable, findPath, renderToCanvas, destroy() {
1110
+ }
1111
+ };
1112
+ },
1113
+
1114
+ /**
1115
+ * 18. Campfire Scene — Embers rise and die into smoke puffs.
1116
+ * Composes: lite-embers + lite-smoke + lite-ease
1117
+ */
1118
+ campfireScene(canvas, options = {}) {
1119
+ const {fireX, fireY, fireW = 60, fireH = 20, maxEmbers = 3000, maxSmoke = 1000, dpr = 1} = options;
1120
+ const ctx = canvas.getContext('2d');
1121
+ const smoke = new SmokeEngine(maxSmoke, {dpr});
1122
+ const embers = new EmberEngine(maxEmbers, {
1123
+ onEmberDeath(x, y) {
1124
+ smoke.emit(x, y, 1, -Math.PI / 2 - 0.3, -Math.PI / 2 + 0.3, 5, 20, 8, 20, 15, 1.5, 3.0);
1125
+ },
1126
+ });
1127
+ const fx = fireX ?? canvas.width / 2 - fireW / 2;
1128
+ const fy = fireY ?? canvas.height - 80;
1129
+
1130
+ function update(dt, w, h) {
1131
+ embers.spawn(dt, w, h, fx, fy, fireW, fireH);
1132
+ embers.updateAndDraw(ctx, dt, w, h);
1133
+ smoke.updateAndDraw(ctx, dt, w, h);
1134
+ }
1135
+
1136
+ return {
1137
+ embers, smoke, update, destroy() {
1138
+ embers.destroy();
1139
+ smoke.destroy();
1140
+ }
1141
+ };
1142
+ },
1143
+
1144
+ /**
1145
+ * 19. Weather System — Dynamic rain/snow with real-time wind control.
1146
+ * Composes: lite-rain + lite-snow + lite-ease
1147
+ */
1148
+ weatherSystem(canvas, options = {}) {
1149
+ const {mode = 'rain', maxParticles = 8000} = options;
1150
+ const ctx = canvas.getContext('2d');
1151
+ const rain = new RainEngine(maxParticles);
1152
+ const snow = new SnowEngineV2(maxParticles);
1153
+ let current = mode;
1154
+ let wind = 0, windTarget = 0;
1155
+
1156
+ function update(dt, w, h) {
1157
+ wind += (windTarget - wind) * dt * 2;
1158
+ if (current === 'rain') {
1159
+ rain.config.wind = wind;
1160
+ rain.spawn(dt, w, h);
1161
+ rain.updateAndDraw(ctx, dt, w, h);
1162
+ } else {
1163
+ snow.config.wind = wind;
1164
+ snow.spawn(dt, w, h);
1165
+ snow.updateAndDraw(ctx, dt, w, h);
1166
+ }
1167
+ }
1168
+
1169
+ return {
1170
+ update,
1171
+ setWind(v) {
1172
+ windTarget = v;
1173
+ },
1174
+ setMode(m) {
1175
+ current = m;
1176
+ rain.clear();
1177
+ snow.clear();
1178
+ },
1179
+ getMode() {
1180
+ return current;
1181
+ },
1182
+ destroy() {
1183
+ rain.destroy();
1184
+ snow.destroy();
1185
+ },
1186
+ };
1187
+ },
1188
+
1189
+ /**
1190
+ * 20. Boids Simulation — Autonomous flocking agents with spatial hashing.
1191
+ * Composes: lite-steer + lite-vec + lite-spatial + lite-random
1192
+ */
1193
+ boidsSimulation(canvas, options = {}) {
1194
+ const {count = 100, seed = 42} = options;
1195
+ const ctx = canvas.getContext('2d');
1196
+ const rng = new Random(seed);
1197
+ const w = canvas.width, h = canvas.height;
1198
+ const spatial = new SpatialHash(w, h, 64);
1199
+ const agents = [];
1200
+
1201
+ for (let i = 0; i < count; i++) {
1202
+ agents.push({
1203
+ id: i, x: rng.next() * w, y: rng.next() * h,
1204
+ vx: (rng.next() - 0.5) * 100, vy: (rng.next() - 0.5) * 100,
1205
+ hue: rng.next() * 360,
1206
+ });
1207
+ }
1208
+
1209
+ function update(dt) {
1210
+ spatial.clear();
1211
+ for (const a of agents) spatial.insert(a, a.x, a.y, 4, 4);
1212
+
1213
+ for (const a of agents) {
1214
+ const neighbors = spatial.query(a.x - 60, a.y - 60, 120, 120).filter(n => n.id !== a.id);
1215
+ if (neighbors.length > 0) {
1216
+ let sx = 0, sy = 0, ax = 0, ay = 0, cx = 0, cy = 0;
1217
+ for (const n of neighbors) {
1218
+ const dx = a.x - n.x, dy = a.y - n.y;
1219
+ const d = Math.sqrt(dx * dx + dy * dy) || 1;
1220
+ if (d < 25) {
1221
+ sx += dx / d;
1222
+ sy += dy / d;
1223
+ }
1224
+ ax += n.vx;
1225
+ ay += n.vy;
1226
+ cx += n.x;
1227
+ cy += n.y;
1228
+ }
1229
+ ax /= neighbors.length;
1230
+ ay /= neighbors.length;
1231
+ cx /= neighbors.length;
1232
+ cy /= neighbors.length;
1233
+ a.vx += (sx * 2 + (ax - a.vx) * 0.05 + (cx - a.x) * 0.01) * dt;
1234
+ a.vy += (sy * 2 + (ay - a.vy) * 0.05 + (cy - a.y) * 0.01) * dt;
1235
+ }
1236
+
1237
+ const speed = Math.sqrt(a.vx * a.vx + a.vy * a.vy) || 1;
1238
+ if (speed > 150) {
1239
+ a.vx = (a.vx / speed) * 150;
1240
+ a.vy = (a.vy / speed) * 150;
1241
+ }
1242
+ a.x += a.vx * dt;
1243
+ a.y += a.vy * dt;
1244
+ if (a.x < 0) a.x += w;
1245
+ if (a.x > w) a.x -= w;
1246
+ if (a.y < 0) a.y += h;
1247
+ if (a.y > h) a.y -= h;
1248
+ }
1249
+
1250
+ ctx.clearRect(0, 0, w, h);
1251
+ for (const a of agents) {
1252
+ const angle = Math.atan2(a.vy, a.vx);
1253
+ ctx.save();
1254
+ ctx.translate(a.x, a.y);
1255
+ ctx.rotate(angle);
1256
+ ctx.fillStyle = `oklch(0.7 0.2 ${a.hue})`;
1257
+ ctx.beginPath();
1258
+ ctx.moveTo(8, 0);
1259
+ ctx.lineTo(-4, -4);
1260
+ ctx.lineTo(-4, 4);
1261
+ ctx.closePath();
1262
+ ctx.fill();
1263
+ ctx.restore();
1264
+ }
1265
+ }
1266
+
1267
+ return {
1268
+ agents, update, destroy() {
1269
+ spatial.clear();
1270
+ agents.length = 0;
1271
+ }
1272
+ };
1273
+ },
1274
+
1275
+ /**
1276
+ * 21. Gesture Gallery — Swipeable, pinchable image carousel with spring snapping.
1277
+ * Composes: lite-gesture + lite-tween + lite-ease + lite-timeline
1278
+ */
1279
+ gestureCarousel(container, slides, options = {}) {
1280
+ const {stiffness = 200, damping = 22} = options;
1281
+ const el = typeof container === 'string' ? document.querySelector(container) : container;
1282
+ if (!el) return {
1283
+ goTo() {
1284
+ }, destroy() {
1285
+ }
1286
+ };
1287
+ const gesture = new GestureRecognizer(el);
1288
+ let currentIndex = 0, offsetX = 0;
1289
+ const slideWidth = el.clientWidth || 300;
1290
+
1291
+ function goTo(idx) {
1292
+ currentIndex = clamp(idx, 0, slides.length - 1);
1293
+ const targetX = -currentIndex * slideWidth;
1294
+ const startX = offsetX;
1295
+ const tl = new Timeline();
1296
+ tl.add({
1297
+ duration: 400, onUpdate(t) {
1298
+ offsetX = startX + (targetX - startX) * easeOutCubic(t);
1299
+ renderSlides();
1300
+ }
1301
+ });
1302
+ tl.play();
1303
+ }
1304
+
1305
+ function renderSlides() {
1306
+ for (let i = 0; i < slides.length; i++) {
1307
+ const s = typeof slides[i] === 'string' ? document.querySelector(slides[i]) : slides[i];
1308
+ if (s) s.style.transform = `translateX(${offsetX + i * slideWidth}px)`;
1309
+ }
1310
+ }
1311
+
1312
+ gesture.on('panEnd', (e) => {
1313
+ if (e.velocityX < -0.3 || e.deltaX < -slideWidth * 0.3) goTo(currentIndex + 1);
1314
+ else if (e.velocityX > 0.3 || e.deltaX > slideWidth * 0.3) goTo(currentIndex - 1);
1315
+ else goTo(currentIndex);
1316
+ });
1317
+
1318
+ gesture.on('pan', (e) => {
1319
+ offsetX = -currentIndex * slideWidth + e.deltaX;
1320
+ renderSlides();
1321
+ });
1322
+
1323
+ renderSlides();
1324
+ return {
1325
+ goTo, getCurrentIndex: () => currentIndex, destroy() {
1326
+ gesture.destroy();
1327
+ }
1328
+ };
1329
+ },
1330
+
1331
+ /**
1332
+ * 22. Timeline Showcase — Choreographed animation sequence with confetti finale.
1333
+ * Composes: lite-timeline + lite-tween + lite-ease + lite-confetti
1334
+ */
1335
+ timelineShowcase(elements, overlayCanvas, options = {}) {
1336
+ const {brandColor = {l: 0.6, c: 0.25, h: 280}} = options;
1337
+ const els = typeof elements === 'string' ? document.querySelectorAll(elements) : elements;
1338
+ const tl = new Timeline({loop: false});
1339
+
1340
+ // Stagger each element in with eased opacity + translateY
1341
+ let offset = 0;
1342
+ for (let i = 0; i < els.length; i++) {
1343
+ const el = els[i];
1344
+ if (!el) continue;
1345
+ el.style.opacity = '0';
1346
+ el.style.transform = 'translateY(30px)';
1347
+ tl.add({
1348
+ delay: offset, duration: 500, onUpdate(t) {
1349
+ const e = easeOutBack(t);
1350
+ el.style.opacity = String(t);
1351
+ el.style.transform = `translateY(${30 * (1 - e)}px)`;
1352
+ }
1353
+ });
1354
+ offset += 150;
1355
+ }
1356
+
1357
+ // Confetti burst after stagger completes
1358
+ tl.add({
1359
+ delay: offset + 200, duration: 0, onComplete() {
1360
+ if (overlayCanvas) confettiFn({
1361
+ y: overlayCanvas.height * 0.3,
1362
+ count: 100,
1363
+ shape: 'star',
1364
+ colors: [brandColor]
1365
+ });
1366
+ }
1367
+ });
1368
+
1369
+ return {
1370
+ timeline: tl, play() {
1371
+ tl.play();
1372
+ }, destroy() {
1373
+ tl.destroy?.();
1374
+ }
1375
+ };
1376
+ },
1377
+
1378
+ /**
1379
+ * 23. Spark Impact — Click-to-explode with sparks, fireworks, and camera shake.
1380
+ * Composes: lite-sparks + lite-fireworks + lite-camera
1381
+ */
1382
+ sparkImpact(canvas, options = {}) {
1383
+ const {maxSparks = 5000, maxFireworks = 3000, shakeIntensity = 8} = options;
1384
+ const ctx = canvas.getContext('2d');
1385
+ const sparks = new SparkEngine(maxSparks);
1386
+ const fireworks = new FireworksEngine(maxFireworks);
1387
+ const cam = new Camera({smoothing: 0.05});
1388
+ let w = canvas.width, h = canvas.height;
1389
+
1390
+ function explodeAt(x, y) {
1391
+ sparks.burst(x, y, 80, 0, Math.PI * 2, 200, 800, 0.3, 1.0);
1392
+ fireworks.explode(x, y, Math.floor(Math.random() * fireworks.colors.length));
1393
+ cam.shake(shakeIntensity, 0.3);
1394
+ }
1395
+
1396
+ function update(dt) {
1397
+ cam.update(dt);
1398
+ const {x: offX, y: offY} = cam.getOffset();
1399
+ ctx.save();
1400
+ ctx.translate(offX, offY);
1401
+ fireworks.updateAndDraw(ctx, dt, w, h);
1402
+ sparks.updateAndDraw(ctx, dt, w, h);
1403
+ ctx.restore();
1404
+ }
1405
+
1406
+ return {
1407
+ explodeAt, update, sparks, fireworks, cam, destroy() {
1408
+ sparks.destroy();
1409
+ fireworks.destroy();
1410
+ }
1411
+ };
1412
+ },
1413
+
1414
+ /**
1415
+ * 24. Audio Reactive VFX — Particles respond to audio beats.
1416
+ * Composes: lite-audio-pool + lite-embers + lite-ease + lite-gradient
1417
+ */
1418
+ audioReactiveVFX(canvas, options = {}) {
1419
+ const {maxEmbers = 4000} = options;
1420
+ const ctx = canvas.getContext('2d');
1421
+ const embers = new EmberEngine(maxEmbers, {buoyancy: 200, driftAmplitude: 40});
1422
+ let audioCtx = null, analyser = null, freqData = null;
1423
+
1424
+ function connectAudio(sourceNode) {
1425
+ if (!audioCtx) audioCtx = sourceNode.context;
1426
+ analyser = audioCtx.createAnalyser();
1427
+ analyser.fftSize = 256;
1428
+ freqData = new Uint8Array(analyser.frequencyBinCount);
1429
+ sourceNode.connect(analyser);
1430
+ }
1431
+
1432
+ function update(dt, w, h) {
1433
+ let energy = 0;
1434
+ if (analyser && freqData) {
1435
+ analyser.getByteFrequencyData(freqData);
1436
+ for (let i = 0; i < 16; i++) energy += freqData[i];
1437
+ energy /= 16 * 255;
1438
+ }
1439
+
1440
+ if (energy > 0.3) {
1441
+ embers.config.density = 5 + energy * 30;
1442
+ embers.config.buoyancy = 100 + energy * 300;
1443
+ } else {
1444
+ embers.config.density = 3;
1445
+ embers.config.buoyancy = 120;
1446
+ }
1447
+
1448
+ embers.spawn(dt, w, h, w * 0.3, h - 50, w * 0.4, 30);
1449
+ embers.updateAndDraw(ctx, dt, w, h);
1450
+ }
1451
+
1452
+ return {
1453
+ connectAudio, update, embers, destroy() {
1454
+ embers.destroy();
1455
+ }
1456
+ };
1457
+ },
1458
+ };
1459
+ export default Recipes;