@jjlmoya/utils-science 1.37.0 → 1.38.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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/dyson-sphere-energy-capture/bibliography.astro +14 -0
  8. package/src/tool/dyson-sphere-energy-capture/bibliography.ts +16 -0
  9. package/src/tool/dyson-sphere-energy-capture/component.astro +253 -0
  10. package/src/tool/dyson-sphere-energy-capture/dyson-sphere-energy-capture.css +502 -0
  11. package/src/tool/dyson-sphere-energy-capture/entry.ts +26 -0
  12. package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +195 -0
  13. package/src/tool/dyson-sphere-energy-capture/i18n/en.ts +195 -0
  14. package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +195 -0
  15. package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +195 -0
  16. package/src/tool/dyson-sphere-energy-capture/i18n/id.ts +195 -0
  17. package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +195 -0
  18. package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +71 -0
  19. package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +71 -0
  20. package/src/tool/dyson-sphere-energy-capture/i18n/nl.ts +197 -0
  21. package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +197 -0
  22. package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +195 -0
  23. package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +195 -0
  24. package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +195 -0
  25. package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +195 -0
  26. package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +71 -0
  27. package/src/tool/dyson-sphere-energy-capture/index.ts +11 -0
  28. package/src/tool/dyson-sphere-energy-capture/logic.ts +120 -0
  29. package/src/tool/dyson-sphere-energy-capture/seo.astro +15 -0
  30. package/src/tool/global-albedo-snowball-simulator/bibliography.astro +14 -0
  31. package/src/tool/global-albedo-snowball-simulator/bibliography.ts +16 -0
  32. package/src/tool/global-albedo-snowball-simulator/component.astro +278 -0
  33. package/src/tool/global-albedo-snowball-simulator/entry.ts +26 -0
  34. package/src/tool/global-albedo-snowball-simulator/global-albedo-snowball-simulator.css +530 -0
  35. package/src/tool/global-albedo-snowball-simulator/i18n/de.ts +169 -0
  36. package/src/tool/global-albedo-snowball-simulator/i18n/en.ts +169 -0
  37. package/src/tool/global-albedo-snowball-simulator/i18n/es.ts +169 -0
  38. package/src/tool/global-albedo-snowball-simulator/i18n/fr.ts +169 -0
  39. package/src/tool/global-albedo-snowball-simulator/i18n/id.ts +169 -0
  40. package/src/tool/global-albedo-snowball-simulator/i18n/it.ts +87 -0
  41. package/src/tool/global-albedo-snowball-simulator/i18n/ja.ts +87 -0
  42. package/src/tool/global-albedo-snowball-simulator/i18n/ko.ts +169 -0
  43. package/src/tool/global-albedo-snowball-simulator/i18n/nl.ts +169 -0
  44. package/src/tool/global-albedo-snowball-simulator/i18n/pl.ts +169 -0
  45. package/src/tool/global-albedo-snowball-simulator/i18n/pt.ts +169 -0
  46. package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +169 -0
  47. package/src/tool/global-albedo-snowball-simulator/i18n/sv.ts +169 -0
  48. package/src/tool/global-albedo-snowball-simulator/i18n/tr.ts +169 -0
  49. package/src/tool/global-albedo-snowball-simulator/i18n/zh.ts +169 -0
  50. package/src/tool/global-albedo-snowball-simulator/index.ts +11 -0
  51. package/src/tool/global-albedo-snowball-simulator/logic.ts +88 -0
  52. package/src/tool/global-albedo-snowball-simulator/seo.astro +15 -0
  53. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -18,10 +18,12 @@ import { mandelbrotFractal } from '../tool/mandelbrot-fractal/index';
18
18
  import { planetAtmosphereSurvival } from '../tool/planet-atmosphere-survival/index';
19
19
  import { threeBodyProblem } from '../tool/three-body-problem/index';
20
20
  import { rocheLimitSatelliteDisruption } from '../tool/roche-limit-satellite-disruption/index';
21
+ import { dysonSphereEnergyCapture } from '../tool/dyson-sphere-energy-capture/index';
22
+ import { globalAlbedoSnowballSimulator } from '../tool/global-albedo-snowball-simulator/index';
21
23
 
22
24
  export const scienceCategory: ScienceCategoryEntry = {
23
25
  icon: 'mdi:flask',
24
- tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption],
26
+ tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator],
25
27
  i18n: {
26
28
  es: () => import('./i18n/es').then((m) => m.content),
27
29
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -18,6 +18,8 @@ export { mandelbrotFractal } from './tool/mandelbrot-fractal/entry';
18
18
  export { planetAtmosphereSurvival } from './tool/planet-atmosphere-survival/entry';
19
19
  export { threeBodyProblem } from './tool/three-body-problem/entry';
20
20
  export { rocheLimitSatelliteDisruption } from './tool/roche-limit-satellite-disruption/entry';
21
+ export { dysonSphereEnergyCapture } from './tool/dyson-sphere-energy-capture/entry';
22
+ export { globalAlbedoSnowballSimulator } from './tool/global-albedo-snowball-simulator/entry';
21
23
  export { scienceCategory } from './category';
22
24
  import { asteroidImpact } from './tool/asteroid-impact/entry';
23
25
  import { cellularRenewal } from './tool/cellular-renewal/entry';
@@ -38,4 +40,6 @@ import { mandelbrotFractal } from './tool/mandelbrot-fractal/entry';
38
40
  import { planetAtmosphereSurvival } from './tool/planet-atmosphere-survival/entry';
39
41
  import { threeBodyProblem } from './tool/three-body-problem/entry';
40
42
  import { rocheLimitSatelliteDisruption } from './tool/roche-limit-satellite-disruption/entry';
41
- export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption];
43
+ import { dysonSphereEnergyCapture } from './tool/dyson-sphere-energy-capture/entry';
44
+ import { globalAlbedoSnowballSimulator } from './tool/global-albedo-snowball-simulator/entry';
45
+ export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption, dysonSphereEnergyCapture, globalAlbedoSnowballSimulator];
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export { MANDELBROT_FRACTAL_TOOL } from './tool/mandelbrot-fractal/index';
19
19
  export { PLANET_ATMOSPHERE_SURVIVAL_TOOL } from './tool/planet-atmosphere-survival/index';
20
20
  export { THREE_BODY_PROBLEM_TOOL } from './tool/three-body-problem/index';
21
21
  export { ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL } from './tool/roche-limit-satellite-disruption/index';
22
+ export { DYSON_SPHERE_ENERGY_CAPTURE_TOOL } from './tool/dyson-sphere-energy-capture/index';
23
+ export { GLOBAL_ALBEDO_SNOWBALL_SIMULATOR_TOOL } from './tool/global-albedo-snowball-simulator/index';
22
24
 
23
25
  export type {
24
26
  KnownLocale,
@@ -18,7 +18,7 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
  });
20
20
 
21
- it('all 19 tools registered', () => {
22
- expect(ALL_TOOLS.length).toBe(19);
21
+ it('all 20 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(21);
23
23
  });
24
24
  });
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 19 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(19);
7
+ it('should have 20 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(21);
9
9
  });
10
10
 
11
11
  it('scienceCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { dysonSphereEnergyCapture } 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 dysonSphereEnergyCapture.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Search for Artificial Stellar Sources of Infrared Radiation',
6
+ url: 'https://epizodsspace.airbase.ru/bibl/inostr-yazyki/science/1960/Dyson_Search_for_Artificial_Stellar_Sources_of_Infrared_Radiation_Science_131_(1960).pdf',
7
+ },
8
+ {
9
+ name: 'The Classification of Cosmic Civilizations',
10
+ url: 'https://en.wikipedia.org/wiki/Kardashev_scale',
11
+ },
12
+ {
13
+ name: 'Solar luminosity nominal value, IAU 2015 Resolution B3',
14
+ url: 'https://ui.adsabs.harvard.edu/abs/2015arXiv151007674M',
15
+ },
16
+ ];
@@ -0,0 +1,253 @@
1
+ ---
2
+ import './dyson-sphere-energy-capture.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div
12
+ class="dyson-root"
13
+ id="dyson-root"
14
+ data-status-underbuilt={ui.statusUnderbuilt}
15
+ data-status-balanced={ui.statusBalanced}
16
+ data-status-extreme={ui.statusExtreme}
17
+ data-mercury-masses={ui.mercuryMasses}
18
+ data-kilograms={ui.kilograms}
19
+ data-days-unit={ui.daysUnit}
20
+ >
21
+ <section class="dyson-stage" aria-label={ui.visualization}>
22
+ <div class="dyson-starfield">
23
+ <div class="dyson-orbit dyson-orbit-outer"></div>
24
+ <div class="dyson-orbit dyson-orbit-inner"></div>
25
+ <div class="dyson-star"></div>
26
+ <div class="dyson-collectors" id="dyson-collectors"></div>
27
+ <div class="dyson-power-beam"></div>
28
+ </div>
29
+ <div class="dyson-stage-readout">
30
+ <span>{ui.kardashevRating}</span>
31
+ <strong id="dyson-kardashev">---</strong>
32
+ </div>
33
+ </section>
34
+
35
+ <section class="dyson-panel">
36
+ <div class="dyson-selector-grid">
37
+ <label>
38
+ <span>{ui.starType}</span>
39
+ <select id="dyson-star-select">
40
+ <option value="m-dwarf">{ui.starMDwarf}</option>
41
+ <option value="sun" selected>{ui.starSun}</option>
42
+ <option value="a-star">{ui.starA}</option>
43
+ <option value="red-giant">{ui.starRedGiant}</option>
44
+ <option value="blue-giant">{ui.starBlueGiant}</option>
45
+ </select>
46
+ </label>
47
+ <label>
48
+ <span>{ui.structureType}</span>
49
+ <select id="dyson-structure-select">
50
+ <option value="swarm" selected>{ui.structureSwarm}</option>
51
+ <option value="ring">{ui.structureRing}</option>
52
+ <option value="shell">{ui.structureShell}</option>
53
+ <option value="statite-cloud">{ui.structureStatite}</option>
54
+ </select>
55
+ </label>
56
+ </div>
57
+
58
+ <div class="dyson-slider-stack">
59
+ <label class="dyson-slider-row" for="dyson-coverage">
60
+ <span>{ui.coverage}</span>
61
+ <strong id="dyson-coverage-value">35%</strong>
62
+ <input id="dyson-coverage" type="range" min="1" max="100" value="35" step="1" />
63
+ </label>
64
+ <label class="dyson-slider-row" for="dyson-temperature">
65
+ <span>{ui.operatingTemp}</span>
66
+ <strong id="dyson-temperature-value">600 K</strong>
67
+ <input id="dyson-temperature" type="range" min="250" max="1200" value="600" step="10" />
68
+ </label>
69
+ <label class="dyson-slider-row" for="dyson-target">
70
+ <span>{ui.kardashevTarget}</span>
71
+ <strong id="dyson-target-value">1.7</strong>
72
+ <input id="dyson-target" type="range" min="1" max="2" value="1.7" step="0.01" />
73
+ </label>
74
+ </div>
75
+ </section>
76
+
77
+ <section class="dyson-results">
78
+ <article>
79
+ <span>{ui.capturedPower}</span>
80
+ <strong id="dyson-captured">---</strong>
81
+ </article>
82
+ <article>
83
+ <span>{ui.optimalRadius}</span>
84
+ <strong id="dyson-radius">---</strong>
85
+ </article>
86
+ <article>
87
+ <span>{ui.targetCoverage}</span>
88
+ <strong id="dyson-target-coverage">---</strong>
89
+ </article>
90
+ <article>
91
+ <span>{ui.materialMass}</span>
92
+ <strong id="dyson-mass">---</strong>
93
+ </article>
94
+ </section>
95
+
96
+ <section class="dyson-diagnostics">
97
+ <div class="dyson-meter">
98
+ <span>{ui.captureMeter}</span>
99
+ <div><i id="dyson-meter-fill"></i></div>
100
+ </div>
101
+ <p id="dyson-status">{ui.statusReady}</p>
102
+ <dl>
103
+ <div>
104
+ <dt>{ui.orbitalPeriod}</dt>
105
+ <dd id="dyson-period">---</dd>
106
+ </div>
107
+ <div>
108
+ <dt>{ui.collectorArea}</dt>
109
+ <dd id="dyson-area">---</dd>
110
+ </div>
111
+ </dl>
112
+ </section>
113
+ </div>
114
+
115
+ <script>
116
+ import { calculateDyson } from './logic';
117
+ import type { StarType, StructureType } from './logic';
118
+
119
+ const root = document.getElementById('dyson-root');
120
+ if (root) {
121
+ const storageKey = 'dyson-sphere-energy-capture:v1';
122
+ const starSelect = document.getElementById('dyson-star-select') as HTMLSelectElement;
123
+ const structureSelect = document.getElementById('dyson-structure-select') as HTMLSelectElement;
124
+ const coverageInput = document.getElementById('dyson-coverage') as HTMLInputElement;
125
+ const temperatureInput = document.getElementById('dyson-temperature') as HTMLInputElement;
126
+ const targetInput = document.getElementById('dyson-target') as HTMLInputElement;
127
+ const collectors = document.getElementById('dyson-collectors');
128
+ const statusText: Record<string, string> = {
129
+ underbuilt: root.dataset.statusUnderbuilt || '',
130
+ balanced: root.dataset.statusBalanced || '',
131
+ extreme: root.dataset.statusExtreme || '',
132
+ };
133
+
134
+ const setText = (id: string, text: string) => {
135
+ const node = document.getElementById(id);
136
+ if (node) node.textContent = text;
137
+ };
138
+
139
+ const formatPower = (value: number) => `${value.toExponential(2).replace('e+', 'e')} W`;
140
+
141
+ function formatMass(kg: number, mercury: number) {
142
+ if (mercury > 0.01) return (root.dataset.mercuryMasses || '{value}').replace('{value}', mercury.toFixed(2));
143
+ return (root.dataset.kilograms || '{value} kg').replace('{value}', kg.toExponential(2));
144
+ }
145
+
146
+ function getCoverageBand(coveragePercent: number) {
147
+ if (coveragePercent > 92) return 'full';
148
+ if (coveragePercent > 62) return 'dense';
149
+ return 'open';
150
+ }
151
+
152
+ function getCollectorCount(coveragePercent: number) {
153
+ const baseCount = Math.max(6, Math.round(coveragePercent * 1.2));
154
+ if (structureSelect.value === 'ring') return Math.max(10, Math.round(coveragePercent * 0.7));
155
+ if (structureSelect.value === 'shell') return Math.max(18, Math.round(coveragePercent * 1.7));
156
+ if (structureSelect.value === 'statite-cloud') return Math.max(14, Math.round(coveragePercent * 1.45));
157
+ return baseCount;
158
+ }
159
+
160
+ function buildCollectors(count: number) {
161
+ if (!collectors) return;
162
+ collectors.innerHTML = '';
163
+ for (let i = 0; i < count; i++) {
164
+ const dot = document.createElement('span');
165
+ const band = i % 4;
166
+ dot.style.setProperty('--angle', `${(360 / count) * i}deg`);
167
+ dot.style.setProperty('--depth', `${86 + band * 17}px`);
168
+ dot.style.setProperty('--delay', `${i * -0.18}s`);
169
+ dot.style.setProperty('--tilt', `${band * 9 - 13}deg`);
170
+ collectors.append(dot);
171
+ }
172
+ }
173
+
174
+ function renderReadouts(result: ReturnType<typeof calculateDyson>, coveragePercent: number, operatingTempK: number, kardashevTarget: number) {
175
+ setText('dyson-coverage-value', `${coveragePercent}%`);
176
+ setText('dyson-temperature-value', `${operatingTempK} K`);
177
+ setText('dyson-target-value', kardashevTarget.toFixed(2));
178
+ setText('dyson-kardashev', `K${result.kardashevRating.toFixed(2)}`);
179
+ setText('dyson-captured', formatPower(result.capturedWatts));
180
+ setText('dyson-radius', `${result.optimalRadiusAu.toFixed(2)} AU`);
181
+ setText('dyson-target-coverage', `${result.targetCoveragePercent.toFixed(1)}%`);
182
+ setText('dyson-mass', formatMass(result.materialMassKg, result.mercuryMasses));
183
+ setText('dyson-period', (root.dataset.daysUnit || '{value} days').replace('{value}', result.orbitalPeriodDays.toLocaleString(undefined, { maximumFractionDigits: 0 })));
184
+ setText('dyson-area', `${result.collectorAreaM2.toExponential(2)} m2`);
185
+ setText('dyson-status', statusText[result.status]);
186
+ }
187
+
188
+ function renderVisuals(result: ReturnType<typeof calculateDyson>, coveragePercent: number) {
189
+ const fill = document.getElementById('dyson-meter-fill');
190
+ if (fill) fill.style.width = `${Math.min(100, (coveragePercent / Math.max(1, result.targetCoveragePercent)) * 100)}%`;
191
+
192
+ const radiusScale = Math.min(1.34, Math.max(0.68, 0.78 + Math.log10(Math.max(0.08, result.optimalRadiusAu)) * 0.18));
193
+ const orbitSpeedSeconds = Math.min(34, Math.max(5, Math.log10(Math.max(2, result.orbitalPeriodDays)) * 8));
194
+ root.style.setProperty('--dyson-coverage', `${coveragePercent}%`);
195
+ root.style.setProperty('--dyson-orbit-scale', radiusScale.toFixed(3));
196
+ root.style.setProperty('--dyson-orbit-speed', `${orbitSpeedSeconds.toFixed(2)}s`);
197
+ root.style.setProperty('--dyson-glow-strength', `${Math.min(0.82, 0.28 + coveragePercent / 160)}`);
198
+ root.dataset.star = starSelect.value;
199
+ root.dataset.structure = structureSelect.value;
200
+ root.dataset.status = result.status;
201
+ root.dataset.coverage = getCoverageBand(coveragePercent);
202
+ buildCollectors(getCollectorCount(coveragePercent));
203
+ }
204
+
205
+ function saveState() {
206
+ const payload = {
207
+ star: starSelect.value,
208
+ structure: structureSelect.value,
209
+ coverage: coverageInput.value,
210
+ temperature: temperatureInput.value,
211
+ target: targetInput.value,
212
+ };
213
+ localStorage.setItem(storageKey, JSON.stringify(payload));
214
+ }
215
+
216
+ function restoreState() {
217
+ const raw = localStorage.getItem(storageKey);
218
+ if (!raw) return;
219
+
220
+ const payload = JSON.parse(raw) as Record<string, string>;
221
+ if (payload.star) starSelect.value = payload.star;
222
+ if (payload.structure) structureSelect.value = payload.structure;
223
+ if (payload.coverage) coverageInput.value = payload.coverage;
224
+ if (payload.temperature) temperatureInput.value = payload.temperature;
225
+ if (payload.target) targetInput.value = payload.target;
226
+ }
227
+
228
+ function update() {
229
+ const coveragePercent = Number(coverageInput.value);
230
+ const operatingTempK = Number(temperatureInput.value);
231
+ const kardashevTarget = Number(targetInput.value);
232
+ const result = calculateDyson({
233
+ star: starSelect.value as StarType,
234
+ structure: structureSelect.value as StructureType,
235
+ coveragePercent,
236
+ operatingTempK,
237
+ kardashevTarget,
238
+ });
239
+
240
+ renderReadouts(result, coveragePercent, operatingTempK, kardashevTarget);
241
+ renderVisuals(result, coveragePercent);
242
+ saveState();
243
+ }
244
+
245
+ [starSelect, structureSelect, coverageInput, temperatureInput, targetInput].forEach((control) => {
246
+ control.addEventListener('input', update);
247
+ control.addEventListener('change', update);
248
+ });
249
+
250
+ restoreState();
251
+ update();
252
+ }
253
+ </script>