@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.d.ts +136 -9
- package/LiteEngine.js +606 -10
- package/README.md +359 -21
- package/llms.txt +37 -36
- package/package.json +56 -15
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
|
-
*
|
|
5
|
-
*
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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;
|