@jjlmoya/utils-science 1.21.0 → 1.23.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 +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/lorenz-attractor/bibliography.astro +14 -0
- package/src/tool/lorenz-attractor/bibliography.ts +12 -0
- package/src/tool/lorenz-attractor/component.astro +146 -0
- package/src/tool/lorenz-attractor/entry.ts +27 -0
- package/src/tool/lorenz-attractor/i18n/de.ts +141 -0
- package/src/tool/lorenz-attractor/i18n/en.ts +185 -0
- package/src/tool/lorenz-attractor/i18n/es.ts +141 -0
- package/src/tool/lorenz-attractor/i18n/fr.ts +141 -0
- package/src/tool/lorenz-attractor/i18n/id.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/it.ts +140 -0
- package/src/tool/lorenz-attractor/i18n/ja.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/ko.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/nl.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/pl.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/pt.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/ru.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/sv.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/tr.ts +139 -0
- package/src/tool/lorenz-attractor/i18n/zh.ts +139 -0
- package/src/tool/lorenz-attractor/index.ts +9 -0
- package/src/tool/lorenz-attractor/logic/LorenzEngine.ts +32 -0
- package/src/tool/lorenz-attractor/lorenz-attractor.css +453 -0
- package/src/tool/lorenz-attractor/renderer.ts +136 -0
- package/src/tool/lorenz-attractor/script.ts +283 -0
- package/src/tool/lorenz-attractor/seo.astro +15 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Point3D } from './logic/LorenzEngine';
|
|
2
|
+
|
|
3
|
+
export interface RenderContext {
|
|
4
|
+
ctx: CanvasRenderingContext2D;
|
|
5
|
+
w: number;
|
|
6
|
+
h: number;
|
|
7
|
+
rx: number;
|
|
8
|
+
ry: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getCssVariable(name: string, fallback: string): string {
|
|
12
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function project(p: Point3D, rx: number, ry: number, dims: { w: number; h: number }): { x: number; y: number } {
|
|
16
|
+
const scale = Math.min(dims.w, dims.h) * 0.0135;
|
|
17
|
+
const cx = dims.w / 2;
|
|
18
|
+
const cy = dims.h / 2;
|
|
19
|
+
const cosX = Math.cos(rx);
|
|
20
|
+
const sinX = Math.sin(rx);
|
|
21
|
+
const cosY = Math.cos(ry);
|
|
22
|
+
const sinY = Math.sin(ry);
|
|
23
|
+
const px = p.x;
|
|
24
|
+
const py = p.y;
|
|
25
|
+
const pz = p.z - 25;
|
|
26
|
+
const x1 = px * cosY - pz * sinY;
|
|
27
|
+
const z1 = px * sinY + pz * cosY;
|
|
28
|
+
const y2 = py * cosX - z1 * sinX;
|
|
29
|
+
return {
|
|
30
|
+
x: cx + x1 * scale,
|
|
31
|
+
y: cy - y2 * scale,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function drawGridLine(ctx: CanvasRenderingContext2D, p1: Point3D, p2: Point3D, opts: { rx: number; ry: number; w: number; h: number }) {
|
|
36
|
+
const dist1 = Math.sqrt(p1.x * p1.x + p1.y * p1.y);
|
|
37
|
+
const dist2 = Math.sqrt(p2.x * p2.x + p2.y * p2.y);
|
|
38
|
+
const avgDist = (dist1 + dist2) / 2;
|
|
39
|
+
const alpha = Math.max(0, 0.22 - avgDist / 120);
|
|
40
|
+
if (alpha <= 0) return;
|
|
41
|
+
ctx.strokeStyle = `rgba(100, 116, 139, ${alpha})`;
|
|
42
|
+
ctx.beginPath();
|
|
43
|
+
const pt1 = project(p1, opts.rx, opts.ry, opts);
|
|
44
|
+
const pt2 = project(p2, opts.rx, opts.ry, opts);
|
|
45
|
+
ctx.moveTo(pt1.x, pt1.y);
|
|
46
|
+
ctx.lineTo(pt2.x, pt2.y);
|
|
47
|
+
ctx.stroke();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function drawGrid(config: RenderContext) {
|
|
51
|
+
const step = 8;
|
|
52
|
+
const limit = 40;
|
|
53
|
+
const opts = { rx: config.rx, ry: config.ry, w: config.w, h: config.h };
|
|
54
|
+
config.ctx.lineWidth = 1 * window.devicePixelRatio;
|
|
55
|
+
for (let x = -limit; x <= limit; x += step) {
|
|
56
|
+
for (let y = -limit; y < limit; y += step) {
|
|
57
|
+
drawGridLine(config.ctx, { x, y, z: 0 }, { x, y: y + step, z: 0 }, opts);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (let y = -limit; y <= limit; y += step) {
|
|
61
|
+
for (let x = -limit; x < limit; x += step) {
|
|
62
|
+
drawGridLine(config.ctx, { x, y, z: 0 }, { x: x + step, y, z: 0 }, opts);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function drawShadowPath(config: RenderContext, history: Point3D[]) {
|
|
68
|
+
if (history.length < 2) return;
|
|
69
|
+
const { ctx, w, h, rx, ry } = config;
|
|
70
|
+
const dims = { w, h };
|
|
71
|
+
ctx.strokeStyle = getCssVariable('--lorenz-shadow-color', 'rgba(0, 0, 0, 0.05)');
|
|
72
|
+
ctx.lineWidth = 1.5 * window.devicePixelRatio;
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
const p0 = project({ x: history[0].x, y: history[0].y, z: 0 }, rx, ry, dims);
|
|
75
|
+
ctx.moveTo(p0.x, p0.y);
|
|
76
|
+
for (let i = 1; i < history.length; i++) {
|
|
77
|
+
const p = project({ x: history[i].x, y: history[i].y, z: 0 }, rx, ry, dims);
|
|
78
|
+
ctx.lineTo(p.x, p.y);
|
|
79
|
+
}
|
|
80
|
+
ctx.stroke();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function drawPath(config: RenderContext, history: Point3D[], color: string) {
|
|
84
|
+
if (history.length < 2) return;
|
|
85
|
+
const { ctx, w, h, rx, ry } = config;
|
|
86
|
+
const dims = { w, h };
|
|
87
|
+
ctx.strokeStyle = color;
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
const p0 = project(history[0], rx, ry, dims);
|
|
90
|
+
ctx.moveTo(p0.x, p0.y);
|
|
91
|
+
for (let i = 1; i < history.length; i++) {
|
|
92
|
+
const p = project(history[i], rx, ry, dims);
|
|
93
|
+
ctx.lineTo(p.x, p.y);
|
|
94
|
+
}
|
|
95
|
+
ctx.stroke();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function drawHead(config: RenderContext, p: Point3D, color: string) {
|
|
99
|
+
const { ctx, w, h, rx, ry } = config;
|
|
100
|
+
const pt = project(p, rx, ry, { w, h });
|
|
101
|
+
ctx.fillStyle = color;
|
|
102
|
+
ctx.beginPath();
|
|
103
|
+
ctx.arc(pt.x, pt.y, 5 * window.devicePixelRatio, 0, Math.PI * 2);
|
|
104
|
+
ctx.fill();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function drawAxes(config: RenderContext) {
|
|
108
|
+
const { ctx, h, rx, ry } = config;
|
|
109
|
+
const ox = 50 * window.devicePixelRatio;
|
|
110
|
+
const oy = h - 50 * window.devicePixelRatio;
|
|
111
|
+
const axisLen = 20 * window.devicePixelRatio;
|
|
112
|
+
const cosX = Math.cos(rx);
|
|
113
|
+
const sinX = Math.sin(rx);
|
|
114
|
+
const cosY = Math.cos(ry);
|
|
115
|
+
const sinY = Math.sin(ry);
|
|
116
|
+
const projectVector = (vx: number, vy: number, vz: number) => {
|
|
117
|
+
const x1 = vx * cosY - vz * sinY;
|
|
118
|
+
const z1 = vx * sinY + vz * cosY;
|
|
119
|
+
const y2 = vy * cosX - z1 * sinX;
|
|
120
|
+
return { dx: x1 * axisLen, dy: -y2 * axisLen };
|
|
121
|
+
};
|
|
122
|
+
ctx.lineWidth = 1 * window.devicePixelRatio;
|
|
123
|
+
ctx.font = `${Math.round(9 * window.devicePixelRatio)}px var(--lorenz-font-mono)`;
|
|
124
|
+
const drawAxisLine = (ax: { dx: number; dy: number }, label: string, color: string) => {
|
|
125
|
+
ctx.strokeStyle = color;
|
|
126
|
+
ctx.fillStyle = color;
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.moveTo(ox, oy);
|
|
129
|
+
ctx.lineTo(ox + ax.dx, oy + ax.dy);
|
|
130
|
+
ctx.stroke();
|
|
131
|
+
ctx.fillText(label, ox + ax.dx + 4, oy + ax.dy + 3);
|
|
132
|
+
};
|
|
133
|
+
drawAxisLine(projectVector(1, 0, 0), 'X', 'rgba(239, 68, 68, 0.4)');
|
|
134
|
+
drawAxisLine(projectVector(0, 1, 0), 'Y', 'rgba(34, 197, 94, 0.4)');
|
|
135
|
+
drawAxisLine(projectVector(0, 0, 1), 'Z', 'rgba(59, 130, 246, 0.4)');
|
|
136
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { LorenzEngine } from './logic/LorenzEngine';
|
|
2
|
+
import type { Point3D, LorenzParams } from './logic/LorenzEngine';
|
|
3
|
+
import {
|
|
4
|
+
getCssVariable,
|
|
5
|
+
drawGrid,
|
|
6
|
+
drawShadowPath,
|
|
7
|
+
drawPath,
|
|
8
|
+
drawHead,
|
|
9
|
+
drawAxes,
|
|
10
|
+
} from './renderer';
|
|
11
|
+
|
|
12
|
+
const canvas = document.getElementById('lorenzCanvas') as HTMLCanvasElement;
|
|
13
|
+
const ctx = canvas?.getContext('2d');
|
|
14
|
+
const chartCanvas = document.getElementById('chartCanvas') as HTMLCanvasElement;
|
|
15
|
+
const chartCtx = chartCanvas?.getContext('2d');
|
|
16
|
+
|
|
17
|
+
const getEl = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
|
|
18
|
+
const slideSigma = getEl<HTMLInputElement>('slideSigma');
|
|
19
|
+
const slideRho = getEl<HTMLInputElement>('slideRho');
|
|
20
|
+
const slideBeta = getEl<HTMLInputElement>('slideBeta');
|
|
21
|
+
const slideDt = getEl<HTMLInputElement>('slideDt');
|
|
22
|
+
const slidePerturb = getEl<HTMLInputElement>('slidePerturb');
|
|
23
|
+
|
|
24
|
+
const valSigma = getEl('valSigma');
|
|
25
|
+
const valRho = getEl('valRho');
|
|
26
|
+
const valBeta = getEl('valBeta');
|
|
27
|
+
const valDt = getEl('valDt');
|
|
28
|
+
const valPerturb = getEl('valPerturb');
|
|
29
|
+
|
|
30
|
+
const btnPlayPause = getEl<HTMLButtonElement>('btnPlayPause');
|
|
31
|
+
const btnReset = getEl<HTMLButtonElement>('btnReset');
|
|
32
|
+
const btnClear = getEl<HTMLButtonElement>('btnClear');
|
|
33
|
+
|
|
34
|
+
const statT1 = getEl('statT1');
|
|
35
|
+
const statT2 = getEl('statT2');
|
|
36
|
+
const statDist = getEl('statDist');
|
|
37
|
+
const statDelta = getEl('statDelta');
|
|
38
|
+
|
|
39
|
+
let isRunning = true;
|
|
40
|
+
let t1: Point3D = { x: 10.0, y: 10.0, z: 10.0 };
|
|
41
|
+
let t2: Point3D = { x: 10.0001, y: 10.0, z: 10.0 };
|
|
42
|
+
let t1History: Point3D[] = [];
|
|
43
|
+
let t2History: Point3D[] = [];
|
|
44
|
+
let distanceHistory: number[] = [];
|
|
45
|
+
let lastDistance = 0.0001;
|
|
46
|
+
|
|
47
|
+
const maxHistory = 1000;
|
|
48
|
+
let rx = 0.6;
|
|
49
|
+
let ry = 0.45;
|
|
50
|
+
let isDragging = false;
|
|
51
|
+
let previousMousePosition = { x: 0, y: 0 };
|
|
52
|
+
|
|
53
|
+
function getParams(): LorenzParams {
|
|
54
|
+
return {
|
|
55
|
+
sigma: parseFloat(slideSigma.value),
|
|
56
|
+
rho: parseFloat(slideRho.value),
|
|
57
|
+
beta: parseFloat(slideBeta.value),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getDt(): number {
|
|
62
|
+
return parseFloat(slideDt.value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getPerturbation(): number {
|
|
66
|
+
return parseFloat(slidePerturb.value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function updateDisplays() {
|
|
70
|
+
if (valSigma) valSigma.textContent = parseFloat(slideSigma.value).toFixed(1);
|
|
71
|
+
if (valRho) valRho.textContent = parseFloat(slideRho.value).toFixed(1);
|
|
72
|
+
if (valBeta) valBeta.textContent = parseFloat(slideBeta.value).toFixed(2);
|
|
73
|
+
if (valDt) valDt.textContent = parseFloat(slideDt.value).toFixed(3);
|
|
74
|
+
if (valPerturb) valPerturb.textContent = parseFloat(slidePerturb.value).toFixed(5);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resetToDefaults() {
|
|
78
|
+
slideSigma.value = '10.0';
|
|
79
|
+
slideRho.value = '28.0';
|
|
80
|
+
slideBeta.value = '2.67';
|
|
81
|
+
slideDt.value = '0.005';
|
|
82
|
+
slidePerturb.value = '0.00010';
|
|
83
|
+
updateDisplays();
|
|
84
|
+
clearPaths();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearPaths() {
|
|
88
|
+
const perturbation = getPerturbation();
|
|
89
|
+
t1 = { x: 10.0, y: 10.0, z: 10.0 };
|
|
90
|
+
t2 = { x: 10.0 + perturbation, y: 10.0, z: 10.0 };
|
|
91
|
+
t1History = [];
|
|
92
|
+
t2History = [];
|
|
93
|
+
distanceHistory = [];
|
|
94
|
+
lastDistance = perturbation;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resize(c: HTMLCanvasElement | null, h: number) {
|
|
98
|
+
if (!c || !c.parentElement) return;
|
|
99
|
+
const rect = c.parentElement.getBoundingClientRect();
|
|
100
|
+
c.width = rect.width * window.devicePixelRatio;
|
|
101
|
+
c.height = h * window.devicePixelRatio;
|
|
102
|
+
c.style.width = '100%';
|
|
103
|
+
c.style.height = `${h}px`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resizeAll() {
|
|
107
|
+
if (canvas && canvas.parentElement) {
|
|
108
|
+
resize(canvas, canvas.parentElement.getBoundingClientRect().height);
|
|
109
|
+
}
|
|
110
|
+
const isMobile = window.innerWidth <= 991;
|
|
111
|
+
resize(chartCanvas, isMobile ? 56 : 60);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function drawAttractor() {
|
|
115
|
+
if (!canvas || !ctx) return;
|
|
116
|
+
const config = { ctx, w: canvas.width, h: canvas.height, rx, ry };
|
|
117
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
118
|
+
drawGrid(config);
|
|
119
|
+
drawShadowPath(config, t1History);
|
|
120
|
+
drawShadowPath(config, t2History);
|
|
121
|
+
ctx.lineWidth = 1.8 * window.devicePixelRatio;
|
|
122
|
+
ctx.lineCap = 'round';
|
|
123
|
+
ctx.lineJoin = 'round';
|
|
124
|
+
const t1Color = getCssVariable('--lorenz-t1', '#00f0ff');
|
|
125
|
+
const t2Color = getCssVariable('--lorenz-t2', '#ff007f');
|
|
126
|
+
drawPath(config, t1History, t1Color);
|
|
127
|
+
drawPath(config, t2History, t2Color);
|
|
128
|
+
ctx.shadowBlur = 8 * window.devicePixelRatio;
|
|
129
|
+
ctx.shadowColor = t1Color;
|
|
130
|
+
drawHead(config, t1, t1Color);
|
|
131
|
+
ctx.shadowColor = t2Color;
|
|
132
|
+
drawHead(config, t2, t2Color);
|
|
133
|
+
ctx.shadowBlur = 0;
|
|
134
|
+
drawAxes(config);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function drawChartBaseline(w: number, h: number) {
|
|
138
|
+
if (!chartCtx) return;
|
|
139
|
+
chartCtx.strokeStyle = getCssVariable('--lorenz-border', 'rgba(255,255,255,0.06)');
|
|
140
|
+
chartCtx.lineWidth = 1 * window.devicePixelRatio;
|
|
141
|
+
chartCtx.setLineDash([4 * window.devicePixelRatio, 4 * window.devicePixelRatio]);
|
|
142
|
+
chartCtx.beginPath();
|
|
143
|
+
chartCtx.moveTo(0, h - 4);
|
|
144
|
+
chartCtx.lineTo(w, h - 4);
|
|
145
|
+
chartCtx.stroke();
|
|
146
|
+
chartCtx.setLineDash([]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getMaxDistance(): number {
|
|
150
|
+
let maxDist = 0.01;
|
|
151
|
+
for (let i = 0; i < distanceHistory.length; i++) {
|
|
152
|
+
if (distanceHistory[i] > maxDist) {
|
|
153
|
+
maxDist = distanceHistory[i];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return maxDist;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function drawChartPath(w: number, h: number, maxDist: number, stepX: number) {
|
|
160
|
+
if (!chartCtx) return;
|
|
161
|
+
chartCtx.strokeStyle = getCssVariable('--lorenz-accent', '#a78bfa');
|
|
162
|
+
chartCtx.lineWidth = 1.5 * window.devicePixelRatio;
|
|
163
|
+
chartCtx.beginPath();
|
|
164
|
+
chartCtx.moveTo(0, h - (distanceHistory[0] / maxDist) * (h - 10));
|
|
165
|
+
for (let i = 1; i < distanceHistory.length; i++) {
|
|
166
|
+
chartCtx.lineTo(i * stepX, h - (distanceHistory[i] / maxDist) * (h - 10));
|
|
167
|
+
}
|
|
168
|
+
chartCtx.stroke();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function fillChartArea(w: number, h: number) {
|
|
172
|
+
if (!chartCtx) return;
|
|
173
|
+
chartCtx.lineTo(w, h);
|
|
174
|
+
chartCtx.lineTo(0, h);
|
|
175
|
+
chartCtx.closePath();
|
|
176
|
+
const gradient = chartCtx.createLinearGradient(0, 0, 0, h);
|
|
177
|
+
gradient.addColorStop(0, `rgba(167, 139, 250, 0.12)`);
|
|
178
|
+
gradient.addColorStop(1, `rgba(167, 139, 250, 0.0)`);
|
|
179
|
+
chartCtx.fillStyle = gradient;
|
|
180
|
+
chartCtx.fill();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function drawChart() {
|
|
184
|
+
if (!chartCanvas || !chartCtx) return;
|
|
185
|
+
const w = chartCanvas.width;
|
|
186
|
+
const h = chartCanvas.height;
|
|
187
|
+
chartCtx.clearRect(0, 0, w, h);
|
|
188
|
+
drawChartBaseline(w, h);
|
|
189
|
+
if (distanceHistory.length < 2) return;
|
|
190
|
+
const maxDist = getMaxDistance();
|
|
191
|
+
const stepX = w / (maxHistory - 1);
|
|
192
|
+
drawChartPath(w, h, maxDist, stepX);
|
|
193
|
+
fillChartArea(w, h);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updatePhysics(params: LorenzParams, dt: number): number {
|
|
197
|
+
let currentDist = lastDistance;
|
|
198
|
+
for (let steps = 0; steps < 4; steps++) {
|
|
199
|
+
t1 = LorenzEngine.nextPoint(t1, params, dt);
|
|
200
|
+
t2 = LorenzEngine.nextPoint(t2, params, dt);
|
|
201
|
+
t1History.push({ ...t1 });
|
|
202
|
+
t2History.push({ ...t2 });
|
|
203
|
+
currentDist = LorenzEngine.getDistance(t1, t2);
|
|
204
|
+
distanceHistory.push(currentDist);
|
|
205
|
+
if (t1History.length > maxHistory) {
|
|
206
|
+
t1History.shift();
|
|
207
|
+
t2History.shift();
|
|
208
|
+
distanceHistory.shift();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return currentDist;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function updateUIStats(currentDist: number, diff: number) {
|
|
215
|
+
if (statT1) statT1.textContent = `X:${t1.x.toFixed(1)} Y:${t1.y.toFixed(1)} Z:${t1.z.toFixed(1)}`;
|
|
216
|
+
if (statT2) statT2.textContent = `X:${t2.x.toFixed(1)} Y:${t2.y.toFixed(1)} Z:${t2.z.toFixed(1)}`;
|
|
217
|
+
if (statDist) statDist.textContent = currentDist.toFixed(4);
|
|
218
|
+
if (statDelta) {
|
|
219
|
+
if (Math.abs(diff) < 0.0001) {
|
|
220
|
+
statDelta.textContent = '--';
|
|
221
|
+
} else if (diff > 0) {
|
|
222
|
+
statDelta.textContent = `▲ +${diff.toFixed(4)}`;
|
|
223
|
+
} else {
|
|
224
|
+
statDelta.textContent = `▼ ${diff.toFixed(4)}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function loop() {
|
|
230
|
+
if (isRunning) {
|
|
231
|
+
const params = getParams();
|
|
232
|
+
const dt = getDt();
|
|
233
|
+
const currentDist = updatePhysics(params, dt);
|
|
234
|
+
const diff = currentDist - lastDistance;
|
|
235
|
+
lastDistance = currentDist;
|
|
236
|
+
updateUIStats(currentDist, diff);
|
|
237
|
+
}
|
|
238
|
+
drawAttractor();
|
|
239
|
+
drawChart();
|
|
240
|
+
requestAnimationFrame(loop);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
244
|
+
isDragging = true;
|
|
245
|
+
previousMousePosition = { x: e.clientX, y: e.clientY };
|
|
246
|
+
canvas.setPointerCapture(e.pointerId);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
window.addEventListener('pointerup', () => {
|
|
250
|
+
isDragging = false;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
window.addEventListener('pointermove', (e) => {
|
|
254
|
+
if (!isDragging) return;
|
|
255
|
+
ry += (e.clientX - previousMousePosition.x) * 0.01;
|
|
256
|
+
rx += (e.clientY - previousMousePosition.y) * 0.01;
|
|
257
|
+
previousMousePosition = { x: e.clientX, y: e.clientY };
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
slideSigma.addEventListener('input', () => { updateDisplays(); clearPaths(); });
|
|
261
|
+
slideRho.addEventListener('input', () => { updateDisplays(); clearPaths(); });
|
|
262
|
+
slideBeta.addEventListener('input', () => { updateDisplays(); clearPaths(); });
|
|
263
|
+
slideDt.addEventListener('input', () => { updateDisplays(); clearPaths(); });
|
|
264
|
+
slidePerturb.addEventListener('input', () => { updateDisplays(); clearPaths(); });
|
|
265
|
+
|
|
266
|
+
btnPlayPause.addEventListener('click', () => {
|
|
267
|
+
isRunning = !isRunning;
|
|
268
|
+
btnPlayPause.textContent = isRunning ? (btnPlayPause.dataset.pause || 'Pause') : (btnPlayPause.dataset.play || 'Resume');
|
|
269
|
+
btnPlayPause.className = isRunning ? 'lorenz-btn lorenz-btn-active-state' : 'lorenz-btn';
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
btnReset.addEventListener('click', resetToDefaults);
|
|
273
|
+
btnClear.addEventListener('click', clearPaths);
|
|
274
|
+
|
|
275
|
+
window.addEventListener('resize', resizeAll);
|
|
276
|
+
|
|
277
|
+
btnPlayPause.dataset.pause = btnPlayPause.textContent || 'Pause';
|
|
278
|
+
btnPlayPause.dataset.play = 'Resume';
|
|
279
|
+
|
|
280
|
+
resizeAll();
|
|
281
|
+
updateDisplays();
|
|
282
|
+
clearPaths();
|
|
283
|
+
loop();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { lorenzAttractor } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await lorenzAttractor.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
package/src/tools.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { SIMULATION_PROBABILITY_TOOL } from './tool/simulation-probability/index
|
|
|
7
7
|
import { CELLULAR_RENEWAL_TOOL } from './tool/cellular-renewal/index';
|
|
8
8
|
import { COSMIC_INFLATION_TOOL } from './tool/cosmic-inflation/index';
|
|
9
9
|
import { TEMPERATURE_TIMELINE_TOOL } from './tool/temperature-timeline/index';
|
|
10
|
+
import { LORENZ_ATTRACTOR_TOOL } from './tool/lorenz-attractor/index';
|
|
10
11
|
|
|
11
12
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
12
13
|
COLONY_COUNTER_TOOL,
|
|
@@ -16,6 +17,7 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
16
17
|
CELLULAR_RENEWAL_TOOL,
|
|
17
18
|
COSMIC_INFLATION_TOOL,
|
|
18
19
|
TEMPERATURE_TIMELINE_TOOL,
|
|
20
|
+
LORENZ_ATTRACTOR_TOOL,
|
|
19
21
|
];
|
|
20
22
|
|
|
21
23
|
|