koi-pond 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 landrew
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # koi-pond
2
+
3
+ Interactive koi fish simulation using boids flocking. Drop it on any HTML canvas.
4
+
5
+ Fish school together, orbit your cursor, and scatter when you click or swipe. Built with Canvas 2D, zero dependencies at runtime.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install koi-pond
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```html
16
+ <canvas id="pond" style="width: 100%; height: 100vh"></canvas>
17
+
18
+ <script type="module">
19
+ import { createKoiPond } from "koi-pond";
20
+
21
+ const canvas = document.getElementById("pond");
22
+ const pond = createKoiPond(canvas);
23
+ pond.start();
24
+ </script>
25
+ ```
26
+
27
+ ## Options
28
+
29
+ ```ts
30
+ import { createKoiPond } from "koi-pond";
31
+
32
+ const pond = createKoiPond(canvas, {
33
+ // Number of fish (default: 18)
34
+ count: 24,
35
+
36
+ // Per-fish opacity — useful for fading fish behind UI elements
37
+ alphaFn: (koi) => (koi.pos.x < 200 ? 0.15 : 1),
38
+
39
+ // Override any simulation config values
40
+ config: {
41
+ maxSpeed: 3,
42
+ separationRadius: 80,
43
+ },
44
+
45
+ // Respect prefers-reduced-motion (default: true)
46
+ // When true and user prefers reduced motion, renders a single static frame
47
+ respectReducedMotion: true,
48
+ });
49
+
50
+ pond.start();
51
+
52
+ // Later...
53
+ pond.stop(); // pause animation (listeners stay attached)
54
+ pond.destroy(); // stop + remove all event listeners
55
+ ```
56
+
57
+ ## Low-level API
58
+
59
+ For full control over the simulation loop:
60
+
61
+ ```ts
62
+ import {
63
+ createSchool,
64
+ stepSimulation,
65
+ createRenderer,
66
+ defaultConfig,
67
+ type PondConfig,
68
+ type MouseState,
69
+ } from "koi-pond";
70
+
71
+ const config: PondConfig = {
72
+ ...defaultConfig,
73
+ width: canvas.width,
74
+ height: canvas.height,
75
+ };
76
+
77
+ let school = createSchool(config);
78
+ const renderer = createRenderer(canvas, config);
79
+ let mouse: MouseState | null = null;
80
+
81
+ const tick = (now: number) => {
82
+ const dt = /* your delta time */ 1;
83
+ school = stepSimulation(school, mouse, config, dt);
84
+ renderer.render(school, now / 1000);
85
+ requestAnimationFrame(tick);
86
+ };
87
+
88
+ requestAnimationFrame(tick);
89
+ ```
90
+
91
+ ## How it works
92
+
93
+ Each fish runs Craig Reynolds' [boids](https://www.red3d.com/cwr/boids/) algorithm with three forces:
94
+
95
+ - **Separation** — avoid crowding nearby fish
96
+ - **Alignment** — steer toward the average heading of neighbors
97
+ - **Cohesion** — move toward the center of nearby fish
98
+
99
+ On top of that, fish:
100
+ - Avoid canvas edges with a soft turn force
101
+ - Wander randomly for natural movement
102
+ - Approach a still cursor and orbit it
103
+ - Scatter away from fast cursor movement or clicks
104
+
105
+ Each fish carries a chain-spine: an array of world-space points that follow the head like links in a chain. Turns bend the body, straight swimming straightens it. The renderer draws the body outline from this spine, with colored splotch patches, pectoral fins, and a flowing tail ribbon.
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,93 @@
1
+ interface Vec2 {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ interface MouseState {
6
+ pos: Vec2;
7
+ speed: number;
8
+ scatter: number;
9
+ }
10
+ /** Number of spine segments per fish. */
11
+ declare const SPINE_POINTS = 20;
12
+ /** Body length = size * this multiplier. */
13
+ declare const BODY_LENGTH_MULT = 2.4;
14
+ /** A color splotch on a koi's body. */
15
+ interface Patch {
16
+ t: number;
17
+ offset: number;
18
+ rx: number;
19
+ ry: number;
20
+ }
21
+ interface Koi {
22
+ pos: Vec2;
23
+ vel: Vec2;
24
+ acc: Vec2;
25
+ patchColor: string;
26
+ patches: Patch[];
27
+ size: number;
28
+ spine: Vec2[];
29
+ swimPhase: number;
30
+ orbiting: boolean;
31
+ }
32
+ interface PondConfig {
33
+ width: number;
34
+ height: number;
35
+ count: number;
36
+ maxSpeed: number;
37
+ maxForce: number;
38
+ separationRadius: number;
39
+ alignmentRadius: number;
40
+ cohesionRadius: number;
41
+ separationWeight: number;
42
+ alignmentWeight: number;
43
+ cohesionWeight: number;
44
+ edgeMargin: number;
45
+ edgeTurnForce: number;
46
+ wanderStrength: number;
47
+ mouseRadius: number;
48
+ mouseWeight: number;
49
+ maxTurnRate: number;
50
+ }
51
+ declare const defaultConfig: PondConfig;
52
+
53
+ /** Optional per-fish alpha callback. Return 0-1 to control visibility. */
54
+ type AlphaFn = (koi: Koi) => number;
55
+ interface Renderer {
56
+ render: (school: Koi[], time: number) => void;
57
+ resize: (width: number, height: number) => void;
58
+ setAlphaFn: (fn: AlphaFn | null) => void;
59
+ }
60
+ declare const createRenderer: (canvas: HTMLCanvasElement, _config: PondConfig, alphaFn?: AlphaFn) => Renderer;
61
+
62
+ interface KoiPondOptions {
63
+ /** Number of fish. Defaults to 18. */
64
+ count?: number;
65
+ /** Override any PondConfig values. */
66
+ config?: Partial<PondConfig>;
67
+ /** Per-fish alpha callback for fading fish in certain regions. */
68
+ alphaFn?: AlphaFn;
69
+ /** Respect prefers-reduced-motion. Defaults to true. */
70
+ respectReducedMotion?: boolean;
71
+ }
72
+ interface KoiPondHandle {
73
+ /** Start the animation loop and attach event listeners. */
74
+ start: () => void;
75
+ /** Pause the animation (listeners stay attached). */
76
+ stop: () => void;
77
+ /** Stop animation and remove all event listeners. */
78
+ destroy: () => void;
79
+ /** Update the alpha function at runtime. */
80
+ setAlphaFn: (fn: AlphaFn | null) => void;
81
+ /** Access the current school of fish (read-only snapshot). */
82
+ school: () => readonly Koi[];
83
+ /** Access the renderer. */
84
+ renderer: Renderer;
85
+ }
86
+ declare const createKoiPond: (canvas: HTMLCanvasElement, options?: KoiPondOptions) => KoiPondHandle;
87
+
88
+ declare const createKoi: (width: number, height: number) => Koi;
89
+ declare const createSchool: (config: PondConfig) => Koi[];
90
+ declare const updateKoi: (koi: Koi, school: Koi[], mouse: MouseState | null, config: PondConfig, dt: number, orbitCount: number) => Koi;
91
+ declare const stepSimulation: (school: Koi[], mouse: MouseState | null, config: PondConfig, dt: number) => Koi[];
92
+
93
+ export { type AlphaFn, BODY_LENGTH_MULT, type Koi, type KoiPondHandle, type KoiPondOptions, type MouseState, type Patch, type PondConfig, type Renderer, SPINE_POINTS, type Vec2, createKoi, createKoiPond, createRenderer, createSchool, defaultConfig, stepSimulation, updateKoi };
package/dist/index.js ADDED
@@ -0,0 +1,721 @@
1
+ // src/types.ts
2
+ var SPINE_POINTS = 20;
3
+ var BODY_LENGTH_MULT = 2.4;
4
+ var defaultConfig = {
5
+ width: 800,
6
+ height: 600,
7
+ count: 18,
8
+ maxSpeed: 2.2,
9
+ maxForce: 0.05,
10
+ separationRadius: 70,
11
+ alignmentRadius: 100,
12
+ cohesionRadius: 120,
13
+ separationWeight: 2.8,
14
+ alignmentWeight: 1,
15
+ cohesionWeight: 1,
16
+ edgeMargin: 80,
17
+ edgeTurnForce: 0.15,
18
+ wanderStrength: 0.02,
19
+ mouseRadius: 150,
20
+ mouseWeight: 1.5,
21
+ maxTurnRate: 0.045
22
+ };
23
+
24
+ // src/boids.ts
25
+ var PATCH_COLORS = [
26
+ "#c85040",
27
+ // hi (red-orange)
28
+ "#e08030",
29
+ // orenji (orange)
30
+ "#cc3333",
31
+ // aka (red)
32
+ "#2c3e50",
33
+ // sumi (ink black)
34
+ "#d4a050",
35
+ // yamabuki (gold)
36
+ "#8c5a3c",
37
+ // chagoi (brown)
38
+ "#b84040",
39
+ // beni (crimson)
40
+ "#404040"
41
+ // shiro-sumi (charcoal)
42
+ ];
43
+ var vec = (x, y) => ({ x, y });
44
+ var add = (a, b) => vec(a.x + b.x, a.y + b.y);
45
+ var sub = (a, b) => vec(a.x - b.x, a.y - b.y);
46
+ var scale = (v, s) => vec(v.x * s, v.y * s);
47
+ var mag = (v) => Math.sqrt(v.x * v.x + v.y * v.y);
48
+ var normalize = (v) => {
49
+ const m = mag(v);
50
+ return m > 0 ? scale(v, 1 / m) : vec(0, 0);
51
+ };
52
+ var limit = (v, max) => {
53
+ const m = mag(v);
54
+ return m > max ? scale(normalize(v), max) : v;
55
+ };
56
+ var randomInRange = (min, max) => Math.random() * (max - min) + min;
57
+ var pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
58
+ var segLen = (size) => size * BODY_LENGTH_MULT / SPINE_POINTS;
59
+ var generatePatches = () => {
60
+ const count = Math.floor(randomInRange(2, 6));
61
+ const patches = [];
62
+ for (let i = 0; i < count; i++) {
63
+ patches.push({
64
+ t: randomInRange(0.05, 0.85),
65
+ offset: randomInRange(-0.6, 0.6),
66
+ rx: randomInRange(0.06, 0.18),
67
+ ry: randomInRange(0.5, 1.4)
68
+ });
69
+ }
70
+ return patches;
71
+ };
72
+ var createKoi = (width, height) => {
73
+ const angle = Math.random() * Math.PI * 2;
74
+ const speed = randomInRange(0.5, 1.5);
75
+ const pos = vec(randomInRange(50, width - 50), randomInRange(50, height - 50));
76
+ const dir = vec(Math.cos(angle), Math.sin(angle));
77
+ const size = randomInRange(18, 32);
78
+ const sl = segLen(size);
79
+ const spine = Array.from(
80
+ { length: SPINE_POINTS + 1 },
81
+ (_, i) => vec(pos.x - dir.x * sl * i, pos.y - dir.y * sl * i)
82
+ );
83
+ return {
84
+ pos,
85
+ vel: vec(dir.x * speed, dir.y * speed),
86
+ acc: vec(0, 0),
87
+ patchColor: pickRandom(PATCH_COLORS),
88
+ patches: generatePatches(),
89
+ size,
90
+ spine,
91
+ swimPhase: Math.random() * Math.PI * 2,
92
+ orbiting: false
93
+ };
94
+ };
95
+ var createSchool = (config) => Array.from(
96
+ { length: config.count },
97
+ () => createKoi(config.width, config.height)
98
+ );
99
+ var distSq = (a, b) => {
100
+ const dx = a.x - b.x;
101
+ const dy = a.y - b.y;
102
+ return dx * dx + dy * dy;
103
+ };
104
+ var computeFlockForces = (koi, school, config) => {
105
+ const { separationRadius, alignmentRadius, cohesionRadius, maxSpeed, maxForce } = config;
106
+ const sepR2 = separationRadius * separationRadius;
107
+ const aliR2 = alignmentRadius * alignmentRadius;
108
+ const cohR2 = cohesionRadius * cohesionRadius;
109
+ const maxR2 = Math.max(sepR2, aliR2, cohR2);
110
+ let sepSteer = vec(0, 0);
111
+ let sepCount = 0;
112
+ let aliAvg = vec(0, 0);
113
+ let aliCount = 0;
114
+ let cohCenter = vec(0, 0);
115
+ let cohCount = 0;
116
+ for (const other of school) {
117
+ const d2 = distSq(koi.pos, other.pos);
118
+ if (d2 === 0 || d2 >= maxR2) continue;
119
+ if (d2 < sepR2) {
120
+ const d = Math.sqrt(d2);
121
+ const diff = scale(normalize(sub(koi.pos, other.pos)), 1 / d);
122
+ sepSteer = add(sepSteer, diff);
123
+ sepCount++;
124
+ }
125
+ if (d2 < aliR2) {
126
+ aliAvg = add(aliAvg, other.vel);
127
+ aliCount++;
128
+ }
129
+ if (d2 < cohR2) {
130
+ cohCenter = add(cohCenter, other.pos);
131
+ cohCount++;
132
+ }
133
+ }
134
+ let sep = vec(0, 0);
135
+ if (sepCount > 0) {
136
+ sepSteer = scale(sepSteer, 1 / sepCount);
137
+ sepSteer = scale(normalize(sepSteer), maxSpeed);
138
+ sep = limit(sub(sepSteer, koi.vel), maxForce);
139
+ }
140
+ let ali = vec(0, 0);
141
+ if (aliCount > 0) {
142
+ aliAvg = scale(aliAvg, 1 / aliCount);
143
+ aliAvg = scale(normalize(aliAvg), maxSpeed);
144
+ ali = limit(sub(aliAvg, koi.vel), maxForce);
145
+ }
146
+ let coh = vec(0, 0);
147
+ if (cohCount > 0) {
148
+ cohCenter = scale(cohCenter, 1 / cohCount);
149
+ const desired = scale(normalize(sub(cohCenter, koi.pos)), maxSpeed);
150
+ coh = limit(sub(desired, koi.vel), maxForce);
151
+ }
152
+ return { sep, ali, coh };
153
+ };
154
+ var avoidEdges = (koi, config) => {
155
+ let steer = vec(0, 0);
156
+ const { edgeMargin: m, edgeTurnForce: f, width: w, height: h } = config;
157
+ if (koi.pos.x < m) steer = add(steer, vec(f, 0));
158
+ if (koi.pos.x > w - m) steer = add(steer, vec(-f, 0));
159
+ if (koi.pos.y < m) steer = add(steer, vec(0, f));
160
+ if (koi.pos.y > h - m) steer = add(steer, vec(0, -f));
161
+ return steer;
162
+ };
163
+ var wander = (config) => vec(
164
+ (Math.random() - 0.5) * config.wanderStrength,
165
+ (Math.random() - 0.5) * config.wanderStrength
166
+ );
167
+ var ATTRACT_RADIUS = 300;
168
+ var SCATTER_RADIUS = 250;
169
+ var ORBIT_ENTER = 80;
170
+ var ORBIT_EXIT = 150;
171
+ var mouseInteraction = (koi, mouse, config, orbitCount) => {
172
+ if (!mouse) return { force: vec(0, 0), orbiting: false };
173
+ const d2 = distSq(koi.pos, mouse.pos);
174
+ const d = Math.sqrt(d2);
175
+ const orbitGrow = orbitCount * 12;
176
+ const enterR = ORBIT_ENTER + orbitGrow;
177
+ const exitR = ORBIT_EXIT + orbitGrow;
178
+ const attractR = Math.max(ATTRACT_RADIUS, exitR + 80);
179
+ if (mouse.scatter > 0.2) {
180
+ if (d > SCATTER_RADIUS || d === 0)
181
+ return { force: vec(0, 0), orbiting: false };
182
+ const away = normalize(sub(koi.pos, mouse.pos));
183
+ const heading = normalize(koi.vel);
184
+ const strength2 = config.maxSpeed * 3 * mouse.scatter * (1 - d / SCATTER_RADIUS);
185
+ const dot = away.x * heading.x + away.y * heading.y;
186
+ let fleeDir;
187
+ if (dot < 0.2) {
188
+ const cross = heading.x * away.y - heading.y * away.x;
189
+ const side = cross >= 0 ? 1 : -1;
190
+ fleeDir = vec(-heading.y * side, heading.x * side);
191
+ } else {
192
+ fleeDir = away;
193
+ }
194
+ const flee = scale(fleeDir, strength2);
195
+ return {
196
+ force: limit(sub(flee, koi.vel), config.maxForce * 6),
197
+ orbiting: false
198
+ };
199
+ }
200
+ const inOrbit = koi.orbiting ? d < exitR : d < enterR;
201
+ if (d > attractR) return { force: vec(0, 0), orbiting: false };
202
+ const toward = normalize(sub(mouse.pos, koi.pos));
203
+ if (inOrbit) {
204
+ const tangent = vec(-toward.y, toward.x);
205
+ const orbitSpeed = config.maxSpeed * 0.45;
206
+ const steer2 = scale(tangent, orbitSpeed);
207
+ const inward = scale(toward, config.maxSpeed * 0.15 * (d / exitR));
208
+ return {
209
+ force: limit(
210
+ sub(add(steer2, inward), koi.vel),
211
+ config.maxForce * 1.5
212
+ ),
213
+ orbiting: true
214
+ };
215
+ }
216
+ const strength = config.maxSpeed * 0.9 * ((d - exitR) / (attractR - exitR));
217
+ const steer = scale(toward, Math.max(0, strength));
218
+ return {
219
+ force: limit(sub(steer, koi.vel), config.maxForce * 2),
220
+ orbiting: false
221
+ };
222
+ };
223
+ var updateSpine = (headPos, oldSpine, sl) => {
224
+ const spine = [headPos];
225
+ for (let i = 1; i <= SPINE_POINTS; i++) {
226
+ const prev = spine[i - 1];
227
+ const cur = oldSpine[i] ?? oldSpine[oldSpine.length - 1];
228
+ const d = sub(cur, prev);
229
+ const dm = mag(d);
230
+ spine.push(
231
+ dm > 0 ? add(prev, scale(d, sl / dm)) : vec(prev.x - sl, prev.y)
232
+ );
233
+ }
234
+ return spine;
235
+ };
236
+ var updateKoi = (koi, school, mouse, config, dt, orbitCount) => {
237
+ const flock = computeFlockForces(koi, school, config);
238
+ const sep = scale(flock.sep, config.separationWeight);
239
+ const ali = scale(flock.ali, config.alignmentWeight);
240
+ const coh = scale(flock.coh, config.cohesionWeight);
241
+ const edge = avoidEdges(koi, config);
242
+ const wan = wander(config);
243
+ const mouseResult = mouseInteraction(koi, mouse, config, orbitCount);
244
+ const mouseForce = scale(mouseResult.force, config.mouseWeight);
245
+ const acc = [sep, ali, coh, edge, wan, mouseForce].reduce(add, vec(0, 0));
246
+ const scatterAmt = mouse?.scatter ?? 0;
247
+ const cruiseMax = config.maxSpeed * 0.6;
248
+ const effectiveMax = scatterAmt > 0.2 ? config.maxSpeed * (1 + scatterAmt * 2.5) : mouseResult.orbiting ? config.maxSpeed : cruiseMax;
249
+ const desiredVel = limit(add(koi.vel, scale(acc, dt)), effectiveMax);
250
+ const isOrbiting = mouseResult.orbiting;
251
+ const turnBoost = isOrbiting ? 2.5 : 1 + scatterAmt * 3;
252
+ const curAngle = Math.atan2(koi.vel.y, koi.vel.x);
253
+ const desiredAngle = Math.atan2(desiredVel.y, desiredVel.x);
254
+ let angleDiff = desiredAngle - curAngle;
255
+ angleDiff = (angleDiff + Math.PI * 3) % (Math.PI * 2) - Math.PI;
256
+ const maxTurn = config.maxTurnRate * dt * turnBoost;
257
+ const clampedAngle = curAngle + Math.max(-maxTurn, Math.min(maxTurn, angleDiff));
258
+ const curSpeed = mag(koi.vel);
259
+ const desiredSpeed = mag(desiredVel);
260
+ const minOrbitSpeed = isOrbiting ? config.maxSpeed * 0.4 : 0;
261
+ const cruiseSpeed = config.maxSpeed * 0.5;
262
+ const belowCruise = curSpeed < cruiseSpeed ? 0.08 : 0;
263
+ const blendRate = desiredSpeed > curSpeed ? 0.06 + scatterAmt * 0.4 + belowCruise + (isOrbiting ? 0.12 : 0) : 0.03;
264
+ const speed = Math.max(
265
+ minOrbitSpeed,
266
+ curSpeed + (desiredSpeed - curSpeed) * blendRate
267
+ );
268
+ const newVel = vec(
269
+ Math.cos(clampedAngle) * speed,
270
+ Math.sin(clampedAngle) * speed
271
+ );
272
+ const newPos = add(koi.pos, scale(newVel, dt));
273
+ const traveled = mag(scale(newVel, dt));
274
+ const bodyLen = koi.size * BODY_LENGTH_MULT;
275
+ const newSwimPhase = koi.swimPhase + traveled * (Math.PI * 2) / bodyLen;
276
+ const speedRatio = speed / config.maxSpeed;
277
+ const wiggleAmp = koi.size * 0.04 * speedRatio;
278
+ const perpX = -Math.sin(clampedAngle);
279
+ const perpY = Math.cos(clampedAngle);
280
+ const headPos = vec(
281
+ newPos.x + perpX * Math.sin(newSwimPhase) * wiggleAmp,
282
+ newPos.y + perpY * Math.sin(newSwimPhase) * wiggleAmp
283
+ );
284
+ const sl = segLen(koi.size);
285
+ const newSpine = updateSpine(headPos, koi.spine, sl);
286
+ return {
287
+ ...koi,
288
+ pos: newPos,
289
+ vel: newVel,
290
+ acc,
291
+ spine: newSpine,
292
+ swimPhase: newSwimPhase,
293
+ orbiting: mouseResult.orbiting
294
+ };
295
+ };
296
+ var stepSimulation = (school, mouse, config, dt) => {
297
+ const orbitCount = school.filter((k) => k.orbiting).length;
298
+ return school.map(
299
+ (koi) => updateKoi(koi, school, mouse, config, dt, orbitCount)
300
+ );
301
+ };
302
+
303
+ // src/renderer.ts
304
+ var perp = (spine, i) => {
305
+ const a = spine[Math.max(0, i - 1)];
306
+ const b = spine[Math.min(spine.length - 1, i + 1)];
307
+ const dx = b.x - a.x;
308
+ const dy = b.y - a.y;
309
+ const len = Math.hypot(dx, dy) || 1;
310
+ return { x: -dy / len, y: dx / len };
311
+ };
312
+ var tang = (spine, i) => {
313
+ const a = spine[Math.max(0, i - 1)];
314
+ const b = spine[Math.min(spine.length - 1, i + 1)];
315
+ const dx = b.x - a.x;
316
+ const dy = b.y - a.y;
317
+ const len = Math.hypot(dx, dy) || 1;
318
+ return { x: dx / len, y: dy / len };
319
+ };
320
+ var widthAt = (t) => {
321
+ const peak = 0.22;
322
+ if (t < peak) {
323
+ const r = t / peak;
324
+ return 0.3 + 0.7 * Math.sqrt(r);
325
+ }
326
+ return Math.pow(1 - (t - peak) / (1 - peak), 0.6);
327
+ };
328
+ var buildBodyOutline = (spine, maxHW) => {
329
+ const n = spine.length;
330
+ const left = [];
331
+ const right = [];
332
+ for (let i = 0; i < n; i++) {
333
+ const t = i / (n - 1);
334
+ const p = spine[i];
335
+ const pr = perp(spine, i);
336
+ const hw = widthAt(t) * maxHW;
337
+ left.push({ x: p.x + pr.x * hw, y: p.y + pr.y * hw });
338
+ right.push({ x: p.x - pr.x * hw, y: p.y - pr.y * hw });
339
+ }
340
+ const path = new Path2D();
341
+ path.moveTo(left[0].x, left[0].y);
342
+ for (let i = 1; i < n; i++) {
343
+ const p = left[i - 1];
344
+ const c = left[i];
345
+ path.quadraticCurveTo((p.x + c.x) / 2, (p.y + c.y) / 2, c.x, c.y);
346
+ }
347
+ path.lineTo(right[n - 1].x, right[n - 1].y);
348
+ for (let i = n - 2; i >= 0; i--) {
349
+ const p = right[i + 1];
350
+ const c = right[i];
351
+ path.quadraticCurveTo((p.x + c.x) / 2, (p.y + c.y) / 2, c.x, c.y);
352
+ }
353
+ path.closePath();
354
+ return { path, left, right };
355
+ };
356
+ var drawKoi = (ctx, koi, alpha) => {
357
+ const spine = koi.spine;
358
+ const n = spine.length;
359
+ const s = koi.size;
360
+ const maxHW = s * 0.28;
361
+ ctx.save();
362
+ ctx.globalAlpha = alpha;
363
+ const tailStart = Math.round(n * 0.65);
364
+ const tailSegments = n - tailStart;
365
+ ctx.globalAlpha = alpha * 0.5;
366
+ ctx.fillStyle = koi.patchColor || "#c4a882";
367
+ ctx.beginPath();
368
+ {
369
+ const ribbonLeft = [];
370
+ const ribbonRight = [];
371
+ for (let i = tailStart; i < n; i++) {
372
+ const localT = (i - tailStart) / (tailSegments - 1);
373
+ const p = spine[i];
374
+ const pr = perp(spine, i);
375
+ let curvature = 0;
376
+ if (i > 0 && i < n - 1) {
377
+ const t0 = tang(spine, i - 1);
378
+ const t1 = tang(spine, i);
379
+ curvature = t0.x * t1.y - t0.y * t1.x;
380
+ }
381
+ const ease = localT * localT * (3 - 2 * localT);
382
+ const bodyW = widthAt(i / (n - 1)) * maxHW;
383
+ const ribbonW = bodyW + ease * maxHW * 0.85;
384
+ const absCurv = Math.abs(curvature);
385
+ const collapse = absCurv * s * 2.5 * ease;
386
+ const swell = curvature * s * 0.6 * ease;
387
+ ribbonLeft.push({
388
+ x: p.x + pr.x * Math.max(
389
+ 0,
390
+ ribbonW + swell - (curvature > 0 ? 0 : collapse)
391
+ ),
392
+ y: p.y + pr.y * Math.max(
393
+ 0,
394
+ ribbonW + swell - (curvature > 0 ? 0 : collapse)
395
+ )
396
+ });
397
+ ribbonRight.push({
398
+ x: p.x - pr.x * Math.max(
399
+ 0,
400
+ ribbonW - swell - (curvature > 0 ? collapse : 0)
401
+ ),
402
+ y: p.y - pr.y * Math.max(
403
+ 0,
404
+ ribbonW - swell - (curvature > 0 ? collapse : 0)
405
+ )
406
+ });
407
+ }
408
+ const lastT = tang(spine, n - 1);
409
+ const lastP = perp(spine, n - 1);
410
+ const tipLen = s * 0.3;
411
+ const tipW = maxHW * 0.35;
412
+ const endCurv = (() => {
413
+ const t0 = tang(spine, n - 2);
414
+ const t1 = tang(spine, n - 1);
415
+ return (t0.x * t1.y - t0.y * t1.x) * s * 1.8;
416
+ })();
417
+ const tip = {
418
+ x: spine[n - 1].x + lastT.x * tipLen,
419
+ y: spine[n - 1].y + lastT.y * tipLen
420
+ };
421
+ ribbonLeft.push({
422
+ x: tip.x + lastP.x * (tipW + endCurv),
423
+ y: tip.y + lastP.y * (tipW + endCurv)
424
+ });
425
+ ribbonRight.push({
426
+ x: tip.x - lastP.x * (tipW - endCurv),
427
+ y: tip.y - lastP.y * (tipW - endCurv)
428
+ });
429
+ ctx.moveTo(ribbonLeft[0].x, ribbonLeft[0].y);
430
+ for (let i = 1; i < ribbonLeft.length; i++) {
431
+ const prev = ribbonLeft[i - 1];
432
+ const cur = ribbonLeft[i];
433
+ const mx = (prev.x + cur.x) / 2;
434
+ const my = (prev.y + cur.y) / 2;
435
+ ctx.quadraticCurveTo(prev.x, prev.y, mx, my);
436
+ }
437
+ const lastL = ribbonLeft[ribbonLeft.length - 1];
438
+ ctx.lineTo(lastL.x, lastL.y);
439
+ ctx.quadraticCurveTo(
440
+ tip.x,
441
+ tip.y,
442
+ ribbonRight[ribbonRight.length - 1].x,
443
+ ribbonRight[ribbonRight.length - 1].y
444
+ );
445
+ for (let i = ribbonRight.length - 2; i >= 0; i--) {
446
+ const prev = ribbonRight[i + 1];
447
+ const cur = ribbonRight[i];
448
+ const mx = (prev.x + cur.x) / 2;
449
+ const my = (prev.y + cur.y) / 2;
450
+ ctx.quadraticCurveTo(prev.x, prev.y, mx, my);
451
+ }
452
+ ctx.lineTo(ribbonRight[0].x, ribbonRight[0].y);
453
+ ctx.closePath();
454
+ ctx.fill();
455
+ }
456
+ ctx.globalAlpha = alpha;
457
+ const body = buildBodyOutline(spine, maxHW);
458
+ const clipBody = buildBodyOutline(spine, maxHW * 0.92);
459
+ ctx.fillStyle = "#d0c0ac";
460
+ ctx.fill(body.path);
461
+ ctx.save();
462
+ ctx.clip(clipBody.path);
463
+ ctx.fillStyle = koi.patchColor;
464
+ for (const patch of koi.patches) {
465
+ const idx = Math.round(patch.t * (n - 1));
466
+ const p = spine[idx];
467
+ const pr = perp(spine, idx);
468
+ const tg = tang(spine, idx);
469
+ const hw = widthAt(patch.t) * maxHW;
470
+ const cx = p.x + pr.x * hw * patch.offset;
471
+ const cy = p.y + pr.y * hw * patch.offset;
472
+ const bodyLen = s * BODY_LENGTH_MULT;
473
+ const rx = patch.rx * bodyLen;
474
+ const ry = patch.ry * maxHW;
475
+ const angle = Math.atan2(tg.y, tg.x);
476
+ ctx.beginPath();
477
+ ctx.ellipse(cx, cy, rx, ry, angle, 0, Math.PI * 2);
478
+ ctx.fill();
479
+ }
480
+ ctx.restore();
481
+ ctx.strokeStyle = "rgba(0,0,0,0.12)";
482
+ ctx.lineWidth = 0.8;
483
+ ctx.stroke(body.path);
484
+ const finIdx = Math.round((n - 1) * 0.2);
485
+ ctx.globalAlpha = alpha * 0.5;
486
+ ctx.fillStyle = koi.patchColor || "#c4a882";
487
+ for (const side of [1, -1]) {
488
+ const fp = spine[finIdx];
489
+ const fPerp = perp(spine, finIdx);
490
+ const fTang = tang(spine, finIdx);
491
+ const bodyW = widthAt(finIdx / (n - 1)) * maxHW;
492
+ const spread = 0.5 + Math.sin(koi.swimPhase * 2.5 + side * 1.2) * 0.2;
493
+ const finR = s * 0.5;
494
+ let curv = 0;
495
+ if (finIdx > 0 && finIdx < n - 1) {
496
+ const t0 = tang(spine, finIdx - 1);
497
+ const t1 = tang(spine, finIdx);
498
+ curv = t0.x * t1.y - t0.y * t1.x;
499
+ }
500
+ const isInnerSide = side === 1 && curv > 0 || side === -1 && curv < 0;
501
+ const collapseAmt = isInnerSide ? Math.min(1, Math.abs(curv) * 25) : 0;
502
+ const finalR = finR * (1 - collapseAmt * 0.6);
503
+ const px = fp.x + fPerp.x * bodyW * side;
504
+ const py = fp.y + fPerp.y * bodyW * side;
505
+ const baseAngle = Math.atan2(
506
+ fPerp.y * side * 0.5 + fTang.y * 0.85,
507
+ fPerp.x * side * 0.5 + fTang.x * 0.85
508
+ );
509
+ ctx.beginPath();
510
+ ctx.moveTo(px, py);
511
+ const segments = 8;
512
+ for (let i = 0; i <= segments; i++) {
513
+ const t = i / segments;
514
+ const angle = baseAngle + (t - 0.5) * spread * 2;
515
+ const r = finalR * (0.85 + 0.15 * Math.sin(t * Math.PI));
516
+ ctx.lineTo(px + Math.cos(angle) * r, py + Math.sin(angle) * r);
517
+ }
518
+ ctx.closePath();
519
+ ctx.fill();
520
+ }
521
+ ctx.globalAlpha = alpha * 0.4;
522
+ ctx.fillStyle = "#333";
523
+ const eyeIdx = Math.max(1, Math.round((n - 1) * 0.04));
524
+ const ep = spine[eyeIdx];
525
+ const en = perp(spine, eyeIdx);
526
+ const eyeOff = widthAt(eyeIdx / (n - 1)) * maxHW * 0.45;
527
+ const eyeR = s * 0.025;
528
+ ctx.beginPath();
529
+ ctx.arc(ep.x + en.x * eyeOff, ep.y + en.y * eyeOff, eyeR, 0, Math.PI * 2);
530
+ ctx.fill();
531
+ ctx.beginPath();
532
+ ctx.arc(ep.x - en.x * eyeOff, ep.y - en.y * eyeOff, eyeR, 0, Math.PI * 2);
533
+ ctx.fill();
534
+ ctx.restore();
535
+ };
536
+ var createRenderer = (canvas, _config, alphaFn) => {
537
+ const ctx = canvas.getContext("2d");
538
+ let currentAlphaFn = alphaFn ?? null;
539
+ const render = (school, _time) => {
540
+ const { width, height } = canvas;
541
+ ctx.fillStyle = "#ffffff";
542
+ ctx.fillRect(0, 0, width, height);
543
+ const sorted = [...school].sort((a, b) => a.size - b.size);
544
+ for (const koi of sorted) {
545
+ const alpha = currentAlphaFn ? currentAlphaFn(koi) : 1;
546
+ drawKoi(ctx, koi, alpha);
547
+ }
548
+ };
549
+ const resize = (width, height) => {
550
+ canvas.width = width;
551
+ canvas.height = height;
552
+ };
553
+ const setAlphaFn = (fn) => {
554
+ currentAlphaFn = fn;
555
+ };
556
+ return { render, resize, setAlphaFn };
557
+ };
558
+
559
+ // src/pond.ts
560
+ var SCATTER_SPEED_THRESHOLD = 8;
561
+ var SCATTER_DECAY = 0.92;
562
+ var createKoiPond = (canvas, options = {}) => {
563
+ const {
564
+ count,
565
+ config: configOverrides,
566
+ alphaFn,
567
+ respectReducedMotion = true
568
+ } = options;
569
+ const config = {
570
+ ...defaultConfig,
571
+ width: canvas.clientWidth || canvas.width,
572
+ height: canvas.clientHeight || canvas.height,
573
+ ...count != null ? { count } : {},
574
+ ...configOverrides
575
+ };
576
+ canvas.width = config.width;
577
+ canvas.height = config.height;
578
+ let school = createSchool(config);
579
+ const renderer = createRenderer(canvas, config, alphaFn);
580
+ let mouseState = null;
581
+ let lastMousePos = null;
582
+ let mouseSpeed = 0;
583
+ let scatter = 0;
584
+ const onMouseMove = (e) => {
585
+ const pos = { x: e.clientX, y: e.clientY };
586
+ if (lastMousePos) {
587
+ const dx = pos.x - lastMousePos.x;
588
+ const dy = pos.y - lastMousePos.y;
589
+ const instantSpeed = Math.sqrt(dx * dx + dy * dy);
590
+ mouseSpeed = mouseSpeed * 0.7 + instantSpeed * 0.3;
591
+ if (mouseSpeed > SCATTER_SPEED_THRESHOLD) {
592
+ scatter = Math.min(1, scatter + 0.3);
593
+ }
594
+ }
595
+ lastMousePos = pos;
596
+ mouseState = { pos, speed: mouseSpeed, scatter };
597
+ };
598
+ const onMouseLeave = () => {
599
+ mouseState = null;
600
+ lastMousePos = null;
601
+ mouseSpeed = 0;
602
+ };
603
+ const onClick = () => {
604
+ scatter = 1;
605
+ if (mouseState) mouseState = { ...mouseState, scatter: 1 };
606
+ };
607
+ const onTouchMove = (e) => {
608
+ const touch = e.touches[0];
609
+ if (!touch) return;
610
+ const pos = { x: touch.clientX, y: touch.clientY };
611
+ if (lastMousePos) {
612
+ const dx = pos.x - lastMousePos.x;
613
+ const dy = pos.y - lastMousePos.y;
614
+ const instantSpeed = Math.sqrt(dx * dx + dy * dy);
615
+ mouseSpeed = mouseSpeed * 0.7 + instantSpeed * 0.3;
616
+ if (mouseSpeed > SCATTER_SPEED_THRESHOLD) {
617
+ scatter = Math.min(1, scatter + 0.3);
618
+ }
619
+ }
620
+ lastMousePos = pos;
621
+ mouseState = { pos, speed: mouseSpeed, scatter };
622
+ };
623
+ const onTouchStart = () => {
624
+ scatter = 1;
625
+ if (mouseState) mouseState = { ...mouseState, scatter: 1 };
626
+ };
627
+ const onTouchEnd = () => {
628
+ mouseState = null;
629
+ lastMousePos = null;
630
+ mouseSpeed = 0;
631
+ };
632
+ const onResize = () => {
633
+ config.width = canvas.clientWidth || window.innerWidth;
634
+ config.height = canvas.clientHeight || window.innerHeight;
635
+ renderer.resize(config.width, config.height);
636
+ };
637
+ let rafId = null;
638
+ let lastTime = 0;
639
+ let running = false;
640
+ const tick = (now) => {
641
+ if (!running) return;
642
+ const rawDt = (now - lastTime) / 1e3;
643
+ const dt = Math.min(rawDt, 0.1) * 60;
644
+ lastTime = now;
645
+ scatter *= SCATTER_DECAY;
646
+ if (mouseState) mouseState = { ...mouseState, scatter };
647
+ mouseSpeed *= 0.95;
648
+ school = stepSimulation(school, mouseState, config, dt);
649
+ renderer.render(school, now / 1e3);
650
+ rafId = requestAnimationFrame(tick);
651
+ };
652
+ let listenersAttached = false;
653
+ const attachListeners = () => {
654
+ if (listenersAttached) return;
655
+ window.addEventListener("mousemove", onMouseMove);
656
+ window.addEventListener("mouseleave", onMouseLeave);
657
+ window.addEventListener("click", onClick);
658
+ window.addEventListener("touchmove", onTouchMove, { passive: true });
659
+ window.addEventListener("touchstart", onTouchStart, { passive: true });
660
+ window.addEventListener("touchend", onTouchEnd);
661
+ window.addEventListener("resize", onResize);
662
+ listenersAttached = true;
663
+ };
664
+ const detachListeners = () => {
665
+ if (!listenersAttached) return;
666
+ window.removeEventListener("mousemove", onMouseMove);
667
+ window.removeEventListener("mouseleave", onMouseLeave);
668
+ window.removeEventListener("click", onClick);
669
+ window.removeEventListener("touchmove", onTouchMove);
670
+ window.removeEventListener("touchstart", onTouchStart);
671
+ window.removeEventListener("touchend", onTouchEnd);
672
+ window.removeEventListener("resize", onResize);
673
+ listenersAttached = false;
674
+ };
675
+ const start = () => {
676
+ if (respectReducedMotion) {
677
+ const prefersReduced = window.matchMedia(
678
+ "(prefers-reduced-motion: reduce)"
679
+ ).matches;
680
+ if (prefersReduced) {
681
+ renderer.render(school, 0);
682
+ return;
683
+ }
684
+ }
685
+ attachListeners();
686
+ running = true;
687
+ lastTime = performance.now();
688
+ rafId = requestAnimationFrame(tick);
689
+ };
690
+ const stop = () => {
691
+ running = false;
692
+ if (rafId != null) {
693
+ cancelAnimationFrame(rafId);
694
+ rafId = null;
695
+ }
696
+ };
697
+ const destroy = () => {
698
+ stop();
699
+ detachListeners();
700
+ };
701
+ return {
702
+ start,
703
+ stop,
704
+ destroy,
705
+ setAlphaFn: (fn) => renderer.setAlphaFn(fn),
706
+ school: () => school,
707
+ renderer
708
+ };
709
+ };
710
+ export {
711
+ BODY_LENGTH_MULT,
712
+ SPINE_POINTS,
713
+ createKoi,
714
+ createKoiPond,
715
+ createRenderer,
716
+ createSchool,
717
+ defaultConfig,
718
+ stepSimulation,
719
+ updateKoi
720
+ };
721
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/boids.ts","../src/renderer.ts","../src/pond.ts"],"sourcesContent":["export interface Vec2 {\n x: number;\n y: number;\n}\n\nexport interface MouseState {\n pos: Vec2;\n speed: number; // pixels per frame (smoothed)\n scatter: number; // 0-1, decays over time after click or fast movement\n}\n\n/** Number of spine segments per fish. */\nexport const SPINE_POINTS = 20;\n\n/** Body length = size * this multiplier. */\nexport const BODY_LENGTH_MULT = 2.4;\n\n/** A color splotch on a koi's body. */\nexport interface Patch {\n t: number; // 0-1 position along spine\n offset: number; // -1 to 1 lateral offset\n rx: number; // radius along body axis (fraction of body length)\n ry: number; // radius perpendicular (fraction of max width)\n}\n\nexport interface Koi {\n pos: Vec2;\n vel: Vec2;\n acc: Vec2;\n patchColor: string;\n patches: Patch[];\n size: number;\n spine: Vec2[]; // world-space chain, [0] = head\n swimPhase: number; // advances with distance traveled (drives head wiggle)\n orbiting: boolean; // true when in orbit mode around cursor\n}\n\nexport interface PondConfig {\n width: number;\n height: number;\n count: number;\n maxSpeed: number;\n maxForce: number;\n separationRadius: number;\n alignmentRadius: number;\n cohesionRadius: number;\n separationWeight: number;\n alignmentWeight: number;\n cohesionWeight: number;\n edgeMargin: number;\n edgeTurnForce: number;\n wanderStrength: number;\n mouseRadius: number;\n mouseWeight: number;\n maxTurnRate: number;\n}\n\nexport const defaultConfig: PondConfig = {\n width: 800,\n height: 600,\n count: 18,\n maxSpeed: 2.2,\n maxForce: 0.05,\n separationRadius: 70,\n alignmentRadius: 100,\n cohesionRadius: 120,\n separationWeight: 2.8,\n alignmentWeight: 1.0,\n cohesionWeight: 1.0,\n edgeMargin: 80,\n edgeTurnForce: 0.15,\n wanderStrength: 0.02,\n mouseRadius: 150,\n mouseWeight: 1.5,\n maxTurnRate: 0.045,\n};\n","// Flocking simulation for koi fish using Craig Reynolds' boids algorithm\n// Three core rules: separation, alignment, cohesion\n// Plus: boundary avoidance, mouse interaction, and gentle wandering\n//\n// Each koi carries a chain-spine: an array of world-space positions that\n// follow the head like links in a chain. This produces natural body curvature\n// from actual movement — turns bend the body, straight swimming straightens it.\n\nimport type { Koi, MouseState, Patch, PondConfig, Vec2 } from \"./types\";\nimport { BODY_LENGTH_MULT, SPINE_POINTS } from \"./types\";\n\n// Patch accent colors — these go on a white base\nconst PATCH_COLORS = [\n \"#c85040\", // hi (red-orange)\n \"#e08030\", // orenji (orange)\n \"#cc3333\", // aka (red)\n \"#2c3e50\", // sumi (ink black)\n \"#d4a050\", // yamabuki (gold)\n \"#8c5a3c\", // chagoi (brown)\n \"#b84040\", // beni (crimson)\n \"#404040\", // shiro-sumi (charcoal)\n];\n\n// --- Vector math (pure functions) ---\n\nconst vec = (x: number, y: number): Vec2 => ({ x, y });\nconst add = (a: Vec2, b: Vec2): Vec2 => vec(a.x + b.x, a.y + b.y);\nconst sub = (a: Vec2, b: Vec2): Vec2 => vec(a.x - b.x, a.y - b.y);\nconst scale = (v: Vec2, s: number): Vec2 => vec(v.x * s, v.y * s);\nconst mag = (v: Vec2): number => Math.sqrt(v.x * v.x + v.y * v.y);\nconst normalize = (v: Vec2): Vec2 => {\n const m = mag(v);\n return m > 0 ? scale(v, 1 / m) : vec(0, 0);\n};\n\nconst limit = (v: Vec2, max: number): Vec2 => {\n const m = mag(v);\n return m > max ? scale(normalize(v), max) : v;\n};\n\n// --- Koi creation ---\n\nconst randomInRange = (min: number, max: number): number =>\n Math.random() * (max - min) + min;\n\nconst pickRandom = <T>(arr: readonly T[]): T =>\n arr[Math.floor(Math.random() * arr.length)];\n\n// Segment length for a given koi size\nconst segLen = (size: number): number =>\n (size * BODY_LENGTH_MULT) / SPINE_POINTS;\n\n// Generate random splotch patches for a koi\nconst generatePatches = (): Patch[] => {\n const count = Math.floor(randomInRange(2, 6));\n const patches: Patch[] = [];\n for (let i = 0; i < count; i++) {\n patches.push({\n t: randomInRange(0.05, 0.85),\n offset: randomInRange(-0.6, 0.6),\n rx: randomInRange(0.06, 0.18),\n ry: randomInRange(0.5, 1.4),\n });\n }\n return patches;\n};\n\nexport const createKoi = (width: number, height: number): Koi => {\n const angle = Math.random() * Math.PI * 2;\n const speed = randomInRange(0.5, 1.5);\n const pos = vec(randomInRange(50, width - 50), randomInRange(50, height - 50));\n const dir = vec(Math.cos(angle), Math.sin(angle));\n const size = randomInRange(18, 32);\n const sl = segLen(size);\n\n // Straight line behind the head\n const spine = Array.from({ length: SPINE_POINTS + 1 }, (_, i) =>\n vec(pos.x - dir.x * sl * i, pos.y - dir.y * sl * i),\n );\n\n return {\n pos,\n vel: vec(dir.x * speed, dir.y * speed),\n acc: vec(0, 0),\n patchColor: pickRandom(PATCH_COLORS),\n patches: generatePatches(),\n size,\n spine,\n swimPhase: Math.random() * Math.PI * 2,\n orbiting: false,\n };\n};\n\nexport const createSchool = (config: PondConfig): Koi[] =>\n Array.from({ length: config.count }, () =>\n createKoi(config.width, config.height),\n );\n\n// --- Flocking forces (fused single-pass) ---\n\nconst distSq = (a: Vec2, b: Vec2): number => {\n const dx = a.x - b.x;\n const dy = a.y - b.y;\n return dx * dx + dy * dy;\n};\n\ninterface FlockForces {\n sep: Vec2;\n ali: Vec2;\n coh: Vec2;\n}\n\nconst computeFlockForces = (\n koi: Koi,\n school: Koi[],\n config: PondConfig,\n): FlockForces => {\n const { separationRadius, alignmentRadius, cohesionRadius, maxSpeed, maxForce } = config;\n // Pre-compute squared radii to avoid sqrt in the hot loop\n const sepR2 = separationRadius * separationRadius;\n const aliR2 = alignmentRadius * alignmentRadius;\n const cohR2 = cohesionRadius * cohesionRadius;\n // Use the largest radius to skip distant fish early\n const maxR2 = Math.max(sepR2, aliR2, cohR2);\n\n let sepSteer = vec(0, 0);\n let sepCount = 0;\n let aliAvg = vec(0, 0);\n let aliCount = 0;\n let cohCenter = vec(0, 0);\n let cohCount = 0;\n\n for (const other of school) {\n const d2 = distSq(koi.pos, other.pos);\n if (d2 === 0 || d2 >= maxR2) continue;\n\n if (d2 < sepR2) {\n const d = Math.sqrt(d2);\n const diff = scale(normalize(sub(koi.pos, other.pos)), 1 / d);\n sepSteer = add(sepSteer, diff);\n sepCount++;\n }\n if (d2 < aliR2) {\n aliAvg = add(aliAvg, other.vel);\n aliCount++;\n }\n if (d2 < cohR2) {\n cohCenter = add(cohCenter, other.pos);\n cohCount++;\n }\n }\n\n // Separation\n let sep = vec(0, 0);\n if (sepCount > 0) {\n sepSteer = scale(sepSteer, 1 / sepCount);\n sepSteer = scale(normalize(sepSteer), maxSpeed);\n sep = limit(sub(sepSteer, koi.vel), maxForce);\n }\n\n // Alignment\n let ali = vec(0, 0);\n if (aliCount > 0) {\n aliAvg = scale(aliAvg, 1 / aliCount);\n aliAvg = scale(normalize(aliAvg), maxSpeed);\n ali = limit(sub(aliAvg, koi.vel), maxForce);\n }\n\n // Cohesion\n let coh = vec(0, 0);\n if (cohCount > 0) {\n cohCenter = scale(cohCenter, 1 / cohCount);\n const desired = scale(normalize(sub(cohCenter, koi.pos)), maxSpeed);\n coh = limit(sub(desired, koi.vel), maxForce);\n }\n\n return { sep, ali, coh };\n};\n\nconst avoidEdges = (koi: Koi, config: PondConfig): Vec2 => {\n let steer = vec(0, 0);\n const { edgeMargin: m, edgeTurnForce: f, width: w, height: h } = config;\n if (koi.pos.x < m) steer = add(steer, vec(f, 0));\n if (koi.pos.x > w - m) steer = add(steer, vec(-f, 0));\n if (koi.pos.y < m) steer = add(steer, vec(0, f));\n if (koi.pos.y > h - m) steer = add(steer, vec(0, -f));\n return steer;\n};\n\nconst wander = (config: PondConfig): Vec2 =>\n vec(\n (Math.random() - 0.5) * config.wanderStrength,\n (Math.random() - 0.5) * config.wanderStrength,\n );\n\nconst SCATTER_SPEED_THRESHOLD = 8; // px/frame above this -> scatter\nconst ATTRACT_RADIUS = 300; // curious approach radius\nconst SCATTER_RADIUS = 250; // flee radius when scared\nconst ORBIT_ENTER = 80; // start orbiting when this close\nconst ORBIT_EXIT = 150; // stop orbiting only when pushed this far\n\ninterface MouseResult {\n force: Vec2;\n orbiting: boolean;\n}\n\nconst mouseInteraction = (\n koi: Koi,\n mouse: MouseState | null,\n config: PondConfig,\n orbitCount: number,\n): MouseResult => {\n if (!mouse) return { force: vec(0, 0), orbiting: false };\n const d2 = distSq(koi.pos, mouse.pos);\n const d = Math.sqrt(d2);\n\n // Orbit zone expands with number of orbiting fish so they spread into rings\n const orbitGrow = orbitCount * 12;\n const enterR = ORBIT_ENTER + orbitGrow;\n const exitR = ORBIT_EXIT + orbitGrow;\n const attractR = Math.max(ATTRACT_RADIUS, exitR + 80);\n\n // Scatter mode: fast movement or click — also breaks orbit\n if (mouse.scatter > 0.2) {\n if (d > SCATTER_RADIUS || d === 0)\n return { force: vec(0, 0), orbiting: false };\n const away = normalize(sub(koi.pos, mouse.pos));\n const heading = normalize(koi.vel);\n const strength =\n config.maxSpeed * 3.0 * mouse.scatter * (1 - d / SCATTER_RADIUS);\n\n const dot = away.x * heading.x + away.y * heading.y;\n let fleeDir: Vec2;\n if (dot < 0.2) {\n const cross = heading.x * away.y - heading.y * away.x;\n const side = cross >= 0 ? 1 : -1;\n fleeDir = vec(-heading.y * side, heading.x * side);\n } else {\n fleeDir = away;\n }\n const flee = scale(fleeDir, strength);\n return {\n force: limit(sub(flee, koi.vel), config.maxForce * 6),\n orbiting: false,\n };\n }\n\n // Hysteresis: enter orbit at enterR, exit at exitR\n const inOrbit = koi.orbiting ? d < exitR : d < enterR;\n\n if (d > attractR) return { force: vec(0, 0), orbiting: false };\n const toward = normalize(sub(mouse.pos, koi.pos));\n\n if (inOrbit) {\n // Orbit: swim tangentially with gentle inward pull\n const tangent = vec(-toward.y, toward.x);\n const orbitSpeed = config.maxSpeed * 0.45;\n const steer = scale(tangent, orbitSpeed);\n const inward = scale(toward, config.maxSpeed * 0.15 * (d / exitR));\n return {\n force: limit(\n sub(add(steer, inward), koi.vel),\n config.maxForce * 1.5,\n ),\n orbiting: true,\n };\n }\n\n // Approaching from far: pull toward cursor\n const strength =\n config.maxSpeed * 0.9 * ((d - exitR) / (attractR - exitR));\n const steer = scale(toward, Math.max(0, strength));\n return {\n force: limit(sub(steer, koi.vel), config.maxForce * 2),\n orbiting: false,\n };\n};\n\n// --- Chain spine update ---\n// Each spine point follows the one in front at a fixed segment distance.\n// This is what makes the body curve through turns and straighten on straights.\n\nconst updateSpine = (\n headPos: Vec2,\n oldSpine: Vec2[],\n sl: number,\n): Vec2[] => {\n const spine = [headPos];\n for (let i = 1; i <= SPINE_POINTS; i++) {\n const prev = spine[i - 1];\n const cur = oldSpine[i] ?? oldSpine[oldSpine.length - 1];\n const d = sub(cur, prev);\n const dm = mag(d);\n // Constrain to exactly segLen behind the previous point\n spine.push(\n dm > 0 ? add(prev, scale(d, sl / dm)) : vec(prev.x - sl, prev.y),\n );\n }\n return spine;\n};\n\n// --- Simulation step ---\n\nexport const updateKoi = (\n koi: Koi,\n school: Koi[],\n mouse: MouseState | null,\n config: PondConfig,\n dt: number,\n orbitCount: number,\n): Koi => {\n const flock = computeFlockForces(koi, school, config);\n const sep = scale(flock.sep, config.separationWeight);\n const ali = scale(flock.ali, config.alignmentWeight);\n const coh = scale(flock.coh, config.cohesionWeight);\n const edge = avoidEdges(koi, config);\n const wan = wander(config);\n const mouseResult = mouseInteraction(koi, mouse, config, orbitCount);\n const mouseForce = scale(mouseResult.force, config.mouseWeight);\n\n const acc = [sep, ali, coh, edge, wan, mouseForce].reduce(add, vec(0, 0));\n\n // Speed cap depends on mode: normal cruising is slower, scatter/orbit use full speed\n const scatterAmt = mouse?.scatter ?? 0;\n const cruiseMax = config.maxSpeed * 0.6; // relaxed idle speed\n const effectiveMax =\n scatterAmt > 0.2\n ? config.maxSpeed * (1 + scatterAmt * 2.5)\n : mouseResult.orbiting\n ? config.maxSpeed\n : cruiseMax;\n const desiredVel = limit(add(koi.vel, scale(acc, dt)), effectiveMax);\n\n // Clamp heading change to maxTurnRate per step for a natural turning arc\n // Loosen turn rate during scatter or orbit so fish can redirect\n const isOrbiting = mouseResult.orbiting;\n const turnBoost = isOrbiting ? 2.5 : 1 + scatterAmt * 3;\n const curAngle = Math.atan2(koi.vel.y, koi.vel.x);\n const desiredAngle = Math.atan2(desiredVel.y, desiredVel.x);\n let angleDiff = desiredAngle - curAngle;\n angleDiff = ((angleDiff + Math.PI * 3) % (Math.PI * 2)) - Math.PI;\n const maxTurn = config.maxTurnRate * dt * turnBoost;\n const clampedAngle =\n curAngle + Math.max(-maxTurn, Math.min(maxTurn, angleDiff));\n // Blend speed: fast when accelerating or below cruise, slow when decelerating\n // Orbiting fish maintain at least orbit speed\n const curSpeed = mag(koi.vel);\n const desiredSpeed = mag(desiredVel);\n const minOrbitSpeed = isOrbiting ? config.maxSpeed * 0.4 : 0;\n const cruiseSpeed = config.maxSpeed * 0.5;\n const belowCruise = curSpeed < cruiseSpeed ? 0.08 : 0;\n const blendRate =\n desiredSpeed > curSpeed\n ? 0.06 +\n scatterAmt * 0.4 +\n belowCruise +\n (isOrbiting ? 0.12 : 0)\n : 0.03;\n const speed = Math.max(\n minOrbitSpeed,\n curSpeed + (desiredSpeed - curSpeed) * blendRate,\n );\n const newVel = vec(\n Math.cos(clampedAngle) * speed,\n Math.sin(clampedAngle) * speed,\n );\n\n const newPos = add(koi.pos, scale(newVel, dt));\n\n // Swim phase advances with distance traveled (~1 tail-beat per body length)\n const traveled = mag(scale(newVel, dt));\n const bodyLen = koi.size * BODY_LENGTH_MULT;\n const newSwimPhase =\n koi.swimPhase + (traveled * (Math.PI * 2)) / bodyLen;\n\n // Subtle lateral head wiggle perpendicular to heading, proportional to speed\n const speedRatio = speed / config.maxSpeed;\n const wiggleAmp = koi.size * 0.04 * speedRatio;\n const perpX = -Math.sin(clampedAngle);\n const perpY = Math.cos(clampedAngle);\n const headPos = vec(\n newPos.x + perpX * Math.sin(newSwimPhase) * wiggleAmp,\n newPos.y + perpY * Math.sin(newSwimPhase) * wiggleAmp,\n );\n\n const sl = segLen(koi.size);\n const newSpine = updateSpine(headPos, koi.spine, sl);\n\n return {\n ...koi,\n pos: newPos,\n vel: newVel,\n acc,\n spine: newSpine,\n swimPhase: newSwimPhase,\n orbiting: mouseResult.orbiting,\n };\n};\n\nexport const stepSimulation = (\n school: Koi[],\n mouse: MouseState | null,\n config: PondConfig,\n dt: number,\n): Koi[] => {\n const orbitCount = school.filter((k) => k.orbiting).length;\n return school.map((koi) =>\n updateKoi(koi, school, mouse, config, dt, orbitCount),\n );\n};\n","// Top-down koi renderer — white base body with colored splotch patches\n// Body outline comes from the world-space chain spine.\n\nimport type { Koi, PondConfig, Vec2 } from \"./types\";\nimport { BODY_LENGTH_MULT } from \"./types\";\n\n/** Optional per-fish alpha callback. Return 0-1 to control visibility. */\nexport type AlphaFn = (koi: Koi) => number;\n\n// --- Geometry helpers ---\n\nconst perp = (spine: Vec2[], i: number): Vec2 => {\n const a = spine[Math.max(0, i - 1)];\n const b = spine[Math.min(spine.length - 1, i + 1)];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n return { x: -dy / len, y: dx / len };\n};\n\nconst tang = (spine: Vec2[], i: number): Vec2 => {\n const a = spine[Math.max(0, i - 1)];\n const b = spine[Math.min(spine.length - 1, i + 1)];\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n return { x: dx / len, y: dy / len };\n};\n\n// Sleek width profile: widest ~25% back, narrow head, long taper\nconst widthAt = (t: number): number => {\n const peak = 0.22;\n if (t < peak) {\n const r = t / peak;\n return 0.3 + 0.7 * Math.sqrt(r); // starts narrow at nose, swells fast\n }\n return Math.pow(1 - (t - peak) / (1 - peak), 0.6);\n};\n\n// --- Build the body outline as a reusable Path2D ---\n\ninterface BodyOutline {\n path: Path2D;\n left: Vec2[];\n right: Vec2[];\n}\n\nconst buildBodyOutline = (spine: Vec2[], maxHW: number): BodyOutline => {\n const n = spine.length;\n const left: Vec2[] = [];\n const right: Vec2[] = [];\n for (let i = 0; i < n; i++) {\n const t = i / (n - 1);\n const p = spine[i];\n const pr = perp(spine, i);\n const hw = widthAt(t) * maxHW;\n left.push({ x: p.x + pr.x * hw, y: p.y + pr.y * hw });\n right.push({ x: p.x - pr.x * hw, y: p.y - pr.y * hw });\n }\n\n const path = new Path2D();\n path.moveTo(left[0].x, left[0].y);\n for (let i = 1; i < n; i++) {\n const p = left[i - 1];\n const c = left[i];\n path.quadraticCurveTo((p.x + c.x) / 2, (p.y + c.y) / 2, c.x, c.y);\n }\n path.lineTo(right[n - 1].x, right[n - 1].y);\n for (let i = n - 2; i >= 0; i--) {\n const p = right[i + 1];\n const c = right[i];\n path.quadraticCurveTo((p.x + c.x) / 2, (p.y + c.y) / 2, c.x, c.y);\n }\n path.closePath();\n\n return { path, left, right };\n};\n\n// --- Drawing ---\n\nconst drawKoi = (\n ctx: CanvasRenderingContext2D,\n koi: Koi,\n alpha: number,\n): void => {\n const spine = koi.spine;\n const n = spine.length;\n const s = koi.size;\n const maxHW = s * 0.28; // sleek but visible\n\n ctx.save();\n ctx.globalAlpha = alpha;\n\n // --- Tail ribbon (drawn first, behind body) ---\n // Flows with spine curvature for a dynamic, swishy look\n const tailStart = Math.round(n * 0.65);\n const tailSegments = n - tailStart;\n ctx.globalAlpha = alpha * 0.5;\n ctx.fillStyle = koi.patchColor || \"#c4a882\";\n ctx.beginPath();\n {\n const ribbonLeft: Vec2[] = [];\n const ribbonRight: Vec2[] = [];\n\n // Compute curvature at each tail segment for billowing\n for (let i = tailStart; i < n; i++) {\n const localT = (i - tailStart) / (tailSegments - 1);\n const p = spine[i];\n const pr = perp(spine, i);\n\n // Curvature: how much direction changes (cross product of adjacent tangents)\n let curvature = 0;\n if (i > 0 && i < n - 1) {\n const t0 = tang(spine, i - 1);\n const t1 = tang(spine, i);\n curvature = t0.x * t1.y - t0.y * t1.x; // positive = turning left\n }\n\n // Ribbon width fans out gently\n const ease = localT * localT * (3 - 2 * localT); // smoothstep\n const bodyW = widthAt(i / (n - 1)) * maxHW;\n const ribbonW = bodyW + ease * maxHW * 0.85;\n\n // Curvature collapses the inner side and only slightly pushes the outer\n const absCurv = Math.abs(curvature);\n const collapse = absCurv * s * 2.5 * ease; // inner side loses width\n const swell = curvature * s * 0.6 * ease; // slight outer push\n\n ribbonLeft.push({\n x:\n p.x +\n pr.x *\n Math.max(\n 0,\n ribbonW + swell - (curvature > 0 ? 0 : collapse),\n ),\n y:\n p.y +\n pr.y *\n Math.max(\n 0,\n ribbonW + swell - (curvature > 0 ? 0 : collapse),\n ),\n });\n ribbonRight.push({\n x:\n p.x -\n pr.x *\n Math.max(\n 0,\n ribbonW - swell - (curvature > 0 ? collapse : 0),\n ),\n y:\n p.y -\n pr.y *\n Math.max(\n 0,\n ribbonW - swell - (curvature > 0 ? collapse : 0),\n ),\n });\n }\n\n // Extend a soft trailing tip beyond spine end\n const lastT = tang(spine, n - 1);\n const lastP = perp(spine, n - 1);\n const tipLen = s * 0.3;\n const tipW = maxHW * 0.35;\n const endCurv = (() => {\n const t0 = tang(spine, n - 2);\n const t1 = tang(spine, n - 1);\n return (t0.x * t1.y - t0.y * t1.x) * s * 1.8;\n })();\n const tip = {\n x: spine[n - 1].x + lastT.x * tipLen,\n y: spine[n - 1].y + lastT.y * tipLen,\n };\n ribbonLeft.push({\n x: tip.x + lastP.x * (tipW + endCurv),\n y: tip.y + lastP.y * (tipW + endCurv),\n });\n ribbonRight.push({\n x: tip.x - lastP.x * (tipW - endCurv),\n y: tip.y - lastP.y * (tipW - endCurv),\n });\n\n // Draw smooth ribbon with cubic bezier through points\n ctx.moveTo(ribbonLeft[0].x, ribbonLeft[0].y);\n for (let i = 1; i < ribbonLeft.length; i++) {\n const prev = ribbonLeft[i - 1];\n const cur = ribbonLeft[i];\n const mx = (prev.x + cur.x) / 2;\n const my = (prev.y + cur.y) / 2;\n ctx.quadraticCurveTo(prev.x, prev.y, mx, my);\n }\n const lastL = ribbonLeft[ribbonLeft.length - 1];\n ctx.lineTo(lastL.x, lastL.y);\n\n // Smooth arc across the tip\n ctx.quadraticCurveTo(\n tip.x,\n tip.y,\n ribbonRight[ribbonRight.length - 1].x,\n ribbonRight[ribbonRight.length - 1].y,\n );\n\n // Back along right side\n for (let i = ribbonRight.length - 2; i >= 0; i--) {\n const prev = ribbonRight[i + 1];\n const cur = ribbonRight[i];\n const mx = (prev.x + cur.x) / 2;\n const my = (prev.y + cur.y) / 2;\n ctx.quadraticCurveTo(prev.x, prev.y, mx, my);\n }\n ctx.lineTo(ribbonRight[0].x, ribbonRight[0].y);\n ctx.closePath();\n ctx.fill();\n }\n\n ctx.globalAlpha = alpha;\n\n // --- Build body paths once, reuse for fill / clip / stroke ---\n const body = buildBodyOutline(spine, maxHW);\n const clipBody = buildBodyOutline(spine, maxHW * 0.92);\n\n // --- White base body ---\n ctx.fillStyle = \"#d0c0ac\"; // warm tan\n ctx.fill(body.path);\n\n // --- Splotch patches (clipped to slightly inset body to prevent bleed) ---\n ctx.save();\n ctx.clip(clipBody.path);\n\n ctx.fillStyle = koi.patchColor;\n for (const patch of koi.patches) {\n // Find the spine point and perpendicular at this t\n const idx = Math.round(patch.t * (n - 1));\n const p = spine[idx];\n const pr = perp(spine, idx);\n const tg = tang(spine, idx);\n const hw = widthAt(patch.t) * maxHW;\n\n // Patch center offset laterally\n const cx = p.x + pr.x * hw * patch.offset;\n const cy = p.y + pr.y * hw * patch.offset;\n\n // Ellipse radii\n const bodyLen = s * BODY_LENGTH_MULT;\n const rx = patch.rx * bodyLen;\n const ry = patch.ry * maxHW;\n\n // Rotation follows the spine tangent\n const angle = Math.atan2(tg.y, tg.x);\n\n ctx.beginPath();\n ctx.ellipse(cx, cy, rx, ry, angle, 0, Math.PI * 2);\n ctx.fill();\n }\n\n ctx.restore(); // un-clip\n\n // --- Thin outline for definition ---\n ctx.strokeStyle = \"rgba(0,0,0,0.12)\";\n ctx.lineWidth = 0.8;\n ctx.stroke(body.path);\n\n // --- Pectoral fins (~20% back, fan-shaped, ripple with swimPhase) ---\n const finIdx = Math.round((n - 1) * 0.2);\n ctx.globalAlpha = alpha * 0.5;\n ctx.fillStyle = koi.patchColor || \"#c4a882\";\n\n for (const side of [1, -1]) {\n const fp = spine[finIdx];\n const fPerp = perp(spine, finIdx);\n const fTang = tang(spine, finIdx);\n const bodyW = widthAt(finIdx / (n - 1)) * maxHW;\n\n // Ripple: fan spread oscillates with swim phase\n const spread =\n 0.5 + Math.sin(koi.swimPhase * 2.5 + side * 1.2) * 0.2; // 0.3-0.7 radians\n const finR = s * 0.5; // radius of the fan\n\n // Curvature collapse\n let curv = 0;\n if (finIdx > 0 && finIdx < n - 1) {\n const t0 = tang(spine, finIdx - 1);\n const t1 = tang(spine, finIdx);\n curv = t0.x * t1.y - t0.y * t1.x;\n }\n const isInnerSide =\n (side === 1 && curv > 0) || (side === -1 && curv < 0);\n const collapseAmt = isInnerSide\n ? Math.min(1, Math.abs(curv) * 25)\n : 0;\n const finalR = finR * (1 - collapseAmt * 0.6);\n\n // Fan pivot point on body edge\n const px = fp.x + fPerp.x * bodyW * side;\n const py = fp.y + fPerp.y * bodyW * side;\n\n // Base angle: mostly swept backward with some outward spread\n const baseAngle = Math.atan2(\n fPerp.y * side * 0.5 + fTang.y * 0.85,\n fPerp.x * side * 0.5 + fTang.x * 0.85,\n );\n\n // Draw fan arc\n ctx.beginPath();\n ctx.moveTo(px, py);\n const segments = 8;\n for (let i = 0; i <= segments; i++) {\n const t = i / segments;\n const angle = baseAngle + (t - 0.5) * spread * 2;\n const r = finalR * (0.85 + 0.15 * Math.sin(t * Math.PI)); // slight bulge in middle\n ctx.lineTo(px + Math.cos(angle) * r, py + Math.sin(angle) * r);\n }\n ctx.closePath();\n ctx.fill();\n }\n\n // --- Eyes ---\n ctx.globalAlpha = alpha * 0.4;\n ctx.fillStyle = \"#333\";\n const eyeIdx = Math.max(1, Math.round((n - 1) * 0.04));\n const ep = spine[eyeIdx];\n const en = perp(spine, eyeIdx);\n const eyeOff = widthAt(eyeIdx / (n - 1)) * maxHW * 0.45;\n const eyeR = s * 0.025;\n ctx.beginPath();\n ctx.arc(ep.x + en.x * eyeOff, ep.y + en.y * eyeOff, eyeR, 0, Math.PI * 2);\n ctx.fill();\n ctx.beginPath();\n ctx.arc(ep.x - en.x * eyeOff, ep.y - en.y * eyeOff, eyeR, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.restore();\n};\n\n// --- Public renderer ---\n\nexport interface Renderer {\n render: (school: Koi[], time: number) => void;\n resize: (width: number, height: number) => void;\n setAlphaFn: (fn: AlphaFn | null) => void;\n}\n\nexport const createRenderer = (\n canvas: HTMLCanvasElement,\n _config: PondConfig,\n alphaFn?: AlphaFn,\n): Renderer => {\n const ctx = canvas.getContext(\"2d\")!;\n let currentAlphaFn: AlphaFn | null = alphaFn ?? null;\n\n const render = (school: Koi[], _time: number): void => {\n const { width, height } = canvas;\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, width, height);\n\n const sorted = [...school].sort((a, b) => a.size - b.size);\n for (const koi of sorted) {\n const alpha = currentAlphaFn ? currentAlphaFn(koi) : 1;\n drawKoi(ctx, koi, alpha);\n }\n };\n\n const resize = (width: number, height: number): void => {\n canvas.width = width;\n canvas.height = height;\n };\n\n const setAlphaFn = (fn: AlphaFn | null): void => {\n currentAlphaFn = fn;\n };\n\n return { render, resize, setAlphaFn };\n};\n","// High-level \"batteries-included\" API — drop a koi pond on any canvas.\n\nimport { createSchool, stepSimulation } from \"./boids\";\nimport { createRenderer, type AlphaFn, type Renderer } from \"./renderer\";\nimport type { Koi, MouseState, PondConfig, Vec2 } from \"./types\";\nimport { defaultConfig } from \"./types\";\n\nexport interface KoiPondOptions {\n /** Number of fish. Defaults to 18. */\n count?: number;\n /** Override any PondConfig values. */\n config?: Partial<PondConfig>;\n /** Per-fish alpha callback for fading fish in certain regions. */\n alphaFn?: AlphaFn;\n /** Respect prefers-reduced-motion. Defaults to true. */\n respectReducedMotion?: boolean;\n}\n\nexport interface KoiPondHandle {\n /** Start the animation loop and attach event listeners. */\n start: () => void;\n /** Pause the animation (listeners stay attached). */\n stop: () => void;\n /** Stop animation and remove all event listeners. */\n destroy: () => void;\n /** Update the alpha function at runtime. */\n setAlphaFn: (fn: AlphaFn | null) => void;\n /** Access the current school of fish (read-only snapshot). */\n school: () => readonly Koi[];\n /** Access the renderer. */\n renderer: Renderer;\n}\n\nconst SCATTER_SPEED_THRESHOLD = 8;\nconst SCATTER_DECAY = 0.92;\n\nexport const createKoiPond = (\n canvas: HTMLCanvasElement,\n options: KoiPondOptions = {},\n): KoiPondHandle => {\n const {\n count,\n config: configOverrides,\n alphaFn,\n respectReducedMotion = true,\n } = options;\n\n const config: PondConfig = {\n ...defaultConfig,\n width: canvas.clientWidth || canvas.width,\n height: canvas.clientHeight || canvas.height,\n ...(count != null ? { count } : {}),\n ...configOverrides,\n };\n\n canvas.width = config.width;\n canvas.height = config.height;\n\n let school = createSchool(config);\n const renderer = createRenderer(canvas, config, alphaFn);\n\n // --- Mouse tracking state ---\n let mouseState: MouseState | null = null;\n let lastMousePos: Vec2 | null = null;\n let mouseSpeed = 0;\n let scatter = 0;\n\n // --- Event handlers ---\n const onMouseMove = (e: MouseEvent): void => {\n const pos = { x: e.clientX, y: e.clientY };\n if (lastMousePos) {\n const dx = pos.x - lastMousePos.x;\n const dy = pos.y - lastMousePos.y;\n const instantSpeed = Math.sqrt(dx * dx + dy * dy);\n mouseSpeed = mouseSpeed * 0.7 + instantSpeed * 0.3;\n if (mouseSpeed > SCATTER_SPEED_THRESHOLD) {\n scatter = Math.min(1, scatter + 0.3);\n }\n }\n lastMousePos = pos;\n mouseState = { pos, speed: mouseSpeed, scatter };\n };\n\n const onMouseLeave = (): void => {\n mouseState = null;\n lastMousePos = null;\n mouseSpeed = 0;\n };\n\n const onClick = (): void => {\n scatter = 1;\n if (mouseState) mouseState = { ...mouseState, scatter: 1 };\n };\n\n const onTouchMove = (e: TouchEvent): void => {\n const touch = e.touches[0];\n if (!touch) return;\n const pos = { x: touch.clientX, y: touch.clientY };\n if (lastMousePos) {\n const dx = pos.x - lastMousePos.x;\n const dy = pos.y - lastMousePos.y;\n const instantSpeed = Math.sqrt(dx * dx + dy * dy);\n mouseSpeed = mouseSpeed * 0.7 + instantSpeed * 0.3;\n if (mouseSpeed > SCATTER_SPEED_THRESHOLD) {\n scatter = Math.min(1, scatter + 0.3);\n }\n }\n lastMousePos = pos;\n mouseState = { pos, speed: mouseSpeed, scatter };\n };\n\n const onTouchStart = (): void => {\n scatter = 1;\n if (mouseState) mouseState = { ...mouseState, scatter: 1 };\n };\n\n const onTouchEnd = (): void => {\n mouseState = null;\n lastMousePos = null;\n mouseSpeed = 0;\n };\n\n const onResize = (): void => {\n config.width = canvas.clientWidth || window.innerWidth;\n config.height = canvas.clientHeight || window.innerHeight;\n renderer.resize(config.width, config.height);\n };\n\n // --- Animation loop ---\n let rafId: number | null = null;\n let lastTime = 0;\n let running = false;\n\n const tick = (now: number): void => {\n if (!running) return;\n const rawDt = (now - lastTime) / 1000;\n const dt = Math.min(rawDt, 0.1) * 60;\n lastTime = now;\n\n // Decay scatter each frame\n scatter *= SCATTER_DECAY;\n if (mouseState) mouseState = { ...mouseState, scatter };\n mouseSpeed *= 0.95;\n\n school = stepSimulation(school, mouseState, config, dt);\n renderer.render(school, now / 1000);\n\n rafId = requestAnimationFrame(tick);\n };\n\n // --- Listener management ---\n let listenersAttached = false;\n\n const attachListeners = (): void => {\n if (listenersAttached) return;\n window.addEventListener(\"mousemove\", onMouseMove);\n window.addEventListener(\"mouseleave\", onMouseLeave);\n window.addEventListener(\"click\", onClick);\n window.addEventListener(\"touchmove\", onTouchMove, { passive: true });\n window.addEventListener(\"touchstart\", onTouchStart, { passive: true });\n window.addEventListener(\"touchend\", onTouchEnd);\n window.addEventListener(\"resize\", onResize);\n listenersAttached = true;\n };\n\n const detachListeners = (): void => {\n if (!listenersAttached) return;\n window.removeEventListener(\"mousemove\", onMouseMove);\n window.removeEventListener(\"mouseleave\", onMouseLeave);\n window.removeEventListener(\"click\", onClick);\n window.removeEventListener(\"touchmove\", onTouchMove);\n window.removeEventListener(\"touchstart\", onTouchStart);\n window.removeEventListener(\"touchend\", onTouchEnd);\n window.removeEventListener(\"resize\", onResize);\n listenersAttached = false;\n };\n\n // --- Public API ---\n const start = (): void => {\n if (respectReducedMotion) {\n const prefersReduced = window.matchMedia(\n \"(prefers-reduced-motion: reduce)\",\n ).matches;\n if (prefersReduced) {\n renderer.render(school, 0);\n return;\n }\n }\n\n attachListeners();\n running = true;\n lastTime = performance.now();\n rafId = requestAnimationFrame(tick);\n };\n\n const stop = (): void => {\n running = false;\n if (rafId != null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n };\n\n const destroy = (): void => {\n stop();\n detachListeners();\n };\n\n return {\n start,\n stop,\n destroy,\n setAlphaFn: (fn) => renderer.setAlphaFn(fn),\n school: () => school,\n renderer,\n };\n};\n"],"mappings":"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","names":["strength","steer"]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "koi-pond",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Interactive koi fish pond simulation using boids flocking — drop it on any canvas",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "typecheck": "tsc --noEmit",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "devDependencies": {
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.0.0"
27
+ },
28
+ "keywords": [
29
+ "koi",
30
+ "boids",
31
+ "flocking",
32
+ "canvas",
33
+ "simulation",
34
+ "fish",
35
+ "interactive"
36
+ ]
37
+ }