@jjlmoya/utils-science 1.26.0 → 1.28.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/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/entropy-second-law/bibliography.astro +14 -0
- package/src/tool/entropy-second-law/bibliography.ts +12 -0
- package/src/tool/entropy-second-law/component.astro +366 -0
- package/src/tool/entropy-second-law/entropy-second-law-simulator.css +445 -0
- package/src/tool/entropy-second-law/entry.ts +26 -0
- package/src/tool/entropy-second-law/i18n/de.ts +210 -0
- package/src/tool/entropy-second-law/i18n/en.ts +210 -0
- package/src/tool/entropy-second-law/i18n/es.ts +210 -0
- package/src/tool/entropy-second-law/i18n/fr.ts +210 -0
- package/src/tool/entropy-second-law/i18n/id.ts +210 -0
- package/src/tool/entropy-second-law/i18n/it.ts +210 -0
- package/src/tool/entropy-second-law/i18n/ja.ts +210 -0
- package/src/tool/entropy-second-law/i18n/ko.ts +210 -0
- package/src/tool/entropy-second-law/i18n/nl.ts +210 -0
- package/src/tool/entropy-second-law/i18n/pl.ts +210 -0
- package/src/tool/entropy-second-law/i18n/pt.ts +210 -0
- package/src/tool/entropy-second-law/i18n/ru.ts +210 -0
- package/src/tool/entropy-second-law/i18n/sv.ts +210 -0
- package/src/tool/entropy-second-law/i18n/tr.ts +210 -0
- package/src/tool/entropy-second-law/i18n/zh.ts +210 -0
- package/src/tool/entropy-second-law/index.ts +11 -0
- package/src/tool/entropy-second-law/logic.ts +208 -0
- package/src/tool/entropy-second-law/seo.astro +15 -0
- package/src/tool/natural-selection-drift/bibliography.astro +14 -0
- package/src/tool/natural-selection-drift/bibliography.ts +16 -0
- package/src/tool/natural-selection-drift/component.astro +104 -0
- package/src/tool/natural-selection-drift/entry.ts +29 -0
- package/src/tool/natural-selection-drift/i18n/de.ts +65 -0
- package/src/tool/natural-selection-drift/i18n/en.ts +180 -0
- package/src/tool/natural-selection-drift/i18n/es.ts +64 -0
- package/src/tool/natural-selection-drift/i18n/fr.ts +204 -0
- package/src/tool/natural-selection-drift/i18n/id.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/it.ts +203 -0
- package/src/tool/natural-selection-drift/i18n/ja.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/ko.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/nl.ts +53 -0
- package/src/tool/natural-selection-drift/i18n/pl.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/pt.ts +52 -0
- package/src/tool/natural-selection-drift/i18n/ru.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/sv.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/tr.ts +48 -0
- package/src/tool/natural-selection-drift/i18n/zh.ts +48 -0
- package/src/tool/natural-selection-drift/index.ts +9 -0
- package/src/tool/natural-selection-drift/logic.ts +114 -0
- package/src/tool/natural-selection-drift/natural-selection-drift.css +429 -0
- package/src/tool/natural-selection-drift/render.ts +219 -0
- package/src/tool/natural-selection-drift/runtime.ts +89 -0
- package/src/tool/natural-selection-drift/seo.astro +15 -0
- package/src/tool/natural-selection-drift/simulation.ts +161 -0
- package/src/tools.ts +4 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { runSelectionDrift, summarizeOutcome } from './logic';
|
|
2
|
+
import { drawBackground, drawParticles, evolveParticles } from './render';
|
|
3
|
+
import {
|
|
4
|
+
readConfig,
|
|
5
|
+
seedParticles,
|
|
6
|
+
showActiveValue,
|
|
7
|
+
updateMetrics,
|
|
8
|
+
updateSliderValues,
|
|
9
|
+
type RuntimeElements,
|
|
10
|
+
} from './simulation';
|
|
11
|
+
|
|
12
|
+
function bindSliders(elements: RuntimeElements, runSimulation: () => void) {
|
|
13
|
+
const sliders = [
|
|
14
|
+
[elements.populationInput, elements.populationValue],
|
|
15
|
+
[elements.generationsInput, elements.generationsValue],
|
|
16
|
+
[elements.mutationInput, elements.mutationValue],
|
|
17
|
+
[elements.pressureInput, elements.pressureValue],
|
|
18
|
+
[elements.driftInput, elements.driftValue],
|
|
19
|
+
[elements.allelesInput, elements.allelesValue],
|
|
20
|
+
[elements.innovationInput, elements.innovationValue],
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
sliders.forEach(([input, valueNode]) => {
|
|
24
|
+
if (!input || !valueNode) return;
|
|
25
|
+
input.addEventListener('pointerdown', () => showActiveValue(input, valueNode));
|
|
26
|
+
input.addEventListener('input', () => {
|
|
27
|
+
updateSliderValues(elements);
|
|
28
|
+
runSimulation();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function startLoop(
|
|
34
|
+
canvas: HTMLCanvasElement,
|
|
35
|
+
ctx: CanvasRenderingContext2D,
|
|
36
|
+
elements: RuntimeElements,
|
|
37
|
+
state: { particles: never[]; outcome: { diversity: number } | null; runSeed: number }
|
|
38
|
+
) {
|
|
39
|
+
let frameCount = 0;
|
|
40
|
+
const frame = () => {
|
|
41
|
+
frameCount += 1;
|
|
42
|
+
const dpr = window.devicePixelRatio || 1;
|
|
43
|
+
const rect = canvas.getBoundingClientRect();
|
|
44
|
+
canvas.width = rect.width * dpr;
|
|
45
|
+
canvas.height = rect.height * dpr;
|
|
46
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
47
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
48
|
+
const config = readConfig(elements, state.runSeed);
|
|
49
|
+
drawBackground(ctx, rect, config.selectionPressure);
|
|
50
|
+
if (state.outcome) state.particles = evolveParticles(state.particles, config, state.outcome) as never[];
|
|
51
|
+
drawParticles(ctx, rect, state.particles, { pressure: config.selectionPressure, drift: config.driftIntensity });
|
|
52
|
+
if (frameCount % 6 === 0) updateMetrics(elements, state.particles);
|
|
53
|
+
requestAnimationFrame(frame);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
requestAnimationFrame(frame);
|
|
57
|
+
return frame;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createNaturalSelectionRuntime(elements: RuntimeElements) {
|
|
61
|
+
const canvas = elements.canvas;
|
|
62
|
+
const ctx = canvas?.getContext('2d');
|
|
63
|
+
if (!canvas || !ctx) return () => {};
|
|
64
|
+
|
|
65
|
+
let runSeed = 0;
|
|
66
|
+
let particles = [];
|
|
67
|
+
let outcome = null;
|
|
68
|
+
const state = { particles, outcome, runSeed };
|
|
69
|
+
|
|
70
|
+
const runSimulation = () => {
|
|
71
|
+
runSeed += 1;
|
|
72
|
+
updateSliderValues(elements);
|
|
73
|
+
const config = readConfig(elements, runSeed);
|
|
74
|
+
const points = runSelectionDrift(config);
|
|
75
|
+
outcome = summarizeOutcome(points);
|
|
76
|
+
particles = seedParticles(config, outcome);
|
|
77
|
+
state.particles = particles;
|
|
78
|
+
state.outcome = outcome;
|
|
79
|
+
state.runSeed = runSeed;
|
|
80
|
+
updateMetrics(elements, particles);
|
|
81
|
+
};
|
|
82
|
+
bindSliders(elements, runSimulation);
|
|
83
|
+
|
|
84
|
+
window.addEventListener('resize', runSimulation);
|
|
85
|
+
runSimulation();
|
|
86
|
+
startLoop(canvas, ctx, elements, state);
|
|
87
|
+
|
|
88
|
+
return () => window.removeEventListener('resize', runSimulation);
|
|
89
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { naturalSelectionDrift } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await naturalSelectionDrift.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { summarizeOutcome } from './logic';
|
|
2
|
+
|
|
3
|
+
export type RuntimeElements = {
|
|
4
|
+
populationInput: HTMLInputElement | null;
|
|
5
|
+
generationsInput: HTMLInputElement | null;
|
|
6
|
+
mutationInput: HTMLInputElement | null;
|
|
7
|
+
pressureInput: HTMLInputElement | null;
|
|
8
|
+
driftInput: HTMLInputElement | null;
|
|
9
|
+
allelesInput: HTMLInputElement | null;
|
|
10
|
+
innovationInput: HTMLInputElement | null;
|
|
11
|
+
populationValue: HTMLElement | null;
|
|
12
|
+
generationsValue: HTMLElement | null;
|
|
13
|
+
mutationValue: HTMLElement | null;
|
|
14
|
+
pressureValue: HTMLElement | null;
|
|
15
|
+
driftValue: HTMLElement | null;
|
|
16
|
+
allelesValue: HTMLElement | null;
|
|
17
|
+
innovationValue: HTMLElement | null;
|
|
18
|
+
aliveBadge: HTMLElement | null;
|
|
19
|
+
dominantTrait: HTMLElement | null;
|
|
20
|
+
fitnessScore: HTMLElement | null;
|
|
21
|
+
diversityScore: HTMLElement | null;
|
|
22
|
+
populationScore: HTMLElement | null;
|
|
23
|
+
alleleRanking: HTMLElement | null;
|
|
24
|
+
ui: {
|
|
25
|
+
aliveLabel: string;
|
|
26
|
+
alleleDefault: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type Particle = {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
vx: number;
|
|
34
|
+
vy: number;
|
|
35
|
+
size: number;
|
|
36
|
+
allele: number;
|
|
37
|
+
alive: boolean;
|
|
38
|
+
hue: number;
|
|
39
|
+
energy: number;
|
|
40
|
+
bias: number;
|
|
41
|
+
age: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type Outcome = ReturnType<typeof summarizeOutcome>;
|
|
45
|
+
type SimConfig = ReturnType<typeof readConfig>;
|
|
46
|
+
|
|
47
|
+
const hues = [160, 208, 284, 28, 338, 118, 246, 52];
|
|
48
|
+
const palette = ['allele-1', 'allele-2', 'allele-3', 'allele-4', 'allele-5', 'allele-6', 'allele-7', 'allele-8'];
|
|
49
|
+
|
|
50
|
+
const readValue = (input: HTMLInputElement | null, fallback: number) => Number(input?.value || fallback);
|
|
51
|
+
const setText = (node: HTMLElement | null, value: string) => {
|
|
52
|
+
if (node) node.textContent = value;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function readConfig(elements: RuntimeElements, runSeed: number) {
|
|
56
|
+
return {
|
|
57
|
+
population: readValue(elements.populationInput, 120),
|
|
58
|
+
generations: readValue(elements.generationsInput, 60),
|
|
59
|
+
mutationRate: readValue(elements.mutationInput, 12) / 100,
|
|
60
|
+
selectionPressure: readValue(elements.pressureInput, 55) / 100,
|
|
61
|
+
driftIntensity: readValue(elements.driftInput, 28) / 100,
|
|
62
|
+
alleleCount: readValue(elements.allelesInput, 3),
|
|
63
|
+
innovationRate: readValue(elements.innovationInput, 2) / 100,
|
|
64
|
+
seed: runSeed,
|
|
65
|
+
habitat: 'forest' as const,
|
|
66
|
+
trait: 'camouflage' as const,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function updateSliderValues(elements: RuntimeElements) {
|
|
71
|
+
setText(elements.populationValue, String(readValue(elements.populationInput, 120)));
|
|
72
|
+
setText(elements.generationsValue, String(readValue(elements.generationsInput, 60)));
|
|
73
|
+
setText(elements.mutationValue, `${readValue(elements.mutationInput, 12)}%`);
|
|
74
|
+
setText(elements.pressureValue, `${readValue(elements.pressureInput, 55)}%`);
|
|
75
|
+
setText(elements.driftValue, `${readValue(elements.driftInput, 28)}%`);
|
|
76
|
+
setText(elements.allelesValue, String(readValue(elements.allelesInput, 3)));
|
|
77
|
+
setText(elements.innovationValue, `${readValue(elements.innovationInput, 2)}%`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function showActiveValue(input: HTMLInputElement | null, valueNode: HTMLElement | null) {
|
|
81
|
+
if (!input || !valueNode) return;
|
|
82
|
+
valueNode.classList.add('is-visible');
|
|
83
|
+
const update = () => {
|
|
84
|
+
const range = Number(input.max) - Number(input.min);
|
|
85
|
+
const value = Number(input.value) - Number(input.min);
|
|
86
|
+
valueNode.style.left = `${range > 0 ? (value / range) * 100 : 0}%`;
|
|
87
|
+
};
|
|
88
|
+
update();
|
|
89
|
+
input.addEventListener('input', update, { once: true });
|
|
90
|
+
input.addEventListener('pointerup', () => valueNode.classList.remove('is-visible'), { once: true });
|
|
91
|
+
input.addEventListener('blur', () => valueNode.classList.remove('is-visible'), { once: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function seedParticles(config: SimConfig, result: Outcome) {
|
|
95
|
+
const total = Math.max(48, Math.min(180, Math.round(config.population / 3)));
|
|
96
|
+
const dominantBias = result.dominant === 'alleleA' ? 0.56 : 0.38;
|
|
97
|
+
const alleleCount = Math.max(2, Number(config.alleleCount || 3));
|
|
98
|
+
return Array.from({ length: total }, (_, index) => {
|
|
99
|
+
const allele = index % alleleCount;
|
|
100
|
+
const angle = Math.random() * Math.PI * 2;
|
|
101
|
+
const radius = 0.08 + Math.random() * 0.48;
|
|
102
|
+
return {
|
|
103
|
+
x: 0.5 + Math.cos(angle) * radius,
|
|
104
|
+
y: 0.5 + Math.sin(angle) * radius,
|
|
105
|
+
vx: (Math.random() - 0.5) * 0.003,
|
|
106
|
+
vy: (Math.random() - 0.5) * 0.003,
|
|
107
|
+
size: 1.8 + Math.random() * 3.2,
|
|
108
|
+
allele,
|
|
109
|
+
alive: true,
|
|
110
|
+
hue: hues[allele % hues.length],
|
|
111
|
+
energy: 0.45 + Math.random() * 0.55,
|
|
112
|
+
bias: dominantBias,
|
|
113
|
+
age: Math.random() * 20,
|
|
114
|
+
} satisfies Particle;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildCountMap(particles: Particle[]) {
|
|
119
|
+
const counts = new Map<number, number>();
|
|
120
|
+
particles.forEach((p) => {
|
|
121
|
+
if (!p.alive) return;
|
|
122
|
+
counts.set(p.allele, (counts.get(p.allele) || 0) + 1);
|
|
123
|
+
});
|
|
124
|
+
return counts;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function updateRanking(elements: RuntimeElements, particles: Particle[]) {
|
|
128
|
+
const counts = buildCountMap(particles);
|
|
129
|
+
const ranking = Array.from(counts.entries())
|
|
130
|
+
.map(([allele, count]) => ({ allele, count }))
|
|
131
|
+
.sort((a, b) => b.count - a.count);
|
|
132
|
+
setText(elements.dominantTrait, ranking.length > 0 ? `Allele ${ranking[0].allele + 1}` : elements.ui.alleleDefault);
|
|
133
|
+
const maxCount = Math.max(1, ...ranking.map((item) => item.count));
|
|
134
|
+
if (elements.alleleRanking) {
|
|
135
|
+
elements.alleleRanking.innerHTML = ranking
|
|
136
|
+
.map((item, index) => {
|
|
137
|
+
const fill = Math.max(0.1, item.count / maxCount);
|
|
138
|
+
const colorIndex = item.allele % palette.length;
|
|
139
|
+
return `<div class="ns-allele-chip ${palette[colorIndex]} is-updated" style="--rank-scale:${fill}" data-allele="${item.allele}">
|
|
140
|
+
<span>#${index + 1} Allele ${item.allele + 1}</span>
|
|
141
|
+
<strong>${item.count}</strong>
|
|
142
|
+
<div class="ns-allele-bar"><i></i></div>
|
|
143
|
+
</div>`;
|
|
144
|
+
})
|
|
145
|
+
.join('');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function updateMetrics(elements: RuntimeElements, particles: Particle[]) {
|
|
150
|
+
const aliveCount = particles.filter((p) => p.alive).length;
|
|
151
|
+
setText(elements.aliveBadge, `${aliveCount} ${elements.ui.aliveLabel}`);
|
|
152
|
+
setText(elements.populationScore, String(aliveCount));
|
|
153
|
+
const liveRatio = particles.length ? Math.round((aliveCount / particles.length) * 100) : 0;
|
|
154
|
+
setText(elements.fitnessScore, `${liveRatio}%`);
|
|
155
|
+
const counts = buildCountMap(particles);
|
|
156
|
+
const values = Array.from(counts.values());
|
|
157
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
158
|
+
const spread = total > 0 && values.length > 0 ? 1 - Math.max(...values) / total : 0;
|
|
159
|
+
setText(elements.diversityScore, `${Math.max(0, Math.round(spread * 100))}%`);
|
|
160
|
+
updateRanking(elements, particles);
|
|
161
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { TEMPERATURE_TIMELINE_TOOL } from './tool/temperature-timeline/index';
|
|
|
10
10
|
import { LORENZ_ATTRACTOR_TOOL } from './tool/lorenz-attractor/index';
|
|
11
11
|
import { STELLAR_HABITABILITY_ZONE_TOOL } from './tool/stellar-habitability-zone/index';
|
|
12
12
|
import { RADIOACTIVE_DECAY_TOOL } from './tool/radioactive-decay/index';
|
|
13
|
+
import { NATURAL_SELECTION_DRIFT_TOOL } from './tool/natural-selection-drift/index';
|
|
14
|
+
import { ENTROPY_SECOND_LAW_TOOL } from './tool/entropy-second-law/index';
|
|
13
15
|
|
|
14
16
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
15
17
|
COLONY_COUNTER_TOOL,
|
|
@@ -22,5 +24,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
22
24
|
LORENZ_ATTRACTOR_TOOL,
|
|
23
25
|
STELLAR_HABITABILITY_ZONE_TOOL,
|
|
24
26
|
RADIOACTIVE_DECAY_TOOL,
|
|
27
|
+
NATURAL_SELECTION_DRIFT_TOOL,
|
|
28
|
+
ENTROPY_SECOND_LAW_TOOL,
|
|
25
29
|
];
|
|
26
|
-
|