@jjlmoya/utils-science 1.25.0 → 1.27.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.
Files changed (57) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -1
  5. package/src/tests/locale_completeness.test.ts +2 -3
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/natural-selection-drift/bibliography.astro +14 -0
  8. package/src/tool/natural-selection-drift/bibliography.ts +16 -0
  9. package/src/tool/natural-selection-drift/component.astro +104 -0
  10. package/src/tool/natural-selection-drift/entry.ts +29 -0
  11. package/src/tool/natural-selection-drift/i18n/de.ts +65 -0
  12. package/src/tool/natural-selection-drift/i18n/en.ts +180 -0
  13. package/src/tool/natural-selection-drift/i18n/es.ts +64 -0
  14. package/src/tool/natural-selection-drift/i18n/fr.ts +204 -0
  15. package/src/tool/natural-selection-drift/i18n/id.ts +48 -0
  16. package/src/tool/natural-selection-drift/i18n/it.ts +203 -0
  17. package/src/tool/natural-selection-drift/i18n/ja.ts +48 -0
  18. package/src/tool/natural-selection-drift/i18n/ko.ts +48 -0
  19. package/src/tool/natural-selection-drift/i18n/nl.ts +53 -0
  20. package/src/tool/natural-selection-drift/i18n/pl.ts +48 -0
  21. package/src/tool/natural-selection-drift/i18n/pt.ts +52 -0
  22. package/src/tool/natural-selection-drift/i18n/ru.ts +48 -0
  23. package/src/tool/natural-selection-drift/i18n/sv.ts +48 -0
  24. package/src/tool/natural-selection-drift/i18n/tr.ts +48 -0
  25. package/src/tool/natural-selection-drift/i18n/zh.ts +48 -0
  26. package/src/tool/natural-selection-drift/index.ts +9 -0
  27. package/src/tool/natural-selection-drift/logic.ts +114 -0
  28. package/src/tool/natural-selection-drift/natural-selection-drift.css +429 -0
  29. package/src/tool/natural-selection-drift/render.ts +219 -0
  30. package/src/tool/natural-selection-drift/runtime.ts +89 -0
  31. package/src/tool/natural-selection-drift/seo.astro +15 -0
  32. package/src/tool/natural-selection-drift/simulation.ts +161 -0
  33. package/src/tool/radioactive-decay/bibliography.astro +15 -0
  34. package/src/tool/radioactive-decay/bibliography.ts +17 -0
  35. package/src/tool/radioactive-decay/component.astro +346 -0
  36. package/src/tool/radioactive-decay/entry.ts +26 -0
  37. package/src/tool/radioactive-decay/i18n/de.ts +78 -0
  38. package/src/tool/radioactive-decay/i18n/en.ts +223 -0
  39. package/src/tool/radioactive-decay/i18n/es.ts +106 -0
  40. package/src/tool/radioactive-decay/i18n/fr.ts +78 -0
  41. package/src/tool/radioactive-decay/i18n/id.ts +66 -0
  42. package/src/tool/radioactive-decay/i18n/it.ts +79 -0
  43. package/src/tool/radioactive-decay/i18n/ja.ts +65 -0
  44. package/src/tool/radioactive-decay/i18n/ko.ts +65 -0
  45. package/src/tool/radioactive-decay/i18n/nl.ts +72 -0
  46. package/src/tool/radioactive-decay/i18n/pl.ts +65 -0
  47. package/src/tool/radioactive-decay/i18n/pt.ts +78 -0
  48. package/src/tool/radioactive-decay/i18n/ru.ts +66 -0
  49. package/src/tool/radioactive-decay/i18n/sv.ts +66 -0
  50. package/src/tool/radioactive-decay/i18n/tr.ts +66 -0
  51. package/src/tool/radioactive-decay/i18n/zh.ts +65 -0
  52. package/src/tool/radioactive-decay/index.ts +12 -0
  53. package/src/tool/radioactive-decay/logic.test.ts +20 -0
  54. package/src/tool/radioactive-decay/logic.ts +120 -0
  55. package/src/tool/radioactive-decay/radioactive-decay-half-life-calculator.css +435 -0
  56. package/src/tool/radioactive-decay/seo.astro +16 -0
  57. package/src/tools.ts +4 -2
@@ -0,0 +1,429 @@
1
+ .ns-app {
2
+ --ns-bg: #f7fafc;
3
+ --ns-text: #0f172a;
4
+ --ns-muted: rgba(15, 23, 42, 0.62);
5
+ --ns-panel: rgba(255, 255, 255, 0.34);
6
+ --ns-border: rgba(255, 255, 255, 0.18);
7
+ --ns-track: rgba(15, 23, 42, 0.12);
8
+ --ns-thumb: #fff;
9
+ --ns-accent: #16a34a;
10
+
11
+ padding: 1.25rem;
12
+ color: var(--ns-text);
13
+ }
14
+
15
+ .theme-light .ns-app {
16
+ --ns-bg: #f8fbff;
17
+ --ns-text: #0f172a;
18
+ --ns-muted: rgba(15, 23, 42, 0.58);
19
+ --ns-panel: rgba(255, 255, 255, 0.72);
20
+ --ns-border: rgba(148, 163, 184, 0.22);
21
+ --ns-track: rgba(15, 23, 42, 0.14);
22
+ --ns-thumb: #fff;
23
+ --ns-accent: #0f766e;
24
+ }
25
+
26
+ .theme-dark .ns-app {
27
+ --ns-bg: #020617;
28
+ --ns-text: #f8fafc;
29
+ --ns-muted: rgba(255, 255, 255, 0.7);
30
+ --ns-panel: rgba(255, 255, 255, 0.03);
31
+ --ns-border: rgba(255, 255, 255, 0.08);
32
+ --ns-track: rgba(255, 255, 255, 0.1);
33
+ --ns-thumb: #fff;
34
+ --ns-accent: #86efac;
35
+ }
36
+
37
+ .ns-card {
38
+ position: relative;
39
+ min-height: min(84vh, 920px);
40
+ overflow: hidden;
41
+ padding: clamp(16px, 4vw, 24px);
42
+ border-radius: 32px;
43
+ background:
44
+ radial-gradient(circle at 50% 50%, rgba(34, 197, 94, 0.08), transparent 34%),
45
+ radial-gradient(circle at 15% 10%, rgba(56, 189, 248, 0.09), transparent 24%),
46
+ var(--ns-bg);
47
+ border: 1px solid var(--ns-border);
48
+ box-shadow: 0 30px 90px rgba(2, 6, 23, 0.18);
49
+ }
50
+
51
+ .theme-light .ns-card {
52
+ background:
53
+ radial-gradient(circle at 50% 50%, rgba(45, 212, 191, 0.16), transparent 32%),
54
+ radial-gradient(circle at 15% 10%, rgba(59, 130, 246, 0.12), transparent 22%),
55
+ linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 248, 255, 0.94)),
56
+ var(--ns-bg);
57
+ box-shadow: 0 28px 80px rgba(148, 163, 184, 0.22);
58
+ }
59
+
60
+ .ns-canvas {
61
+ position: absolute;
62
+ inset: 0;
63
+ width: 100%;
64
+ height: 100%;
65
+ display: block;
66
+ z-index: 1;
67
+ }
68
+
69
+ .ns-canvas-badge {
70
+ position: absolute;
71
+ top: clamp(16px, 3vw, 24px);
72
+ right: clamp(16px, 3vw, 24px);
73
+ z-index: 2;
74
+ padding: 0.4rem 0.7rem;
75
+ border-radius: 999px;
76
+ background: rgba(2, 6, 23, 0.75);
77
+ border: 1px solid rgba(255, 255, 255, 0.12);
78
+ color: rgba(255, 255, 255, 0.92);
79
+ font-size: 11px;
80
+ font-weight: 700;
81
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
82
+ pointer-events: none;
83
+ }
84
+
85
+ .theme-dark .ns-canvas-badge {
86
+ background: rgba(255, 255, 255, 0.08);
87
+ }
88
+
89
+ .theme-light .ns-canvas-badge {
90
+ background: rgba(255, 255, 255, 0.88);
91
+ border-color: rgba(148, 163, 184, 0.2);
92
+ color: #0f172a;
93
+ text-shadow: none;
94
+ box-shadow: 0 10px 30px rgba(148, 163, 184, 0.16);
95
+ }
96
+
97
+ .ns-top-hud {
98
+ position: absolute;
99
+ left: max(360px, clamp(16px, 3vw, 24px));
100
+ right: clamp(120px, 18vw, 220px);
101
+ top: clamp(16px, 3vw, 24px);
102
+ z-index: 2;
103
+ display: grid;
104
+ grid-template-columns: repeat(2, minmax(0, 1fr));
105
+ gap: 1rem;
106
+ pointer-events: none;
107
+ }
108
+
109
+ .ns-top-hud-item {
110
+ background: transparent;
111
+ border: 0;
112
+ padding: 0;
113
+ }
114
+
115
+ .ns-top-hud-item span {
116
+ display: block;
117
+ color: var(--ns-muted);
118
+ font-size: 11px;
119
+ letter-spacing: 0.08em;
120
+ text-transform: uppercase;
121
+ }
122
+
123
+ .ns-top-hud-item strong {
124
+ display: block;
125
+ margin-top: 8px;
126
+ font-size: clamp(1.1rem, 1.6vw, 1.6rem);
127
+ line-height: 1;
128
+ color: var(--ns-text);
129
+ }
130
+
131
+ .ns-console {
132
+ position: absolute;
133
+ left: clamp(16px, 4vw, 24px);
134
+ top: clamp(16px, 4vw, 24px);
135
+ bottom: clamp(16px, 4vw, 24px);
136
+ z-index: 2;
137
+ width: min(340px, calc(100% - 2rem));
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 1rem;
141
+ padding: clamp(16px, 2.5vw, 24px);
142
+ border-radius: 24px;
143
+ background: rgba(0, 0, 0, 0.02);
144
+ border: 1px solid rgba(255, 255, 255, 0.08);
145
+ backdrop-filter: blur(16px);
146
+ }
147
+
148
+ .theme-dark .ns-console {
149
+ background: rgba(255, 255, 255, 0.03);
150
+ }
151
+
152
+ .theme-light .ns-console {
153
+ background: rgba(255, 255, 255, 0.84);
154
+ border-color: rgba(148, 163, 184, 0.18);
155
+ box-shadow: 0 18px 50px rgba(148, 163, 184, 0.14);
156
+ }
157
+
158
+ .ns-console-header h3 {
159
+ margin: 0.2rem 0 0;
160
+ font-size: 1.15rem;
161
+ line-height: 1.1;
162
+ }
163
+
164
+ .ns-console-kicker {
165
+ display: inline-block;
166
+ font-size: 0.72rem;
167
+ letter-spacing: 0.18em;
168
+ text-transform: uppercase;
169
+ color: var(--ns-muted);
170
+ }
171
+
172
+ .ns-slider-row {
173
+ position: relative;
174
+ display: grid;
175
+ gap: 0.45rem;
176
+ }
177
+
178
+ .ns-slider-row label {
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: space-between;
182
+ gap: 1rem;
183
+ font-size: 0.9rem;
184
+ font-weight: 600;
185
+ color: var(--ns-muted);
186
+ }
187
+
188
+ .ns-slider-value {
189
+ position: static;
190
+ padding: 0;
191
+ border-radius: 0;
192
+ background: transparent;
193
+ color: var(--ns-accent);
194
+ font-size: 0.78rem;
195
+ line-height: 1;
196
+ font-variant-numeric: tabular-nums;
197
+ opacity: 1;
198
+ transition: none;
199
+ pointer-events: none;
200
+ }
201
+
202
+ .theme-dark .ns-slider-value {
203
+ background: transparent;
204
+ color: var(--ns-accent);
205
+ }
206
+
207
+ .ns-slider-value.is-visible {
208
+ opacity: 1;
209
+ }
210
+
211
+ .ns-console input[type="range"] {
212
+ width: 100%;
213
+ appearance: none;
214
+ -webkit-appearance: none;
215
+ height: 18px;
216
+ background: transparent;
217
+ margin: 0;
218
+ }
219
+
220
+ .ns-console input[type="range"]::-webkit-slider-runnable-track {
221
+ height: 1px;
222
+ background: var(--ns-track);
223
+ border-radius: 999px;
224
+ }
225
+
226
+ .ns-console input[type="range"]::-moz-range-track {
227
+ height: 1px;
228
+ background: var(--ns-track);
229
+ border-radius: 999px;
230
+ }
231
+
232
+ .ns-console input[type="range"]::-webkit-slider-thumb {
233
+ -webkit-appearance: none;
234
+ appearance: none;
235
+ width: 16px;
236
+ height: 16px;
237
+ margin-top: -8px;
238
+ border-radius: 999px;
239
+ border: 0;
240
+ background: var(--ns-thumb);
241
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
242
+ }
243
+
244
+ .ns-console input[type="range"]::-moz-range-thumb {
245
+ width: 16px;
246
+ height: 16px;
247
+ border-radius: 999px;
248
+ border: 0;
249
+ background: var(--ns-thumb);
250
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
251
+ }
252
+
253
+ .ns-run-btn {
254
+ margin-top: auto;
255
+ height: 40px;
256
+ display: inline-flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ gap: 0.5rem;
260
+ border: 0;
261
+ border-radius: 999px;
262
+ padding: 0.9rem 1rem;
263
+ background: linear-gradient(135deg, rgba(22, 163, 74, 0.95), rgba(56, 189, 248, 0.92));
264
+ color: #001018;
265
+ font-weight: 800;
266
+ box-shadow: 0 16px 30px rgba(34, 197, 94, 0.16);
267
+ transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
268
+ }
269
+
270
+ .ns-run-btn:hover {
271
+ transform: translateY(-1px);
272
+ filter: saturate(1.06);
273
+ }
274
+
275
+ .ns-run-btn:active {
276
+ transform: translateY(1px) scale(0.99);
277
+ }
278
+
279
+ .ns-hud {
280
+ position: absolute;
281
+ left: max(360px, clamp(16px, 3vw, 24px));
282
+ right: clamp(16px, 3vw, 24px);
283
+ bottom: clamp(16px, 3vw, 24px);
284
+ z-index: 2;
285
+ display: grid;
286
+ grid-template-columns: repeat(2, minmax(0, 1fr));
287
+ gap: 1rem;
288
+ pointer-events: none;
289
+ }
290
+
291
+ .ns-hud-item {
292
+ padding: 0.5rem 0;
293
+ background: transparent;
294
+ border: 0;
295
+ }
296
+
297
+ .ns-hud-item span {
298
+ display: block;
299
+ color: var(--ns-muted);
300
+ font-size: 11px;
301
+ letter-spacing: 0.08em;
302
+ text-transform: uppercase;
303
+ }
304
+
305
+ .ns-hud-item strong {
306
+ display: block;
307
+ margin-top: 8px;
308
+ font-size: clamp(1.4rem, 2vw, 2.5rem);
309
+ line-height: 1;
310
+ color: var(--ns-text);
311
+ }
312
+
313
+ .ns-alleles {
314
+ position: absolute;
315
+ right: clamp(16px, 3vw, 24px);
316
+ top: clamp(72px, 8vw, 108px);
317
+ z-index: 2;
318
+ display: grid;
319
+ grid-template-columns: minmax(180px, 226px);
320
+ gap: 0.3rem;
321
+ pointer-events: none;
322
+ }
323
+
324
+ .ns-allele-chip {
325
+ --chip-accent: rgba(255, 255, 255, 0.24);
326
+ --rank-scale: 0.5;
327
+
328
+ display: grid;
329
+ grid-template-columns: 1fr auto;
330
+ align-items: center;
331
+ gap: 0.35rem;
332
+ padding: 0.36rem 0.5rem 0.36rem 0.78rem;
333
+ border-radius: 14px;
334
+ background: rgba(2, 6, 23, 0.42);
335
+ border: 1px solid rgba(255, 255, 255, 0.08);
336
+ backdrop-filter: blur(14px);
337
+ position: relative;
338
+ overflow: hidden;
339
+ transition: transform 0.18s ease, opacity 0.18s ease;
340
+ }
341
+
342
+ .ns-allele-chip::before {
343
+ content: "";
344
+ position: absolute;
345
+ inset: 0;
346
+ width: 4px;
347
+ background: var(--chip-accent);
348
+ }
349
+
350
+ .ns-allele-chip.allele-1 { --chip-accent: linear-gradient(180deg, #7dd3fc, #0284c7); }
351
+ .ns-allele-chip.allele-2 { --chip-accent: linear-gradient(180deg, #86efac, #16a34a); }
352
+ .ns-allele-chip.allele-3 { --chip-accent: linear-gradient(180deg, #f9a8d4, #db2777); }
353
+ .ns-allele-chip.allele-4 { --chip-accent: linear-gradient(180deg, #fde68a, #f59e0b); }
354
+ .ns-allele-chip.allele-5 { --chip-accent: linear-gradient(180deg, #c4b5fd, #7c3aed); }
355
+ .ns-allele-chip.allele-6 { --chip-accent: linear-gradient(180deg, #fdba74, #ea580c); }
356
+ .ns-allele-chip.allele-7 { --chip-accent: linear-gradient(180deg, #6ee7b7, #059669); }
357
+ .ns-allele-chip.allele-8 { --chip-accent: linear-gradient(180deg, #fca5a5, #ef4444); }
358
+
359
+ .ns-allele-chip.is-updated {
360
+ animation: chipPulse 180ms ease-out;
361
+ }
362
+
363
+ .theme-dark .ns-allele-chip {
364
+ background: rgba(255, 255, 255, 0.03);
365
+ }
366
+
367
+ .theme-light .ns-allele-chip {
368
+ background: rgba(255, 255, 255, 0.78);
369
+ border-color: rgba(148, 163, 184, 0.18);
370
+ box-shadow: 0 12px 28px rgba(148, 163, 184, 0.12);
371
+ }
372
+
373
+ .ns-allele-chip span {
374
+ font-size: 8px;
375
+ letter-spacing: 0.08em;
376
+ text-transform: uppercase;
377
+ color: var(--ns-muted);
378
+ }
379
+
380
+ .ns-allele-chip strong {
381
+ font-size: 0.92rem;
382
+ line-height: 1;
383
+ color: var(--ns-text);
384
+ text-shadow: 0 0 10px rgba(255,255,255,0.04);
385
+ justify-self: end;
386
+ }
387
+
388
+ .ns-allele-bar {
389
+ grid-column: 1 / -1;
390
+ height: 2px;
391
+ border-radius: 999px;
392
+ background: rgba(255,255,255,0.06);
393
+ overflow: hidden;
394
+ }
395
+
396
+ .ns-allele-bar i {
397
+ display: block;
398
+ width: calc(var(--rank-scale) * 100%);
399
+ height: 100%;
400
+ border-radius: inherit;
401
+ background: var(--chip-accent);
402
+ box-shadow: 0 0 18px rgba(255,255,255,0.18);
403
+ transition: width 180ms ease;
404
+ }
405
+
406
+ @keyframes chipPulse {
407
+ 0% { transform: translateY(0) scale(1); }
408
+ 50% { transform: translateY(-1px) scale(1.02); }
409
+ 100% { transform: translateY(0) scale(1); }
410
+ }
411
+
412
+ @media (min-width: 900px) {
413
+ .ns-console {
414
+ width: 320px;
415
+ }
416
+ }
417
+
418
+ @media (max-width: 840px) {
419
+ .ns-top-hud,
420
+ .ns-hud {
421
+ left: clamp(16px, 3vw, 24px);
422
+ right: clamp(16px, 3vw, 24px);
423
+ }
424
+
425
+ .ns-alleles {
426
+ top: 124px;
427
+ grid-template-columns: minmax(0, 1fr);
428
+ }
429
+ }
@@ -0,0 +1,219 @@
1
+ import type { Particle } from './simulation';
2
+
3
+ type SimConfig = {
4
+ population: number;
5
+ mutationRate: number;
6
+ selectionPressure: number;
7
+ driftIntensity: number;
8
+ alleleCount: number;
9
+ innovationRate: number;
10
+ };
11
+
12
+ type Outcome = { diversity: number };
13
+
14
+ const pickFactor = (allele: number) => {
15
+ if (allele === 0) return 1.1;
16
+ if (allele === 1) return 0.95;
17
+ return 0.8;
18
+ };
19
+
20
+ const pickFertility = (allele: number, base: number) => {
21
+ if (allele === 0) return base * 1.08;
22
+ if (allele === 1) return base;
23
+ return base * 0.96;
24
+ };
25
+ const pickChildAllele = (parentAllele: number, mutation: number, innovationRate: number, alleleCount: number) => {
26
+ const roll = Math.random();
27
+ if (roll < mutation * 0.6) return (parentAllele + 1 + Math.floor(Math.random() * 2)) % alleleCount;
28
+ if (roll < mutation * 0.6 + innovationRate) return alleleCount + Math.floor(Math.random() * 3);
29
+ return parentAllele;
30
+ };
31
+
32
+ function clampPosition(value: number) {
33
+ return Math.max(0, Math.min(1, value));
34
+ }
35
+
36
+ function updateNeighborCohesion(particle: Particle, neighbors: { x: number; y: number }[]) {
37
+ let cohesionX = 0;
38
+ let cohesionY = 0;
39
+ neighbors.forEach((other) => {
40
+ cohesionX += other.x - particle.x;
41
+ cohesionY += other.y - particle.y;
42
+ });
43
+ return { cohesionX, cohesionY };
44
+ }
45
+
46
+ function moveParticle(args: {
47
+ particle: Particle;
48
+ cohesionX: number;
49
+ cohesionY: number;
50
+ config: SimConfig;
51
+ index: number;
52
+ squeeze: number;
53
+ }) {
54
+ const { particle, cohesionX, cohesionY, config, index, squeeze } = args;
55
+ const centerPull = 0.00018 + config.selectionPressure * 0.0012;
56
+ const jitter = Math.sin(index + performance.now() * 0.0007) * 0.00025 + (Math.random() - 0.5) * 0.00045;
57
+ particle.vx += (0.5 - particle.x) * centerPull + jitter + cohesionX * (1 - Math.max(0.08, config.driftIntensity)) * 0.0011 + (Math.random() - 0.5) * config.driftIntensity * 0.0008;
58
+ particle.vy += (0.5 - particle.y) * centerPull + jitter + cohesionY * (1 - Math.max(0.08, config.driftIntensity)) * 0.0011 + (Math.random() - 0.5) * config.driftIntensity * 0.0008;
59
+ particle.vx *= 0.99;
60
+ particle.vy *= 0.99;
61
+ particle.x += particle.vx;
62
+ particle.y += particle.vy;
63
+
64
+ const dx = particle.x - 0.5;
65
+ const dy = particle.y - 0.5;
66
+ const distance = Math.sqrt(dx * dx + dy * dy);
67
+ if (distance > 0.52 * squeeze) {
68
+ const scale = (0.52 * squeeze) / distance;
69
+ particle.x = 0.5 + dx * scale;
70
+ particle.y = 0.5 + dy * scale;
71
+ particle.vx *= -0.25;
72
+ particle.vy *= -0.25;
73
+ }
74
+ }
75
+
76
+ function mutateAndCull(particle: Particle, config: SimConfig) {
77
+ if (Math.random() < config.mutationRate * 0.02) {
78
+ particle.allele = (particle.allele + 1 + Math.floor(Math.random() * 2)) % Math.max(2, Number(config.alleleCount || 3));
79
+ particle.vx += (Math.random() - 0.5) * 0.006;
80
+ particle.vy += (Math.random() - 0.5) * 0.006;
81
+ }
82
+ const agePressure = Math.min(1.35, 0.55 + particle.age * 0.01);
83
+ const killChance = config.selectionPressure * 0.00006 * agePressure * pickFactor(particle.allele);
84
+ if (Math.random() < killChance) {
85
+ particle.alive = false;
86
+ }
87
+ }
88
+
89
+ function gatherAlleleCentroids(particles: Particle[]) {
90
+ const groups = new Map<number, { x: number; y: number; count: number }>();
91
+ particles.forEach((particle) => {
92
+ if (!particle.alive) return;
93
+ const group = groups.get(particle.allele) || { x: 0, y: 0, count: 0 };
94
+ group.x += particle.x;
95
+ group.y += particle.y;
96
+ group.count += 1;
97
+ groups.set(particle.allele, group);
98
+ });
99
+ return groups;
100
+ }
101
+
102
+ function createBirthFromParent(parent: Particle, config: SimConfig, mutationPush: number) {
103
+ const childAllele = pickChildAllele(parent.allele, config.mutationRate, config.innovationRate, Math.max(2, Number(config.alleleCount || 3)));
104
+ return {
105
+ x: clampPosition(parent.x + (Math.random() - 0.5) * 0.02),
106
+ y: clampPosition(parent.y + (Math.random() - 0.5) * 0.02),
107
+ vx: parent.vx * 0.72 + (Math.random() - 0.5) * mutationPush * 0.008,
108
+ vy: parent.vy * 0.72 + (Math.random() - 0.5) * mutationPush * 0.008,
109
+ size: Math.max(1.1, parent.size * (0.9 + Math.random() * 0.18)),
110
+ allele: childAllele,
111
+ alive: true,
112
+ hue: parent.hue,
113
+ energy: Math.max(0.25, Math.min(0.95, parent.energy + (Math.random() - 0.5) * 0.2)),
114
+ bias: parent.bias,
115
+ age: 0,
116
+ } satisfies Particle;
117
+ }
118
+
119
+ function createFallbackBirth(parent: Particle) {
120
+ return {
121
+ x: clampPosition(parent.x + (Math.random() - 0.5) * 0.02),
122
+ y: clampPosition(parent.y + (Math.random() - 0.5) * 0.02),
123
+ vx: parent.vx * 0.7,
124
+ vy: parent.vy * 0.7,
125
+ size: Math.max(1.1, parent.size * 0.95),
126
+ allele: parent.allele,
127
+ alive: true,
128
+ hue: parent.hue,
129
+ energy: parent.energy,
130
+ bias: parent.bias,
131
+ age: 0,
132
+ } satisfies Particle;
133
+ }
134
+
135
+ function reproduce(particles: Particle[], config: SimConfig, diversityFactor: number) {
136
+ const survivors = particles.filter((particle) => particle.alive);
137
+ const mutationPush = Math.max(0.1, config.mutationRate * 1.6);
138
+ const reproductionRate = 0.035 + config.selectionPressure * 0.02 + diversityFactor * 0.012;
139
+ const minBirths = Math.max(1, Math.round(survivors.length * 0.06));
140
+ const targetBirths = Math.min(Math.max(2, minBirths), Math.max(6, Math.round(config.population * 0.08)));
141
+ const births: Particle[] = [];
142
+
143
+ for (let i = 0; i < survivors.length && births.length < targetBirths; i += 1) {
144
+ const parent = survivors[i];
145
+ if (Math.random() > pickFertility(parent.allele, reproductionRate)) continue;
146
+ births.push(createBirthFromParent(parent, config, mutationPush));
147
+ }
148
+
149
+ while (births.length < minBirths && survivors.length > 0) {
150
+ const parent = survivors[births.length % survivors.length];
151
+ births.push(createFallbackBirth(parent));
152
+ }
153
+
154
+ return births;
155
+ }
156
+
157
+ function trimPopulation(particles: Particle[], population: number) {
158
+ if (particles.length <= population * 1.25) return particles;
159
+ return particles.sort((a, b) => {
160
+ if (a.alive !== b.alive) return a.alive ? -1 : 1;
161
+ return a.age - b.age;
162
+ }).slice(-Math.round(population * 1.25));
163
+ }
164
+
165
+ export function evolveParticles(particles: Particle[], config: SimConfig, outcome: Outcome) {
166
+ const diversityFactor = Math.max(0.08, outcome.diversity / 100);
167
+ const squeeze = 1 - config.selectionPressure * 0.3;
168
+ const groups = gatherAlleleCentroids(particles);
169
+
170
+ particles.forEach((particle, index) => {
171
+ particle.age += 1;
172
+ if (!particle.alive) return;
173
+ const centroid = groups.get(particle.allele);
174
+ const neighbors = centroid && centroid.count > 1 ? [{ x: centroid.x / centroid.count, y: centroid.y / centroid.count }] : [];
175
+ const { cohesionX, cohesionY } = updateNeighborCohesion(particle, neighbors);
176
+ moveParticle({ particle, cohesionX, cohesionY, config, index, squeeze });
177
+ mutateAndCull(particle, config);
178
+ });
179
+
180
+ const births = reproduce(particles, config, diversityFactor);
181
+ if (births.length > 0) {
182
+ const limit = Math.min(births.length, Math.max(4, Math.round(config.population * 0.08)));
183
+ for (let i = 0; i < limit; i += 1) {
184
+ particles.push(births[i]);
185
+ }
186
+ }
187
+ return trimPopulation(particles, config.population);
188
+ }
189
+
190
+ export function drawBackground(ctx: CanvasRenderingContext2D, rect: DOMRect, pressure: number) {
191
+ const background = ctx.createRadialGradient(rect.width / 2, rect.height / 2, 40, rect.width / 2, rect.height / 2, rect.width * 0.65);
192
+ background.addColorStop(0, `rgba(56, 189, 248, ${0.08 + pressure * 0.12})`);
193
+ background.addColorStop(0.45, 'rgba(34, 197, 94, 0.08)');
194
+ background.addColorStop(1, 'rgba(2, 6, 23, 0.14)');
195
+ ctx.fillStyle = background;
196
+ ctx.fillRect(0, 0, rect.width, rect.height);
197
+ const centerGlow = ctx.createRadialGradient(rect.width / 2, rect.height / 2, 20, rect.width / 2, rect.height / 2, rect.width * 0.34);
198
+ centerGlow.addColorStop(0, `rgba(255,255,255,${0.02 + pressure * 0.04})`);
199
+ centerGlow.addColorStop(1, 'rgba(255,255,255,0)');
200
+ ctx.fillStyle = centerGlow;
201
+ ctx.fillRect(0, 0, rect.width, rect.height);
202
+ }
203
+
204
+ export function drawParticles(ctx: CanvasRenderingContext2D, rect: DOMRect, particles: Particle[], atmosphere: { pressure: number; drift: number }) {
205
+ const { pressure, drift } = atmosphere;
206
+ particles.forEach((particle) => {
207
+ if (!particle.alive) return;
208
+ const x = particle.x * rect.width;
209
+ const y = particle.y * rect.height;
210
+ const radius = particle.size * (0.75 + pressure * 0.35);
211
+ ctx.beginPath();
212
+ ctx.fillStyle = `hsla(${particle.hue}, 86%, ${62 + particle.energy * 14}%, ${0.55 + particle.energy * 0.35})`;
213
+ ctx.shadowBlur = 10 + drift * 24;
214
+ ctx.shadowColor = `hsla(${particle.hue}, 86%, 66%, 0.22)`;
215
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
216
+ ctx.fill();
217
+ });
218
+ ctx.shadowBlur = 0;
219
+ }