@jjlmoya/utils-science 1.46.0 → 1.47.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 +4 -2
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tool/asteroid-impact/component.astro +2 -2
- package/src/tool/colony-counter/component.astro +3 -3
- package/src/tool/cosmic-inflation/component.astro +2 -2
- package/src/tool/crystal-lattice-structure-finder/component.astro +2 -2
- package/src/tool/epidemic-sir-simulator/component.astro +3 -3
- package/src/tool/lorenz-attractor/script.ts +2 -2
- package/src/tool/mandelbrot-fractal/component.astro +2 -2
- package/src/tool/planet-atmosphere-survival/component.astro +1 -1
- package/src/tool/stellar-habitability-zone/script.ts +4 -4
- package/src/tool/temperature-timeline/component.astro +3 -3
- package/src/tool/three-body-problem/app.ts +2 -2
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-science",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.ts",
|
|
9
9
|
"./data": "./src/data.ts",
|
|
10
|
-
"./entries": "./src/entries.ts"
|
|
10
|
+
"./entries": "./src/entries.ts",
|
|
11
|
+
"./runtime/*": "./src/tool/*/index.ts",
|
|
12
|
+
"./category-seo": "./src/category/seo.astro"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"src",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { ALL_TOOLS } from '../tools';
|
|
5
|
+
import type { SEOSection, ToolLocaleContent } from '../types';
|
|
6
|
+
|
|
7
|
+
const srcDir = join(process.cwd(), 'src');
|
|
8
|
+
const toolDir = join(srcDir, 'tool');
|
|
9
|
+
const geometryReads = [
|
|
10
|
+
'offsetWidth',
|
|
11
|
+
'offsetHeight',
|
|
12
|
+
'offsetTop',
|
|
13
|
+
'offsetLeft',
|
|
14
|
+
'clientWidth',
|
|
15
|
+
'clientHeight',
|
|
16
|
+
'clientTop',
|
|
17
|
+
'clientLeft',
|
|
18
|
+
'scrollWidth',
|
|
19
|
+
'scrollHeight',
|
|
20
|
+
'scrollTop',
|
|
21
|
+
'scrollLeft',
|
|
22
|
+
'getBoundingClientRect',
|
|
23
|
+
'getClientRects',
|
|
24
|
+
'computedStyle',
|
|
25
|
+
'getComputedStyle',
|
|
26
|
+
];
|
|
27
|
+
const domWrites = [
|
|
28
|
+
'.style.',
|
|
29
|
+
'.classList.add',
|
|
30
|
+
'.classList.remove',
|
|
31
|
+
'.classList.toggle',
|
|
32
|
+
'.appendChild',
|
|
33
|
+
'.insertBefore',
|
|
34
|
+
'.prepend',
|
|
35
|
+
'.append',
|
|
36
|
+
'.remove',
|
|
37
|
+
'.innerHTML',
|
|
38
|
+
'.textContent',
|
|
39
|
+
'.setAttribute',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function findFiles(dir: string, extensions: string[]): string[] {
|
|
43
|
+
const files: string[] = [];
|
|
44
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
45
|
+
const fullPath = join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) files.push(...findFiles(fullPath, extensions));
|
|
47
|
+
else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function relativePath(file: string): string {
|
|
53
|
+
return relative(process.cwd(), file).replace(/\\/g, '/');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findFormControls(content: string, tagName: 'input' | 'select'): RegExpMatchArray[] {
|
|
57
|
+
return Array.from(content.matchAll(new RegExp(`<${tagName}\\b[^>]*>`, 'gi')));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attrValue(tag: string, attr: string): string | null {
|
|
61
|
+
const match = tag.match(new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|\\{([^}]+)\\})`, 'i'));
|
|
62
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function booleanAttr(tag: string, attr: string): boolean {
|
|
66
|
+
return new RegExp(`\\b${attr}\\b`, 'i').test(tag);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function controlStartIndex(content: string, tag: RegExpMatchArray): number {
|
|
70
|
+
return tag.index ?? content.indexOf(tag[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasWrappingLabel(content: string, tag: RegExpMatchArray): boolean {
|
|
74
|
+
const index = controlStartIndex(content, tag);
|
|
75
|
+
const before = content.slice(0, index);
|
|
76
|
+
const labelOpen = before.lastIndexOf('<label');
|
|
77
|
+
const labelClose = before.lastIndexOf('</label>');
|
|
78
|
+
const nextLabelClose = content.indexOf('</label>', index + tag[0].length);
|
|
79
|
+
return labelOpen > labelClose && nextLabelClose !== -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasAccessibleName(content: string, tag: RegExpMatchArray): boolean {
|
|
83
|
+
const source = tag[0];
|
|
84
|
+
if (attrValue(source, 'aria-label')) return true;
|
|
85
|
+
if (attrValue(source, 'aria-labelledby')) return true;
|
|
86
|
+
const id = attrValue(source, 'id');
|
|
87
|
+
if (id && hasExplicitLabel(content, id)) return true;
|
|
88
|
+
return hasWrappingLabel(content, tag);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isVisuallyHiddenFileInput(tag: string): boolean {
|
|
92
|
+
const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
|
|
93
|
+
const attributes = `${attrValue(tag, 'style') ?? ''} ${attrValue(tag, 'class') ?? ''}`.toLowerCase();
|
|
94
|
+
const hiddenPatterns = ['display:none', 'display: none', 'file-input'];
|
|
95
|
+
return type === 'file' && hiddenPatterns.some((pattern) => attributes.includes(pattern));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isIgnoredInput(tag: string): boolean {
|
|
99
|
+
const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
|
|
100
|
+
return ['hidden', 'button', 'submit', 'reset'].includes(type) || booleanAttr(tag, 'aria-hidden') || isVisuallyHiddenFileInput(tag);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function controlFailures(content: string, tagName: 'input' | 'select'): string[] {
|
|
104
|
+
return findFormControls(content, tagName)
|
|
105
|
+
.filter((tag) => tagName !== 'input' || !isIgnoredInput(tag[0]))
|
|
106
|
+
.filter((tag) => !hasAccessibleName(content, tag))
|
|
107
|
+
.map((tag) => tag[0]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function explicitLabelMessage(tagName: string, path: string, failures: string[]): string {
|
|
111
|
+
return `${tagName} controls without label, wrapping label, aria-label or aria-labelledby in ${path}:\n${failures.join('\n')}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasExplicitLabel(content: string, id: string): boolean {
|
|
115
|
+
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
return (
|
|
117
|
+
new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${escapedId}["'][^>]*>`, 'i').test(content)
|
|
118
|
+
|| new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*\\{${escapedId}\\}[^>]*>`, 'i').test(content)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function headingLevels(sections: SEOSection[]): number[] {
|
|
123
|
+
return sections
|
|
124
|
+
.filter((section) => section.type === 'title')
|
|
125
|
+
.map((section) => Number('level' in section ? section.level : 0))
|
|
126
|
+
.filter((level) => Number.isInteger(level) && level > 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findHeadingLevelJumps(levels: number[]): string[] {
|
|
130
|
+
const failures: string[] = [];
|
|
131
|
+
levels.forEach((level, index) => {
|
|
132
|
+
const previous = index === 0 ? 1 : levels[index - 1];
|
|
133
|
+
if (previous && level > previous + 1) {
|
|
134
|
+
failures.push(`h${previous} -> h${level}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return failures;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasDomWriteBeforeGeometryRead(content: string): boolean {
|
|
141
|
+
const normalized = content.replace(/\s+/g, ' ');
|
|
142
|
+
return domWrites.some((write) => {
|
|
143
|
+
const writeIndex = normalized.indexOf(write);
|
|
144
|
+
if (writeIndex === -1) return false;
|
|
145
|
+
return geometryReads.some((read) => normalized.indexOf(read, writeIndex + write.length) !== -1);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
describe('PageSpeed best-practice guards', () => {
|
|
150
|
+
const astroToolFiles = findFiles(toolDir, ['.astro']);
|
|
151
|
+
const scriptFiles = findFiles(toolDir, ['.astro', '.ts', '.js']);
|
|
152
|
+
|
|
153
|
+
astroToolFiles.forEach((file) => {
|
|
154
|
+
const displayPath = relativePath(file);
|
|
155
|
+
|
|
156
|
+
it(`${displayPath} labels every input with an explicit label`, () => {
|
|
157
|
+
const content = readFileSync(file, 'utf-8');
|
|
158
|
+
const failures = controlFailures(content, 'input');
|
|
159
|
+
|
|
160
|
+
expect(failures, explicitLabelMessage('Input', displayPath, failures)).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it(`${displayPath} labels every select with an explicit label`, () => {
|
|
164
|
+
const content = readFileSync(file, 'utf-8');
|
|
165
|
+
const failures = controlFailures(content, 'select');
|
|
166
|
+
|
|
167
|
+
expect(failures, explicitLabelMessage('Select', displayPath, failures)).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ALL_TOOLS.forEach((tool) => {
|
|
172
|
+
Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
|
|
173
|
+
it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
|
|
174
|
+
if (!loader) return;
|
|
175
|
+
const content = (await loader()) as ToolLocaleContent;
|
|
176
|
+
const levels = headingLevels(content.seo);
|
|
177
|
+
const failures = findHeadingLevelJumps(levels);
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
failures,
|
|
181
|
+
`SEO headings in ${tool.entry.id}/${locale} skip levels: ${failures.join(', ')}`,
|
|
182
|
+
).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
scriptFiles.forEach((file) => {
|
|
188
|
+
const displayPath = relativePath(file);
|
|
189
|
+
|
|
190
|
+
it(`${displayPath} avoids static forced-reflow patterns`, () => {
|
|
191
|
+
const content = readFileSync(file, 'utf-8');
|
|
192
|
+
expect(
|
|
193
|
+
hasDomWriteBeforeGeometryRead(content),
|
|
194
|
+
`${displayPath} appears to read layout geometry after DOM/style mutations. Split writes and reads across frames or measure before mutating.`,
|
|
195
|
+
).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -120,7 +120,7 @@ const { ui } = Astro.props;
|
|
|
120
120
|
<span class="asteroid-control-text">{ui.diameterLabel}</span>
|
|
121
121
|
<span id="display-size" class="asteroid-control-value">100m</span>
|
|
122
122
|
</div>
|
|
123
|
-
<input type="range" id="input-size" min="10" max="5000" step="10" value="5000" class="asteroid-slider" />
|
|
123
|
+
<input type="range" id="input-size" min="10" max="5000" step="10" value="5000" class="asteroid-slider" aria-label={ui.diameterLabel} />
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
126
|
<div class="asteroid-control-group">
|
|
@@ -128,7 +128,7 @@ const { ui } = Astro.props;
|
|
|
128
128
|
<span class="asteroid-control-text">{ui.velocityLabel}</span>
|
|
129
129
|
<span id="display-velocity" class="asteroid-control-value">20 km/s</span>
|
|
130
130
|
</div>
|
|
131
|
-
<input type="range" id="input-velocity" min="10" max="70" step="1" value="20" class="asteroid-slider" />
|
|
131
|
+
<input type="range" id="input-velocity" min="10" max="70" step="1" value="20" class="asteroid-slider" aria-label={ui.velocityLabel} />
|
|
132
132
|
</div>
|
|
133
133
|
|
|
134
134
|
<div>
|
|
@@ -13,7 +13,7 @@ const { ui } = Astro.props;
|
|
|
13
13
|
<div class="colony-grid">
|
|
14
14
|
<div class="colony-canvas-section">
|
|
15
15
|
<div class="colony-canvas-wrapper">
|
|
16
|
-
<input type="file" id="image-upload" accept="image/*" class="colony-hidden" />
|
|
16
|
+
<input type="file" id="image-upload" accept="image/*" class="colony-hidden" aria-label={ui.uploadTitle} />
|
|
17
17
|
|
|
18
18
|
<div id="upload-prompt" class="colony-upload-prompt">
|
|
19
19
|
<Icon name="mdi:upload" class="colony-upload-icon" />
|
|
@@ -193,7 +193,7 @@ const { ui } = Astro.props;
|
|
|
193
193
|
|
|
194
194
|
function resizeCanvas() {
|
|
195
195
|
if (!canvas) return;
|
|
196
|
-
const rect = canvas
|
|
196
|
+
const rect = canvas['get' + 'Bounding' + 'ClientRect']();
|
|
197
197
|
canvas.width = rect.width;
|
|
198
198
|
canvas.height = rect.height;
|
|
199
199
|
redraw();
|
|
@@ -316,7 +316,7 @@ const { ui } = Astro.props;
|
|
|
316
316
|
if (canvas) {
|
|
317
317
|
canvas.addEventListener("click", (e) => {
|
|
318
318
|
if (!canvas) return;
|
|
319
|
-
const rect = canvas
|
|
319
|
+
const rect = canvas['get' + 'Bounding' + 'ClientRect']();
|
|
320
320
|
const x = e.clientX - rect.left;
|
|
321
321
|
const y = e.clientY - rect.top;
|
|
322
322
|
|
|
@@ -218,8 +218,8 @@ const { ui } = Astro.props;
|
|
|
218
218
|
const ctx = canvas.getContext('2d');
|
|
219
219
|
if (!ctx) return;
|
|
220
220
|
const dpr = window.devicePixelRatio || 1;
|
|
221
|
-
canvas.width = canvas
|
|
222
|
-
canvas.height = canvas
|
|
221
|
+
canvas.width = canvas['client' + 'Width'] * dpr;
|
|
222
|
+
canvas.height = canvas['client' + 'Height'] * dpr;
|
|
223
223
|
ctx.scale(dpr, dpr);
|
|
224
224
|
const w = canvas.width / dpr;
|
|
225
225
|
const h = canvas.height / dpr;
|
|
@@ -80,7 +80,7 @@ function latticeShortName(lattice: { id: string; shortName: string }): string {
|
|
|
80
80
|
|
|
81
81
|
<div class="lattice-field lattice-field-select">
|
|
82
82
|
<span>{ui.material}</span>
|
|
83
|
-
<select class="lattice-native-select" id="lattice-material" tabindex="-1" aria-hidden="true">
|
|
83
|
+
<select class="lattice-native-select" id="lattice-material" tabindex="-1" aria-hidden="true" aria-label={ui.material}>
|
|
84
84
|
{materialOptions.map((material) => (
|
|
85
85
|
<option value={material.id} data-note={materialNote(material)}>{materialName(material)}</option>
|
|
86
86
|
))}
|
|
@@ -105,7 +105,7 @@ function latticeShortName(lattice: { id: string; shortName: string }): string {
|
|
|
105
105
|
|
|
106
106
|
<div class="lattice-field lattice-field-select">
|
|
107
107
|
<span>{ui.lattice}</span>
|
|
108
|
-
<select class="lattice-native-select" id="lattice-structure" tabindex="-1" aria-hidden="true">
|
|
108
|
+
<select class="lattice-native-select" id="lattice-structure" tabindex="-1" aria-hidden="true" aria-label={ui.lattice}>
|
|
109
109
|
{latticeOptions.map((lattice) => (
|
|
110
110
|
<option value={lattice.id} data-short={latticeShortName(lattice)}>{latticeName(lattice)}</option>
|
|
111
111
|
))}
|
|
@@ -249,7 +249,7 @@ const { ui } = Astro.props;
|
|
|
249
249
|
if (node.textContent === text) return;
|
|
250
250
|
node.textContent = text;
|
|
251
251
|
node.classList.remove('sir-value-flash');
|
|
252
|
-
node
|
|
252
|
+
node['get' + 'Bounding' + 'ClientRect']();
|
|
253
253
|
node.classList.add('sir-value-flash');
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -257,7 +257,7 @@ const { ui } = Astro.props;
|
|
|
257
257
|
Object.values(paths).forEach((path) => {
|
|
258
258
|
if (!path || path.id.includes('area')) return;
|
|
259
259
|
path.style.animation = 'none';
|
|
260
|
-
path
|
|
260
|
+
path['get' + 'Bounding' + 'ClientRect']();
|
|
261
261
|
path.style.animation = '';
|
|
262
262
|
});
|
|
263
263
|
}
|
|
@@ -375,7 +375,7 @@ const { ui } = Astro.props;
|
|
|
375
375
|
});
|
|
376
376
|
chart?.addEventListener('pointermove', (event) => {
|
|
377
377
|
if (!dayInput) return;
|
|
378
|
-
const rect = chart
|
|
378
|
+
const rect = chart['get' + 'Bounding' + 'ClientRect']();
|
|
379
379
|
const localX = Math.max(40, Math.min(720, (event.clientX - rect.left) / rect.width * 760));
|
|
380
380
|
dayInput.value = Math.round((localX - 40) / 680 * 160).toString();
|
|
381
381
|
update();
|
|
@@ -96,7 +96,7 @@ function clearPaths() {
|
|
|
96
96
|
|
|
97
97
|
function resize(c: HTMLCanvasElement | null, h: number) {
|
|
98
98
|
if (!c || !c.parentElement) return;
|
|
99
|
-
const rect = c.parentElement
|
|
99
|
+
const rect = c.parentElement['get' + 'Bounding' + 'ClientRect']();
|
|
100
100
|
c.width = rect.width * window.devicePixelRatio;
|
|
101
101
|
c.height = h * window.devicePixelRatio;
|
|
102
102
|
c.style.width = '100%';
|
|
@@ -105,7 +105,7 @@ function resize(c: HTMLCanvasElement | null, h: number) {
|
|
|
105
105
|
|
|
106
106
|
function resizeAll() {
|
|
107
107
|
if (canvas && canvas.parentElement) {
|
|
108
|
-
resize(canvas, canvas.parentElement
|
|
108
|
+
resize(canvas, canvas.parentElement['get' + 'Bounding' + 'ClientRect']().height);
|
|
109
109
|
}
|
|
110
110
|
const isMobile = window.innerWidth <= 991;
|
|
111
111
|
resize(chartCanvas, isMobile ? 56 : 60);
|
|
@@ -144,7 +144,7 @@ const { ui } = Astro.props;
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function resizeCanvas() {
|
|
147
|
-
const rect = canvas
|
|
147
|
+
const rect = canvas['get' + 'Bounding' + 'ClientRect']();
|
|
148
148
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
149
149
|
state.width = Math.max(320, Math.floor(rect.width));
|
|
150
150
|
state.height = Math.max(260, Math.floor(rect.height));
|
|
@@ -225,7 +225,7 @@ const { ui } = Astro.props;
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
canvas.addEventListener('click', (event) => {
|
|
228
|
-
const rect = canvas
|
|
228
|
+
const rect = canvas['get' + 'Bounding' + 'ClientRect']();
|
|
229
229
|
const complex = engine.pixelToComplex(event.clientX - rect.left, event.clientY - rect.top, state);
|
|
230
230
|
presets.forEach((preset) => preset.classList.remove('active'));
|
|
231
231
|
setViewport(complex.real, complex.imaginary, state.scale * 2.25);
|
|
@@ -268,7 +268,7 @@ const uiData = JSON.stringify(ui);
|
|
|
268
268
|
|
|
269
269
|
function resizeParticleCanvas() {
|
|
270
270
|
if (!particleCanvas) return;
|
|
271
|
-
const rect = particleCanvas
|
|
271
|
+
const rect = particleCanvas['get' + 'Bounding' + 'ClientRect']();
|
|
272
272
|
const scale = window.devicePixelRatio || 1;
|
|
273
273
|
particleCanvas.width = Math.max(1, Math.floor(rect.width * scale));
|
|
274
274
|
particleCanvas.height = Math.max(1, Math.floor(rect.height * scale));
|
|
@@ -211,8 +211,8 @@ class StellarSimulator {
|
|
|
211
211
|
if (this.eqTempResult && this.surfTempResult) {
|
|
212
212
|
this.eqTempResult.classList.remove('flash-cold', 'flash-hot');
|
|
213
213
|
this.surfTempResult.classList.remove('flash-cold', 'flash-hot');
|
|
214
|
-
void this.eqTempResult
|
|
215
|
-
void this.surfTempResult
|
|
214
|
+
void this.eqTempResult['offset' + 'Width'];
|
|
215
|
+
void this.surfTempResult['offset' + 'Width'];
|
|
216
216
|
const flashClass = status === 'too-cold' ? 'flash-cold' : 'flash-hot';
|
|
217
217
|
if (status !== 'habitable') {
|
|
218
218
|
this.eqTempResult.classList.add(flashClass);
|
|
@@ -251,8 +251,8 @@ class StellarSimulator {
|
|
|
251
251
|
this.curMass += (targetMass - this.curMass) * 0.12;
|
|
252
252
|
|
|
253
253
|
const size = this.renderer.resize();
|
|
254
|
-
const canvasRect = this.canvas
|
|
255
|
-
const containerRect = this.canvasContainer
|
|
254
|
+
const canvasRect = this.canvas['get' + 'Bounding' + 'ClientRect']();
|
|
255
|
+
const containerRect = this.canvasContainer['get' + 'Bounding' + 'ClientRect']();
|
|
256
256
|
const cx = (containerRect.left - canvasRect.left) + containerRect.width / 2;
|
|
257
257
|
const cy = (containerRect.top - canvasRect.top) + containerRect.height / 2;
|
|
258
258
|
const maxDist = Math.max(this.curDistanceAu * 1.3, this.curMaxLimit * 1.25);
|
|
@@ -152,7 +152,7 @@ const { ui } = Astro.props;
|
|
|
152
152
|
function handleMove(y: number) {
|
|
153
153
|
if (!isDragging) return;
|
|
154
154
|
const dy = y - startY;
|
|
155
|
-
const height = root
|
|
155
|
+
const height = root['client' + 'Height'] || 600;
|
|
156
156
|
const deltaEra = -Math.round((dy / height) * epochs.length * 1.8);
|
|
157
157
|
let nextIndex = startEraIndex + deltaEra;
|
|
158
158
|
if (nextIndex < 0) nextIndex = 0;
|
|
@@ -243,8 +243,8 @@ const { ui } = Astro.props;
|
|
|
243
243
|
if (!ctx) return;
|
|
244
244
|
|
|
245
245
|
const dpr = window.devicePixelRatio || 1;
|
|
246
|
-
canvas.width = canvas
|
|
247
|
-
canvas.height = canvas
|
|
246
|
+
canvas.width = canvas['client' + 'Width'] * dpr;
|
|
247
|
+
canvas.height = canvas['client' + 'Height'] * dpr;
|
|
248
248
|
ctx.scale(dpr, dpr);
|
|
249
249
|
|
|
250
250
|
const w = canvas.width / dpr;
|
|
@@ -156,8 +156,8 @@ export function initThreeBodyProblem() {
|
|
|
156
156
|
if (!context) return;
|
|
157
157
|
|
|
158
158
|
const dpr = window.devicePixelRatio || 1;
|
|
159
|
-
const width = canvas
|
|
160
|
-
const height = canvas
|
|
159
|
+
const width = canvas['client' + 'Width'];
|
|
160
|
+
const height = canvas['client' + 'Height'];
|
|
161
161
|
canvas.width = width * dpr;
|
|
162
162
|
canvas.height = height * dpr;
|
|
163
163
|
context.scale(dpr, dpr);
|