@jjlmoya/utils-science 1.20.0 → 1.22.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 (114) hide show
  1. package/package.json +2 -1
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/fr.ts +6 -6
  4. package/src/category/i18n/ru.ts +1 -1
  5. package/src/category/index.ts +4 -1
  6. package/src/category/seo.astro +2 -2
  7. package/src/entries.ts +7 -1
  8. package/src/index.ts +3 -0
  9. package/src/pages/[locale]/[slug].astro +5 -4
  10. package/src/tests/locale_completeness.test.ts +2 -2
  11. package/src/tests/no_en_dash.test.ts +70 -0
  12. package/src/tests/seo_length.test.ts +5 -3
  13. package/src/tests/title_quality.test.ts +1 -1
  14. package/src/tests/tool_validation.test.ts +2 -2
  15. package/src/tool/asteroid-impact/bibliography.astro +2 -2
  16. package/src/tool/asteroid-impact/component.astro +16 -9
  17. package/src/tool/asteroid-impact/i18n/fr.ts +6 -6
  18. package/src/tool/asteroid-impact/i18n/ru.ts +4 -4
  19. package/src/tool/asteroid-impact/index.ts +1 -0
  20. package/src/tool/asteroid-impact/script.ts +13 -7
  21. package/src/tool/cellular-renewal/bibliography.astro +2 -2
  22. package/src/tool/cellular-renewal/i18n/fr.ts +13 -13
  23. package/src/tool/cellular-renewal/i18n/ru.ts +17 -17
  24. package/src/tool/cellular-renewal/i18n/zh.ts +9 -9
  25. package/src/tool/cellular-renewal/index.ts +1 -0
  26. package/src/tool/colony-counter/bibliography.astro +2 -2
  27. package/src/tool/colony-counter/i18n/ru.ts +5 -5
  28. package/src/tool/colony-counter/i18n/zh.ts +2 -2
  29. package/src/tool/colony-counter/index.ts +1 -0
  30. package/src/tool/cosmic-inflation/bibliography.astro +14 -0
  31. package/src/tool/cosmic-inflation/bibliography.ts +12 -0
  32. package/src/tool/cosmic-inflation/component.astro +270 -0
  33. package/src/tool/cosmic-inflation/cosmic-inflation-calculator.css +277 -0
  34. package/src/tool/cosmic-inflation/entry.ts +26 -0
  35. package/src/tool/cosmic-inflation/i18n/de.ts +188 -0
  36. package/src/tool/cosmic-inflation/i18n/en.ts +188 -0
  37. package/src/tool/cosmic-inflation/i18n/es.ts +168 -0
  38. package/src/tool/cosmic-inflation/i18n/fr.ts +188 -0
  39. package/src/tool/cosmic-inflation/i18n/id.ts +188 -0
  40. package/src/tool/cosmic-inflation/i18n/it.ts +188 -0
  41. package/src/tool/cosmic-inflation/i18n/ja.ts +188 -0
  42. package/src/tool/cosmic-inflation/i18n/ko.ts +188 -0
  43. package/src/tool/cosmic-inflation/i18n/nl.ts +188 -0
  44. package/src/tool/cosmic-inflation/i18n/pl.ts +188 -0
  45. package/src/tool/cosmic-inflation/i18n/pt.ts +188 -0
  46. package/src/tool/cosmic-inflation/i18n/ru.ts +188 -0
  47. package/src/tool/cosmic-inflation/i18n/sv.ts +188 -0
  48. package/src/tool/cosmic-inflation/i18n/tr.ts +188 -0
  49. package/src/tool/cosmic-inflation/i18n/zh.ts +188 -0
  50. package/src/tool/cosmic-inflation/index.ts +11 -0
  51. package/src/tool/cosmic-inflation/logic/CosmicInflationEngine.ts +21 -0
  52. package/src/tool/cosmic-inflation/seo.astro +15 -0
  53. package/src/tool/lorenz-attractor/bibliography.astro +14 -0
  54. package/src/tool/lorenz-attractor/bibliography.ts +12 -0
  55. package/src/tool/lorenz-attractor/component.astro +146 -0
  56. package/src/tool/lorenz-attractor/entry.ts +27 -0
  57. package/src/tool/lorenz-attractor/i18n/de.ts +113 -0
  58. package/src/tool/lorenz-attractor/i18n/en.ts +185 -0
  59. package/src/tool/lorenz-attractor/i18n/es.ts +113 -0
  60. package/src/tool/lorenz-attractor/i18n/fr.ts +113 -0
  61. package/src/tool/lorenz-attractor/i18n/id.ts +113 -0
  62. package/src/tool/lorenz-attractor/i18n/it.ts +113 -0
  63. package/src/tool/lorenz-attractor/i18n/ja.ts +113 -0
  64. package/src/tool/lorenz-attractor/i18n/ko.ts +113 -0
  65. package/src/tool/lorenz-attractor/i18n/nl.ts +113 -0
  66. package/src/tool/lorenz-attractor/i18n/pl.ts +113 -0
  67. package/src/tool/lorenz-attractor/i18n/pt.ts +113 -0
  68. package/src/tool/lorenz-attractor/i18n/ru.ts +113 -0
  69. package/src/tool/lorenz-attractor/i18n/sv.ts +113 -0
  70. package/src/tool/lorenz-attractor/i18n/tr.ts +113 -0
  71. package/src/tool/lorenz-attractor/i18n/zh.ts +113 -0
  72. package/src/tool/lorenz-attractor/index.ts +9 -0
  73. package/src/tool/lorenz-attractor/logic/LorenzEngine.ts +32 -0
  74. package/src/tool/lorenz-attractor/lorenz-attractor.css +335 -0
  75. package/src/tool/lorenz-attractor/renderer.ts +136 -0
  76. package/src/tool/lorenz-attractor/script.ts +282 -0
  77. package/src/tool/lorenz-attractor/seo.astro +15 -0
  78. package/src/tool/microwave-detector/bibliography.astro +2 -2
  79. package/src/tool/microwave-detector/component.astro +9 -7
  80. package/src/tool/microwave-detector/i18n/fr.ts +4 -4
  81. package/src/tool/microwave-detector/i18n/ru.ts +18 -18
  82. package/src/tool/microwave-detector/i18n/zh.ts +10 -10
  83. package/src/tool/microwave-detector/index.ts +1 -0
  84. package/src/tool/microwave-detector/logic/MicrowaveEngine.ts +5 -1
  85. package/src/tool/simulation-probability/bibliography.astro +2 -2
  86. package/src/tool/simulation-probability/i18n/fr.ts +5 -5
  87. package/src/tool/simulation-probability/i18n/ru.ts +7 -7
  88. package/src/tool/simulation-probability/i18n/zh.ts +4 -4
  89. package/src/tool/simulation-probability/index.ts +1 -0
  90. package/src/tool/temperature-timeline/bibliography.astro +14 -0
  91. package/src/tool/temperature-timeline/bibliography.ts +12 -0
  92. package/src/tool/temperature-timeline/component.astro +289 -0
  93. package/src/tool/temperature-timeline/entry.ts +26 -0
  94. package/src/tool/temperature-timeline/i18n/de.ts +213 -0
  95. package/src/tool/temperature-timeline/i18n/en.ts +213 -0
  96. package/src/tool/temperature-timeline/i18n/es.ts +178 -0
  97. package/src/tool/temperature-timeline/i18n/fr.ts +213 -0
  98. package/src/tool/temperature-timeline/i18n/id.ts +213 -0
  99. package/src/tool/temperature-timeline/i18n/it.ts +213 -0
  100. package/src/tool/temperature-timeline/i18n/ja.ts +213 -0
  101. package/src/tool/temperature-timeline/i18n/ko.ts +213 -0
  102. package/src/tool/temperature-timeline/i18n/nl.ts +213 -0
  103. package/src/tool/temperature-timeline/i18n/pl.ts +213 -0
  104. package/src/tool/temperature-timeline/i18n/pt.ts +213 -0
  105. package/src/tool/temperature-timeline/i18n/ru.ts +213 -0
  106. package/src/tool/temperature-timeline/i18n/sv.ts +213 -0
  107. package/src/tool/temperature-timeline/i18n/tr.ts +213 -0
  108. package/src/tool/temperature-timeline/i18n/zh.ts +213 -0
  109. package/src/tool/temperature-timeline/index.ts +11 -0
  110. package/src/tool/temperature-timeline/logic/TemperatureTimelineEngine.ts +58 -0
  111. package/src/tool/temperature-timeline/planet-temperature-timeline.css +158 -0
  112. package/src/tool/temperature-timeline/seo.astro +15 -0
  113. package/src/tools.ts +6 -0
  114. package/src/types.ts +1 -1
@@ -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,282 @@
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
+ resize(chartCanvas, 80);
111
+ }
112
+
113
+ function drawAttractor() {
114
+ if (!canvas || !ctx) return;
115
+ const config = { ctx, w: canvas.width, h: canvas.height, rx, ry };
116
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
117
+ drawGrid(config);
118
+ drawShadowPath(config, t1History);
119
+ drawShadowPath(config, t2History);
120
+ ctx.lineWidth = 1.8 * window.devicePixelRatio;
121
+ ctx.lineCap = 'round';
122
+ ctx.lineJoin = 'round';
123
+ const t1Color = getCssVariable('--lorenz-t1', '#00f0ff');
124
+ const t2Color = getCssVariable('--lorenz-t2', '#ff007f');
125
+ drawPath(config, t1History, t1Color);
126
+ drawPath(config, t2History, t2Color);
127
+ ctx.shadowBlur = 8 * window.devicePixelRatio;
128
+ ctx.shadowColor = t1Color;
129
+ drawHead(config, t1, t1Color);
130
+ ctx.shadowColor = t2Color;
131
+ drawHead(config, t2, t2Color);
132
+ ctx.shadowBlur = 0;
133
+ drawAxes(config);
134
+ }
135
+
136
+ function drawChartBaseline(w: number, h: number) {
137
+ if (!chartCtx) return;
138
+ chartCtx.strokeStyle = getCssVariable('--lorenz-border', 'rgba(255,255,255,0.06)');
139
+ chartCtx.lineWidth = 1 * window.devicePixelRatio;
140
+ chartCtx.setLineDash([4 * window.devicePixelRatio, 4 * window.devicePixelRatio]);
141
+ chartCtx.beginPath();
142
+ chartCtx.moveTo(0, h - 4);
143
+ chartCtx.lineTo(w, h - 4);
144
+ chartCtx.stroke();
145
+ chartCtx.setLineDash([]);
146
+ }
147
+
148
+ function getMaxDistance(): number {
149
+ let maxDist = 0.01;
150
+ for (let i = 0; i < distanceHistory.length; i++) {
151
+ if (distanceHistory[i] > maxDist) {
152
+ maxDist = distanceHistory[i];
153
+ }
154
+ }
155
+ return maxDist;
156
+ }
157
+
158
+ function drawChartPath(w: number, h: number, maxDist: number, stepX: number) {
159
+ if (!chartCtx) return;
160
+ chartCtx.strokeStyle = getCssVariable('--lorenz-accent', '#a78bfa');
161
+ chartCtx.lineWidth = 1.5 * window.devicePixelRatio;
162
+ chartCtx.beginPath();
163
+ chartCtx.moveTo(0, h - (distanceHistory[0] / maxDist) * (h - 10));
164
+ for (let i = 1; i < distanceHistory.length; i++) {
165
+ chartCtx.lineTo(i * stepX, h - (distanceHistory[i] / maxDist) * (h - 10));
166
+ }
167
+ chartCtx.stroke();
168
+ }
169
+
170
+ function fillChartArea(w: number, h: number) {
171
+ if (!chartCtx) return;
172
+ chartCtx.lineTo(w, h);
173
+ chartCtx.lineTo(0, h);
174
+ chartCtx.closePath();
175
+ const gradient = chartCtx.createLinearGradient(0, 0, 0, h);
176
+ gradient.addColorStop(0, `rgba(167, 139, 250, 0.12)`);
177
+ gradient.addColorStop(1, `rgba(167, 139, 250, 0.0)`);
178
+ chartCtx.fillStyle = gradient;
179
+ chartCtx.fill();
180
+ }
181
+
182
+ function drawChart() {
183
+ if (!chartCanvas || !chartCtx) return;
184
+ const w = chartCanvas.width;
185
+ const h = chartCanvas.height;
186
+ chartCtx.clearRect(0, 0, w, h);
187
+ drawChartBaseline(w, h);
188
+ if (distanceHistory.length < 2) return;
189
+ const maxDist = getMaxDistance();
190
+ const stepX = w / (maxHistory - 1);
191
+ drawChartPath(w, h, maxDist, stepX);
192
+ fillChartArea(w, h);
193
+ }
194
+
195
+ function updatePhysics(params: LorenzParams, dt: number): number {
196
+ let currentDist = lastDistance;
197
+ for (let steps = 0; steps < 4; steps++) {
198
+ t1 = LorenzEngine.nextPoint(t1, params, dt);
199
+ t2 = LorenzEngine.nextPoint(t2, params, dt);
200
+ t1History.push({ ...t1 });
201
+ t2History.push({ ...t2 });
202
+ currentDist = LorenzEngine.getDistance(t1, t2);
203
+ distanceHistory.push(currentDist);
204
+ if (t1History.length > maxHistory) {
205
+ t1History.shift();
206
+ t2History.shift();
207
+ distanceHistory.shift();
208
+ }
209
+ }
210
+ return currentDist;
211
+ }
212
+
213
+ function updateUIStats(currentDist: number, diff: number) {
214
+ if (statT1) statT1.textContent = `X:${t1.x.toFixed(1)} Y:${t1.y.toFixed(1)} Z:${t1.z.toFixed(1)}`;
215
+ if (statT2) statT2.textContent = `X:${t2.x.toFixed(1)} Y:${t2.y.toFixed(1)} Z:${t2.z.toFixed(1)}`;
216
+ if (statDist) statDist.textContent = currentDist.toFixed(4);
217
+ if (statDelta) {
218
+ if (Math.abs(diff) < 0.0001) {
219
+ statDelta.textContent = '--';
220
+ } else if (diff > 0) {
221
+ statDelta.textContent = `▲ +${diff.toFixed(4)}`;
222
+ } else {
223
+ statDelta.textContent = `▼ ${diff.toFixed(4)}`;
224
+ }
225
+ }
226
+ }
227
+
228
+ function loop() {
229
+ if (isRunning) {
230
+ const params = getParams();
231
+ const dt = getDt();
232
+ const currentDist = updatePhysics(params, dt);
233
+ const diff = currentDist - lastDistance;
234
+ lastDistance = currentDist;
235
+ updateUIStats(currentDist, diff);
236
+ }
237
+ drawAttractor();
238
+ drawChart();
239
+ requestAnimationFrame(loop);
240
+ }
241
+
242
+ canvas.addEventListener('pointerdown', (e) => {
243
+ isDragging = true;
244
+ previousMousePosition = { x: e.clientX, y: e.clientY };
245
+ canvas.setPointerCapture(e.pointerId);
246
+ });
247
+
248
+ window.addEventListener('pointerup', () => {
249
+ isDragging = false;
250
+ });
251
+
252
+ window.addEventListener('pointermove', (e) => {
253
+ if (!isDragging) return;
254
+ ry += (e.clientX - previousMousePosition.x) * 0.01;
255
+ rx += (e.clientY - previousMousePosition.y) * 0.01;
256
+ previousMousePosition = { x: e.clientX, y: e.clientY };
257
+ });
258
+
259
+ slideSigma.addEventListener('input', () => { updateDisplays(); clearPaths(); });
260
+ slideRho.addEventListener('input', () => { updateDisplays(); clearPaths(); });
261
+ slideBeta.addEventListener('input', () => { updateDisplays(); clearPaths(); });
262
+ slideDt.addEventListener('input', () => { updateDisplays(); clearPaths(); });
263
+ slidePerturb.addEventListener('input', () => { updateDisplays(); clearPaths(); });
264
+
265
+ btnPlayPause.addEventListener('click', () => {
266
+ isRunning = !isRunning;
267
+ btnPlayPause.textContent = isRunning ? (btnPlayPause.dataset.pause || 'Pause') : (btnPlayPause.dataset.play || 'Resume');
268
+ btnPlayPause.className = isRunning ? 'lorenz-btn lorenz-btn-active-state' : 'lorenz-btn';
269
+ });
270
+
271
+ btnReset.addEventListener('click', resetToDefaults);
272
+ btnClear.addEventListener('click', clearPaths);
273
+
274
+ window.addEventListener('resize', resizeAll);
275
+
276
+ btnPlayPause.dataset.pause = btnPlayPause.textContent || 'Pause';
277
+ btnPlayPause.dataset.play = 'Resume';
278
+
279
+ resizeAll();
280
+ updateDisplays();
281
+ clearPaths();
282
+ 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 }} />}
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { Bibliography } from '@jjlmoya/utils-shared';
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
3
  import { microwaveDetector } from './index';
4
4
  import type { KnownLocale } from '../../types';
5
5
 
@@ -11,4 +11,4 @@ const { locale = 'es' } = Astro.props;
11
11
  const content = await microwaveDetector.i18n[locale]?.();
12
12
  ---
13
13
 
14
- {content && <Bibliography links={content.bibliography} />}
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -87,7 +87,7 @@ const { ui } = Astro.props;
87
87
  if (root) {
88
88
  const startBtn = document.getElementById("start-btn");
89
89
  const modal = document.getElementById("initial-modal");
90
- const canvas = document.getElementById("interference-canvas");
90
+ const canvas = document.getElementById("interference-canvas") as HTMLCanvasElement | null;
91
91
  const ctx = canvas?.getContext("2d");
92
92
 
93
93
  const jitterDisplay = document.getElementById("jitter-value");
@@ -104,7 +104,7 @@ const { ui } = Astro.props;
104
104
  const engine = new MicrowaveEngine(currentLang);
105
105
  let running = false;
106
106
  let audioEnabled = true;
107
- let audioCtx = null;
107
+ let audioCtx: AudioContext | null = null;
108
108
 
109
109
  const history = new Array(60).fill(0);
110
110
 
@@ -143,10 +143,12 @@ const { ui } = Astro.props;
143
143
  ctx.fill();
144
144
  }
145
145
 
146
- async function playClick(_intensity) {
146
+ async function playClick(_intensity: number) {
147
147
  if (!audioEnabled) return;
148
- if (!audioCtx)
149
- audioCtx = new (window.AudioContext || window.webkitAudioContext)();
148
+ if (!audioCtx) {
149
+ const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
150
+ audioCtx = new AudioContextClass();
151
+ }
150
152
 
151
153
  const osc = audioCtx.createOscillator();
152
154
  const gain = audioCtx.createGain();
@@ -176,7 +178,7 @@ const { ui } = Astro.props;
176
178
  };
177
179
 
178
180
  if (verdictContainer) {
179
- const colors = colorMap[level.color] || colorMap.emerald;
181
+ const colors = colorMap[level.color as keyof typeof colorMap] || colorMap.emerald;
180
182
  verdictContainer.style.backgroundColor = colors.bg;
181
183
  verdictContainer.style.borderColor = colors.border;
182
184
  verdictContainer.style.color = colors.text;
@@ -189,7 +191,7 @@ const { ui } = Astro.props;
189
191
  orange: "#f97316",
190
192
  red: "#dc2626",
191
193
  };
192
- statusDot.style.backgroundColor = colorValues[level.color] || colorValues.emerald;
194
+ statusDot.style.backgroundColor = colorValues[level.color as keyof typeof colorValues] || colorValues.emerald;
193
195
  }
194
196
  }
195
197
 
@@ -1,5 +1,5 @@
1
1
  const description = 'Analysez si votre micro-ondes perd des radiations en mesurant l\'interférence en temps réel sur votre réseau WiFi. Outil de sécurité scientifique.';
2
- const title = 'Détecteur de Fuites Micro ondes : Visualiseur d\'Interférences WiFi';
2
+ const title = 'Détecteur de Fuites Micro ondes: Visualiseur d\'Interférences WiFi';
3
3
  const slug = 'detecteur-fuites-micro-ondes';
4
4
  const howTo = [
5
5
  {
@@ -79,7 +79,7 @@ export const content: ToolLocaleContent = {
79
79
  seo: [
80
80
  {
81
81
  type: 'title',
82
- text: 'PHYSIQUE DES ONDES : Votre micro-ondes fuit-il réellement des radiations ?',
82
+ text: 'PHYSIQUE DES ONDES: Votre micro-ondes fuit-il réellement des radiations ?',
83
83
  level: 2,
84
84
  },
85
85
  {
@@ -88,7 +88,7 @@ export const content: ToolLocaleContent = {
88
88
  },
89
89
  {
90
90
  type: 'paragraph',
91
- html: 'D\'un point de vue purement physique, un four à micro-ondes est un <strong>résonateur à cavité</strong> conçu pour bombarder les molécules d\'eau de radiations électromagnétiques à une fréquence très spécifique : <strong>2,45 GHz</strong>. Cette fréquence n\'est pas arbitraire ; elle est nécessaire pour provoquer l\'oscillation dipolaire des particules d\'eau, générant de la chaleur par friction. Le problème est que c\'est exactement la même fréquence que celle utilisée par la norme WiFi 802.11b/g/n.',
91
+ html: 'D\'un point de vue purement physique, un four à micro-ondes est un <strong>résonateur à cavité</strong> conçu pour bombarder les molécules d\'eau de radiations électromagnétiques à une fréquence très spécifique: <strong>2,45 GHz</strong>. Cette fréquence n\'est pas arbitraire ; elle est nécessaire pour provoquer l\'oscillation dipolaire des particules d\'eau, générant de la chaleur par friction. Le problème est que c\'est exactement la même fréquence que celle utilisée par la norme WiFi 802.11b/g/n.',
92
92
  },
93
93
  {
94
94
  type: 'title',
@@ -128,7 +128,7 @@ export const content: ToolLocaleContent = {
128
128
  type: 'list',
129
129
  items: [
130
130
  '<strong>"Le micro-ondes altère la structure moléculaire de l\'eau"</strong> - FAUX. Le rayonnement micro-ondes est <em>non ionisant</em>. Il n\'a pas assez d\'énergie pour rompre les liaisons chimiques ou altérer l\'ADN. Il fait simplement vibrer les molécules d\'eau, augmentant leur énergie cinétique (température).',
131
- '<strong>"Le rayonnement s\'accumule dans les aliments"</strong> - FAUX. Les micro-ondes sont comme la lumière : une fois que vous éteignez l\'interrupteur, elles disparaissent. Un aliment chaud n\'émet pas de rayonnement micro-ondes, seulement un rayonnement infrarouge (chaleur) tout comme un feu ou une poêle.',
131
+ '<strong>"Le rayonnement s\'accumule dans les aliments"</strong> - FAUX. Les micro-ondes sont comme la lumière: une fois que vous éteignez l\'interrupteur, elles disparaissent. Un aliment chaud n\'émet pas de rayonnement micro-ondes, seulement un rayonnement infrarouge (chaleur) tout comme un feu ou une poêle.',
132
132
  '<strong>"Regarder le plateau tourner endommage les yeux"</strong> - PARTIELLEMENT VRAI. La vitre de la porte est conçue avec une grille qui bloque les longueurs d\'onde de 2,45 GHz. Cependant, si la grille est endommagée, la vitre n\'arrêtera pas les micro-ondes. Le cristallin de l\'œil est très sensible à la chaleur et a une mauvaise circulation sanguine, donc une exposition directe prolongée (près d\'une fuite) pourrait causer des cataractes thermiques.',
133
133
  ],
134
134
  },
@@ -8,25 +8,25 @@ const howTo = [
8
8
  },
9
9
  {
10
10
  name: 'Запустите базовый тест задержки',
11
- text: 'Нажмите кнопку «Начать» при выключенной микроволновке, чтобы установить стабильный базовый уровень соединения.',
11
+ text: 'Нажмите кнопку "Начать" при выключенной микроволновке, чтобы установить стабильный базовый уровень соединения.',
12
12
  },
13
13
  {
14
14
  name: 'Включите микроволноку',
15
- text: 'Разогрейте стакан воды в течение 3060 секунд и встаньте рядом с прибором со своим устройством.',
15
+ text: 'Разогрейте стакан воды в течение 30-60 секунд и встаньте рядом с прибором со своим устройством.',
16
16
  },
17
17
  {
18
18
  name: 'Проанализируйте график',
19
- text: 'Наблюдайте, поднимается ли задержка выше 100200 мс или случаются ли потери пакетов во время работы прибора.',
19
+ text: 'Наблюдайте, поднимается ли задержка выше 100-200 мс или случаются ли потери пакетов во время работы прибора.',
20
20
  },
21
21
  ];
22
22
  const faq = [
23
23
  {
24
24
  question: 'Как веб-сайт может обнаружить мою микроволновку?',
25
- answer: 'Мы используем не магические сенсоры, а задержку сети. Как WiFi 2,4 ГГц, так и микроволновые печи работают на одной частоте (около 2450 МГц). Если экранирование микроволновки нарушено, оно создает «шум», который сталкивается с WiFi, резко увеличивая задержку (пинг).',
25
+ answer: 'Мы используем не магические сенсоры, а задержку сети. Как WiFi 2,4 ГГц, так и микроволновые печи работают на одной частоте (около 2450 МГц). Если экранирование микроволновки нарушено, оно создает "шум", который сталкивается с WiFi, резко увеличивая задержку (пинг).',
26
26
  },
27
27
  {
28
28
  question: 'Опасно ли, если моя микроволновка пропускает излучение?',
29
- answer: 'Небольшие помехи WiFi это нормально и не несут немедленного риска для здоровья, так как мощность быстро падает с расстоянием. Однако сильная утечка указывает на плохую герметичность дверцы или сетки, что следует проверить в целях технической безопасности.',
29
+ answer: 'Небольшие помехи WiFi - это нормально и не несут немедленного риска для здоровья, так как мощность быстро падает с расстоянием. Однако сильная утечка указывает на плохую герметичность дверцы или сетки, что следует проверить в целях технической безопасности.',
30
30
  },
31
31
  {
32
32
  question: 'Почему тест не работает с WiFi 5 ГГц?',
@@ -34,7 +34,7 @@ const faq = [
34
34
  },
35
35
  {
36
36
  question: 'Что означают скачки задержки на графике?',
37
- answer: 'Постоянные скачки во время работы микроволновки говорят о том, что электромагнитный сигнал «утекает» наружу и насыщает эфир, не давая пакетам данных WiFi приходить вовремя.',
37
+ answer: 'Постоянные скачки во время работы микроволновки говорят о том, что электромагнитный сигнал "утекает" наружу и насыщает эфир, не давая пакетам данных WiFi приходить вовремя.',
38
38
  },
39
39
  ];
40
40
  import { bibliography } from '../bibliography';
@@ -84,11 +84,11 @@ export const content: ToolLocaleContent = {
84
84
  },
85
85
  {
86
86
  type: 'paragraph',
87
- html: 'Микроволновка самый недопонятый прибор на современной кухне. Ее ненавидят пуристы и любят прагматики, но за ней скрывается постоянная борьба между замкнутой энергией и внешним миром.',
87
+ html: 'Микроволновка - самый недопонятый прибор на современной кухне. Ее ненавидят пуристы и любят прагматики, но за ней скрывается постоянная борьба между замкнутой энергией и внешним миром.',
88
88
  },
89
89
  {
90
90
  type: 'paragraph',
91
- html: 'С чисто физической точки зрения микроволновая печь это <strong>объемный резонатор</strong>, предназначенный для бомбардировки молекул воды электромагнитным излучением на строго определенной частоте: <strong>2,45 ГГц</strong>. Эта частота не случайна; она необходима для вызова дипольных колебаний частиц воды, генерирующих тепло за счет трения. Проблема в том, что это именно та частота, которую использует стандарт WiFi 802.11b/g/n.',
91
+ html: 'С чисто физической точки зрения микроволновая печь - это <strong>объемный резонатор</strong>, предназначенный для бомбардировки молекул воды электромагнитным излучением на строго определенной частоте: <strong>2,45 ГГц</strong>. Эта частота не случайна; она необходима для вызова дипольных колебаний частиц воды, генерирующих тепло за счет трения. Проблема в том, что это именно та частота, которую использует стандарт WiFi 802.11b/g/n.',
92
92
  },
93
93
  {
94
94
  type: 'title',
@@ -97,11 +97,11 @@ export const content: ToolLocaleContent = {
97
97
  },
98
98
  {
99
99
  type: 'paragraph',
100
- html: 'Любое WiFi-устройство, работающее в диапазоне 2,4 ГГц, конкурирует за тот же «воздух», что и ваша микроволновка. В идеальной печи <strong>клетка Фарадея</strong> (металлическая сетка, которую вы видите на дверце и внутренних стенках) должна удерживать 100% энергии.',
100
+ html: 'Любое WiFi-устройство, работающее в диапазоне 2,4 ГГц, конкурирует за тот же "воздух", что и ваша микроволновка. В идеальной печи <strong>клетка Фарадея</strong> (металлическая сетка, которую вы видите на дверце и внутренних стенках) должна удерживать 100% энергии.',
101
101
  },
102
102
  {
103
103
  type: 'paragraph',
104
- html: 'Однако ни одна клетка не идеальна в долгосрочной перспективе. Уплотнители дверцы, магнитные защелки и износ металла могут привести к утечке небольшого количества энергии. Хотя эти уровни обычно значительно ниже пределов безопасности для человека, они <strong>огромны</strong> для сетевой карты WiFi, вызывая задержки, потерю пакетов и характерный «джиттер», который измеряет наш инструмент.',
104
+ html: 'Однако ни одна клетка не идеальна в долгосрочной перспективе. Уплотнители дверцы, магнитные защелки и износ металла могут привести к утечке небольшого количества энергии. Хотя эти уровни обычно значительно ниже пределов безопасности для человека, они <strong>огромны</strong> для сетевой карты WiFi, вызывая задержки, потерю пакетов и характерный "джиттер", который измеряет наш инструмент.',
105
105
  },
106
106
  {
107
107
  type: 'title',
@@ -115,8 +115,8 @@ export const content: ToolLocaleContent = {
115
115
  {
116
116
  type: 'list',
117
117
  items: [
118
- '<strong>Шаг 1 Телефон:</strong> Положите мобильный телефон внутрь микроволновки (<strong>НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ НЕ ВКЛЮЧАЙТЕ ПЕЧЬ!</strong> Просто закройте дверцу).',
119
- '<strong>Шаг 2 Звонок:</strong> Попробуйте позвонить на этот телефон с другого устройства. Если телефон зазвонит, клетка Фарадея неисправна или не настроена на сотовые частоты. Если абонент «вне зоны доступа», экранирование работает правильно.',
118
+ '<strong>Шаг 1 - Телефон:</strong> Положите мобильный телефон внутрь микроволновки (<strong>НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ НЕ ВКЛЮЧАЙТЕ ПЕЧЬ!</strong> Просто закройте дверцу).',
119
+ '<strong>Шаг 2 - Звонок:</strong> Попробуйте позвонить на этот телефон с другого устройства. Если телефон зазвонит, клетка Фарадея неисправна или не настроена на сотовые частоты. Если абонент "вне зоны доступа", экранирование работает правильно.',
120
120
  ],
121
121
  },
122
122
  {
@@ -127,9 +127,9 @@ export const content: ToolLocaleContent = {
127
127
  {
128
128
  type: 'list',
129
129
  items: [
130
- '<strong>«Микроволновки изменяют молекулярную структуру воды»</strong> ЛОЖЬ. Микроволновое излучение является <em>неионизирующим</em>. У него недостаточно энергии, чтобы разорвать химические связи или изменить ДНК. Оно просто заставляет молекулы воды вибрировать, увеличивая их кинетическую энергию (температуру).',
131
- '<strong>«Радиация накапливается в еде»</strong> ЛОЖЬ. Микроволны подобны свету: как только вы выключаете выключатель, они исчезают. Горячая еда не испускает микроволновое излучение, только инфракрасное (тепло), точно так же, как костер или сковорода.',
132
- '<strong>«Наблюдение за вращающейся тарелкой портит зрение»</strong> ОТЧАСТИ ПРАВДА. Стекло дверцы снабжено сеткой, блокирующей волны 2,45 ГГц. Однако, если сетка повреждена, стекло не остановит микроволны. Хрусталик глаза очень чувствителен к нагреву и плохо снабжается кровью, поэтому длительное прямое воздействие (рядом с утечкой) может вызвать тепловую катаракту.',
130
+ '<strong>"Микроволновки изменяют молекулярную структуру воды"</strong> - ЛОЖЬ. Микроволновое излучение является <em>неионизирующим</em>. У него недостаточно энергии, чтобы разорвать химические связи или изменить ДНК. Оно просто заставляет молекулы воды вибрировать, увеличивая их кинетическую энергию (температуру).',
131
+ '<strong>"Радиация накапливается в еде"</strong> - ЛОЖЬ. Микроволны подобны свету: как только вы выключаете выключатель, они исчезают. Горячая еда не испускает микроволновое излучение, только инфракрасное (тепло), точно так же, как костер или сковорода.',
132
+ '<strong>"Наблюдение за вращающейся тарелкой портит зрение"</strong> - ОТЧАСТИ ПРАВДА. Стекло дверцы снабжено сеткой, блокирующей волны 2,45 ГГц. Однако, если сетка повреждена, стекло не остановит микроволны. Хрусталик глаза очень чувствителен к нагреву и плохо снабжается кровью, поэтому длительное прямое воздействие (рядом с утечкой) может вызвать тепловую катаракту.',
133
133
  ],
134
134
  },
135
135
  {
@@ -139,15 +139,15 @@ export const content: ToolLocaleContent = {
139
139
  },
140
140
  {
141
141
  type: 'paragraph',
142
- html: 'Наш инструмент не измеряет радиацию напрямую (в смартфонах нет таких датчиков), но он измеряет <strong>главный симптом</strong>. Когда частица излучения 2,45 ГГц покидает печь, она «сталкивается» с WiFi-сигналом вашего устройства. Это вызывает:',
142
+ html: 'Наш инструмент не измеряет радиацию напрямую (в смартфонах нет таких датчиков), но он измеряет <strong>главный симптом</strong>. Когда частица излучения 2,45 ГГц покидает печь, она "сталкивается" с WiFi-сигналом вашего устройства. Это вызывает:',
143
143
  },
144
144
  {
145
145
  type: 'list',
146
146
  items: [
147
147
  '<strong>Повторную передачу пакетов:</strong> Роутер обнаруживает, что сообщение пришло поврежденным, и вынужден отправить его снова, что увеличивает задержку.',
148
- '<strong>Вариацию джиттера:</strong> Это показатель нестабильности времени отклика. Высокий джиттер однозначный признак массовых внешних помех.',
148
+ '<strong>Вариацию джиттера:</strong> Это показатель нестабильности времени отклика. Высокий джиттер - однозначный признак массовых внешних помех.',
149
149
  '<strong>Падение производительности:</strong> Скорость передачи падает из-за насыщения радиочастотного спектра.',
150
- '<strong>Фоновый шум:</strong> Микроволновка вносит шум, который повышает «порог шума», из-за чего устройству становится трудно отличить реальные данные от радиоактивного хаоса.',
150
+ '<strong>Фоновый шум:</strong> Микроволновка вносит шум, который повышает "порог шума", из-за чего устройству становится трудно отличить реальные данные от радиоактивного хаоса.',
151
151
  ],
152
152
  },
153
153
  ],