@jjlmoya/utils-science 1.42.0 → 1.43.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 (129) 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/diacritics_density.test.ts +140 -0
  6. package/src/tests/inverted_punctuation.test.ts +84 -0
  7. package/src/tests/locale_completeness.test.ts +2 -2
  8. package/src/tests/script_density.test.ts +94 -0
  9. package/src/tests/tool_validation.test.ts +2 -3
  10. package/src/tool/conway-life-rule-lab/i18n/de.ts +62 -5
  11. package/src/tool/conway-life-rule-lab/i18n/es.ts +62 -5
  12. package/src/tool/conway-life-rule-lab/i18n/fr.ts +62 -5
  13. package/src/tool/conway-life-rule-lab/i18n/id.ts +102 -45
  14. package/src/tool/conway-life-rule-lab/i18n/it.ts +62 -5
  15. package/src/tool/conway-life-rule-lab/i18n/ja.ts +62 -5
  16. package/src/tool/conway-life-rule-lab/i18n/ko.ts +62 -5
  17. package/src/tool/conway-life-rule-lab/i18n/nl.ts +102 -45
  18. package/src/tool/conway-life-rule-lab/i18n/pl.ts +62 -5
  19. package/src/tool/conway-life-rule-lab/i18n/pt.ts +62 -5
  20. package/src/tool/conway-life-rule-lab/i18n/ru.ts +62 -5
  21. package/src/tool/conway-life-rule-lab/i18n/sv.ts +62 -5
  22. package/src/tool/conway-life-rule-lab/i18n/tr.ts +62 -5
  23. package/src/tool/conway-life-rule-lab/i18n/zh.ts +62 -5
  24. package/src/tool/cosmic-inflation/i18n/es.ts +5 -5
  25. package/src/tool/cosmic-inflation/i18n/it.ts +51 -51
  26. package/src/tool/cosmic-inflation/i18n/ru.ts +42 -150
  27. package/src/tool/double-slit-decoherence/i18n/fr.ts +42 -86
  28. package/src/tool/double-slit-decoherence/i18n/it.ts +28 -72
  29. package/src/tool/double-slit-decoherence/i18n/pt.ts +35 -79
  30. package/src/tool/double-slit-decoherence/i18n/ru.ts +59 -59
  31. package/src/tool/double-slit-decoherence/i18n/sv.ts +33 -81
  32. package/src/tool/double-slit-decoherence/i18n/tr.ts +55 -55
  33. package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +42 -123
  34. package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +6 -6
  35. package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +63 -131
  36. package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +36 -113
  37. package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +66 -42
  38. package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +66 -42
  39. package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +50 -133
  40. package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +51 -132
  41. package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +61 -98
  42. package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +56 -137
  43. package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +61 -142
  44. package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +67 -43
  45. package/src/tool/epidemic-sir-simulator/bibliography.astro +15 -0
  46. package/src/tool/epidemic-sir-simulator/bibliography.ts +37 -0
  47. package/src/tool/epidemic-sir-simulator/component.astro +384 -0
  48. package/src/tool/epidemic-sir-simulator/entry.ts +30 -0
  49. package/src/tool/epidemic-sir-simulator/epidemic-sir-simulator.css +624 -0
  50. package/src/tool/epidemic-sir-simulator/i18n/de.ts +347 -0
  51. package/src/tool/epidemic-sir-simulator/i18n/en.ts +347 -0
  52. package/src/tool/epidemic-sir-simulator/i18n/es.ts +347 -0
  53. package/src/tool/epidemic-sir-simulator/i18n/fr.ts +347 -0
  54. package/src/tool/epidemic-sir-simulator/i18n/id.ts +347 -0
  55. package/src/tool/epidemic-sir-simulator/i18n/it.ts +347 -0
  56. package/src/tool/epidemic-sir-simulator/i18n/ja.ts +347 -0
  57. package/src/tool/epidemic-sir-simulator/i18n/ko.ts +347 -0
  58. package/src/tool/epidemic-sir-simulator/i18n/nl.ts +347 -0
  59. package/src/tool/epidemic-sir-simulator/i18n/pl.ts +347 -0
  60. package/src/tool/epidemic-sir-simulator/i18n/pt.ts +347 -0
  61. package/src/tool/epidemic-sir-simulator/i18n/ru.ts +347 -0
  62. package/src/tool/epidemic-sir-simulator/i18n/sv.ts +347 -0
  63. package/src/tool/epidemic-sir-simulator/i18n/tr.ts +347 -0
  64. package/src/tool/epidemic-sir-simulator/i18n/zh.ts +347 -0
  65. package/src/tool/epidemic-sir-simulator/index.ts +12 -0
  66. package/src/tool/epidemic-sir-simulator/logic.ts +163 -0
  67. package/src/tool/epidemic-sir-simulator/seo.astro +16 -0
  68. package/src/tool/fermi-paradox-filter-lab/bibliography.astro +14 -0
  69. package/src/tool/fermi-paradox-filter-lab/bibliography.ts +24 -0
  70. package/src/tool/fermi-paradox-filter-lab/component.astro +257 -0
  71. package/src/tool/fermi-paradox-filter-lab/entry.ts +26 -0
  72. package/src/tool/fermi-paradox-filter-lab/fermi-paradox-filter-lab.css +228 -0
  73. package/src/tool/fermi-paradox-filter-lab/i18n/de.ts +192 -0
  74. package/src/tool/fermi-paradox-filter-lab/i18n/en.ts +192 -0
  75. package/src/tool/fermi-paradox-filter-lab/i18n/es.ts +193 -0
  76. package/src/tool/fermi-paradox-filter-lab/i18n/fr.ts +192 -0
  77. package/src/tool/fermi-paradox-filter-lab/i18n/id.ts +192 -0
  78. package/src/tool/fermi-paradox-filter-lab/i18n/it.ts +192 -0
  79. package/src/tool/fermi-paradox-filter-lab/i18n/ja.ts +192 -0
  80. package/src/tool/fermi-paradox-filter-lab/i18n/ko.ts +192 -0
  81. package/src/tool/fermi-paradox-filter-lab/i18n/nl.ts +192 -0
  82. package/src/tool/fermi-paradox-filter-lab/i18n/pl.ts +193 -0
  83. package/src/tool/fermi-paradox-filter-lab/i18n/pt.ts +192 -0
  84. package/src/tool/fermi-paradox-filter-lab/i18n/ru.ts +192 -0
  85. package/src/tool/fermi-paradox-filter-lab/i18n/sv.ts +192 -0
  86. package/src/tool/fermi-paradox-filter-lab/i18n/tr.ts +192 -0
  87. package/src/tool/fermi-paradox-filter-lab/i18n/zh.ts +192 -0
  88. package/src/tool/fermi-paradox-filter-lab/index.ts +11 -0
  89. package/src/tool/fermi-paradox-filter-lab/logic.ts +125 -0
  90. package/src/tool/fermi-paradox-filter-lab/seo.astro +15 -0
  91. package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +50 -63
  92. package/src/tool/lorenz-attractor/i18n/de.ts +52 -78
  93. package/src/tool/lorenz-attractor/i18n/es.ts +5 -5
  94. package/src/tool/lorenz-attractor/i18n/fr.ts +51 -77
  95. package/src/tool/lorenz-attractor/i18n/it.ts +36 -36
  96. package/src/tool/lorenz-attractor/i18n/pt.ts +51 -75
  97. package/src/tool/mandelbrot-fractal/i18n/de.ts +31 -58
  98. package/src/tool/mandelbrot-fractal/i18n/es.ts +5 -5
  99. package/src/tool/mandelbrot-fractal/i18n/fr.ts +39 -57
  100. package/src/tool/mandelbrot-fractal/i18n/it.ts +30 -48
  101. package/src/tool/mandelbrot-fractal/i18n/pl.ts +34 -52
  102. package/src/tool/mandelbrot-fractal/i18n/pt.ts +30 -30
  103. package/src/tool/mandelbrot-fractal/i18n/ru.ts +42 -60
  104. package/src/tool/mandelbrot-fractal/i18n/sv.ts +38 -65
  105. package/src/tool/natural-selection-drift/i18n/it.ts +18 -95
  106. package/src/tool/natural-selection-drift/i18n/pt.ts +76 -19
  107. package/src/tool/natural-selection-drift/i18n/ru.ts +27 -24
  108. package/src/tool/natural-selection-drift/i18n/sv.ts +9 -6
  109. package/src/tool/phase-diagram-critical-points/i18n/de.ts +30 -68
  110. package/src/tool/phase-diagram-critical-points/i18n/es.ts +4 -4
  111. package/src/tool/phase-diagram-critical-points/i18n/fr.ts +37 -75
  112. package/src/tool/phase-diagram-critical-points/i18n/it.ts +8 -8
  113. package/src/tool/planet-atmosphere-survival/i18n/ru.ts +49 -192
  114. package/src/tool/radioactive-decay/i18n/de.ts +16 -32
  115. package/src/tool/radioactive-decay/i18n/es.ts +5 -5
  116. package/src/tool/radioactive-decay/i18n/fr.ts +24 -40
  117. package/src/tool/radioactive-decay/i18n/it.ts +18 -35
  118. package/src/tool/radioactive-decay/i18n/pt.ts +34 -34
  119. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +52 -128
  120. package/src/tool/temperature-timeline/i18n/de.ts +46 -174
  121. package/src/tool/temperature-timeline/i18n/es.ts +5 -5
  122. package/src/tool/temperature-timeline/i18n/fr.ts +69 -144
  123. package/src/tool/temperature-timeline/i18n/it.ts +34 -133
  124. package/src/tool/temperature-timeline/i18n/pt.ts +35 -174
  125. package/src/tool/temperature-timeline/i18n/ru.ts +48 -174
  126. package/src/tool/twin-paradox-visualizer/i18n/es.ts +5 -5
  127. package/src/tool/twin-paradox-visualizer/i18n/fr.ts +34 -34
  128. package/src/tool/twin-paradox-visualizer/i18n/pt.ts +31 -71
  129. package/src/tools.ts +4 -0
@@ -2,21 +2,45 @@ import { bibliography } from '../bibliography';
2
2
  import type { ToolLocaleContent } from '../../../types';
3
3
 
4
4
  const slug = 'dyson-sphere-energy-capture';
5
- const title = 'Dyson Sphere Energy Capture Simulator';
6
- const description = 'Compare Dyson collectors around different stars with estimates for power, radius, mass, and Kardashev coverage.';
5
+ const title = '戴森球能量捕获模拟器';
6
+ const description = '通过捕获功率、轨道半径、材料质量和卡尔达肖夫尺度覆盖率,比较戴森群、环形、刚性壳和静态镜云等巨型结构设计。';
7
7
 
8
8
  const howTo = [
9
- { name: 'Choose a star', text: 'Select a representative star to set luminosity and mass.' },
10
- { name: 'Choose a collector', text: 'Compare swarm, ring, shell, and statite cloud designs.' },
11
- { name: 'Adjust controls', text: 'Set coverage, operating temperature, and target Kardashev rating.' },
12
- { name: 'Review output', text: 'Read captured power, radius, mass, area, and target coverage.' },
9
+ {
10
+ name: '选择恒星类型',
11
+ text: '选择M型矮星、类太阳G型星、A型星、红巨星或蓝巨星,以设定光度和质量。',
12
+ },
13
+ {
14
+ name: '选择收集器结构',
15
+ text: '比较戴森群、赤道环、刚性壳和静态镜云的不同捕获假设。',
16
+ },
17
+ {
18
+ name: '调整覆盖率和温度',
19
+ text: '改变覆盖率和工作温度,观察捕获功率和安全轨道半径如何变化。',
20
+ },
21
+ {
22
+ name: '查看卡尔达肖夫目标',
23
+ text: '使用卡尔达肖夫目标滑块估算达到所选文明能量等级所需的恒星输出比例。',
24
+ },
13
25
  ];
14
26
 
15
27
  const faq = [
16
- { question: 'What is a Dyson sphere?', answer: 'It is a family of concepts for collecting a large fraction of stellar luminosity.' },
17
- { question: 'Why include several structures?', answer: 'Different structures have different efficiency, mass, and stability assumptions.' },
18
- { question: 'What does target coverage show?', answer: 'It estimates the percentage of stellar output required for a selected Kardashev rating.' },
19
- { question: 'Is this a precise design tool?', answer: 'No. It is an educational calculator for first-order physical intuition.' },
28
+ {
29
+ question: '戴森群是什么?',
30
+ answer: '戴森群是围绕恒星运行的大量独立收集器的集合,通常比刚性壳更具可行性。',
31
+ },
32
+ {
33
+ question: '为什么轨道半径会变化?',
34
+ answer: '轨道半径由收集器的工作温度和恒星光度共同决定。温度越高的收集器可以在更近的轨道上运行。',
35
+ },
36
+ {
37
+ question: '卡尔达肖夫评级是什么意思?',
38
+ answer: '它将捕获的瓦特数转换为对数制的文明能量等级。',
39
+ },
40
+ {
41
+ question: '材料质量精确吗?',
42
+ answer: '不精确。这是基于面积、面密度和稳定性假设的教育性估算。',
43
+ },
20
44
  ];
21
45
 
22
46
  export const content: ToolLocaleContent = {
@@ -24,41 +48,41 @@ export const content: ToolLocaleContent = {
24
48
  title,
25
49
  description,
26
50
  ui: {
27
- visualization: 'Dyson collector visualization',
28
- starType: 'Star type',
29
- structureType: 'Structure',
30
- coverage: 'Collector coverage',
31
- operatingTemp: 'Operating temperature',
32
- kardashevTarget: 'Kardashev target',
33
- kardashevRating: 'Current rating',
34
- capturedPower: 'Captured power',
35
- optimalRadius: 'Optimal radius',
36
- targetCoverage: 'Target coverage',
37
- materialMass: 'Material mass',
38
- captureMeter: 'Target capture progress',
39
- statusReady: 'Adjust the system to estimate collector demand.',
40
- statusUnderbuilt: 'Coverage is below the selected target.',
41
- statusBalanced: 'Coverage is close to the selected target.',
42
- statusExtreme: 'This configuration overshoots the target.',
43
- orbitalPeriod: 'Orbital period',
44
- collectorArea: 'Collector area',
45
- mercuryMasses: '{value} Mercury masses',
46
- kilograms: '{value} kg',
47
- daysUnit: '{value} days',
48
- starMDwarf: 'M dwarf',
49
- starSun: 'Sun-like G star',
50
- starA: 'A-type star',
51
- starRedGiant: 'Red giant',
52
- starBlueGiant: 'Blue giant',
53
- structureSwarm: 'Dyson swarm',
54
- structureRing: 'Equatorial ring',
55
- structureShell: 'Rigid shell',
56
- structureStatite: 'Statite mirror cloud',
51
+ visualization: '戴森收集器可视化',
52
+ starType: '恒星类型',
53
+ structureType: '结构',
54
+ coverage: '收集器覆盖率',
55
+ operatingTemp: '工作温度',
56
+ kardashevTarget: '卡尔达肖夫目标',
57
+ kardashevRating: '当前评级',
58
+ capturedPower: '捕获功率',
59
+ optimalRadius: '最优半径',
60
+ targetCoverage: '目标覆盖率',
61
+ materialMass: '材料质量',
62
+ captureMeter: '目标捕获进度',
63
+ statusReady: '调整系统以估算收集器需求。',
64
+ statusUnderbuilt: '覆盖率低于所选目标。',
65
+ statusBalanced: '覆盖率接近所选目标。',
66
+ statusExtreme: '此配置大幅超过目标。',
67
+ orbitalPeriod: '轨道周期',
68
+ collectorArea: '收集器面积',
69
+ mercuryMasses: '{value} 水星质量',
70
+ kilograms: '{value} 千克',
71
+ daysUnit: '{value} ',
72
+ starMDwarf: 'M型矮星',
73
+ starSun: '类太阳G型星',
74
+ starA: 'A型星',
75
+ starRedGiant: '红巨星',
76
+ starBlueGiant: '蓝巨星',
77
+ structureSwarm: '戴森群',
78
+ structureRing: '赤道环',
79
+ structureShell: '刚性壳',
80
+ structureStatite: '静态镜云',
57
81
  },
58
82
  seo: [
59
- { type: 'title', text: 'Dyson Sphere Energy Capture Simulator', level: 2 },
60
- { type: 'paragraph', html: 'This tool estimates power and material requirements for several Dyson megastructure concepts.' },
61
- { type: 'paragraph', html: 'It is useful for learning how stellar luminosity, temperature limits, and coverage affect large-scale energy capture.' },
83
+ { type: 'title', text: '戴森球能量捕获模拟器', level: 2 },
84
+ { type: 'paragraph', html: '本模拟器使用一阶恒星能量计算,比较戴森群、环形、壳体和静态云等概念。' },
85
+ { type: 'paragraph', html: '估算捕获功率、热轨道半径、收集器面积、轨道周期、材料质量以及达到卡尔达肖夫目标所需的覆盖率。' },
62
86
  ],
63
87
  faq,
64
88
  bibliography,
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { epidemicSirSimulator } 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 epidemicSirSimulator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
15
+
@@ -0,0 +1,37 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Kermack and McKendrick, A contribution to the mathematical theory of epidemics',
6
+ url: 'https://royalsocietypublishing.org/doi/10.1098/rspa.1927.0118',
7
+ },
8
+ {
9
+ name: 'Centers for Disease Control and Prevention, Principles of Epidemiology',
10
+ url: 'https://www.cdc.gov/csels/dsepd/ss1978/index.html',
11
+ },
12
+ {
13
+ name: 'World Health Organization, Vaccines and immunization',
14
+ url: 'https://www.who.int/health-topics/vaccines-and-immunization',
15
+ },
16
+ {
17
+ name: 'Anderson and May, Infectious Diseases of Humans: Dynamics and Control',
18
+ url: 'https://global.oup.com/academic/product/infectious-diseases-of-humans-9780198540403',
19
+ },
20
+ {
21
+ name: 'Keeling and Rohani, Modeling Infectious Diseases in Humans and Animals',
22
+ url: 'https://press.princeton.edu/books/hardcover/9780691116174/modeling-infectious-diseases-in-humans-and-animals',
23
+ },
24
+ {
25
+ name: 'WHO, Estimating mortality from COVID-19',
26
+ url: 'https://www.who.int/news-room/commentaries/detail/estimating-mortality-from-covid-19',
27
+ },
28
+ {
29
+ name: 'Fine, Herd immunity: history, theory, practice',
30
+ url: 'https://pmc.ncbi.nlm.nih.gov/articles/PMC1447608/',
31
+ },
32
+ {
33
+ name: 'Delamater et al., Complexity of the Basic Reproduction Number (R0)',
34
+ url: 'https://pmc.ncbi.nlm.nih.gov/articles/PMC6537322/',
35
+ },
36
+ ];
37
+
@@ -0,0 +1,384 @@
1
+ ---
2
+ import './epidemic-sir-simulator.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div
12
+ class="sir-lab"
13
+ id="sir-lab"
14
+ data-day-label={ui.dayLabel}
15
+ data-days-unit={ui.daysUnit}
16
+ data-infectious-label={ui.infectiousLabel}
17
+ data-re-label={ui.reLabel}
18
+ >
19
+ <div class="sir-card">
20
+ <header class="sir-map-top">
21
+ <div>
22
+ <p>{ui.liveScenario}</p>
23
+ <h2>{ui.curveChart}</h2>
24
+ </div>
25
+ </header>
26
+
27
+ <div class="sir-card-body">
28
+ <section class="sir-map" aria-label={ui.curveChart}>
29
+ <div class="sir-chart-shell">
30
+ <svg class="sir-chart" id="sir-chart" viewBox="0 0 760 430" role="img" aria-label={ui.curveChart}>
31
+ <defs>
32
+ <linearGradient id="sir-grid-fade" x1="0" x2="1" y1="0" y2="1">
33
+ <stop offset="0%" stop-color="#eef7f4" />
34
+ <stop offset="100%" stop-color="#ffffff" />
35
+ </linearGradient>
36
+ </defs>
37
+ <rect class="sir-chart-bg" x="0" y="0" width="760" height="430" rx="28"></rect>
38
+ <g class="sir-grid" id="sir-grid"></g>
39
+ <line class="sir-baseline" x1="40" x2="720" y1="364" y2="364"></line>
40
+ <path class="sir-area sir-area-susceptible" id="sir-area-susceptible"></path>
41
+ <path class="sir-area sir-area-infected" id="sir-area-infected"></path>
42
+ <path class="sir-area sir-area-recovered" id="sir-area-recovered"></path>
43
+ <path class="sir-line sir-line-susceptible" id="sir-line-susceptible"></path>
44
+ <path class="sir-line sir-line-infected" id="sir-line-infected"></path>
45
+ <path class="sir-line sir-line-recovered" id="sir-line-recovered"></path>
46
+ <path class="sir-line sir-line-deaths" id="sir-line-deaths"></path>
47
+ <g class="sir-day-marker" id="sir-day-marker">
48
+ <line y1="34" y2="364"></line>
49
+ <circle r="7"></circle>
50
+ </g>
51
+ </svg>
52
+ <div class="sir-readout" id="sir-readout" aria-hidden="true">
53
+ <strong id="sir-readout-day">{ui.dayLabel} 0</strong>
54
+ <span><i class="sir-key sir-key-s"></i><b id="sir-readout-s">0</b></span>
55
+ <span><i class="sir-key sir-key-i"></i><b id="sir-readout-i">0</b></span>
56
+ <span><i class="sir-key sir-key-r"></i><b id="sir-readout-r">0</b></span>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="sir-timeline">
61
+ <input id="sir-day" type="range" min="0" max="160" value="80" aria-label={ui.inspectDay} />
62
+ <output id="sir-day-output">{ui.dayLabel} 80</output>
63
+ </div>
64
+
65
+ <div class="sir-legend" aria-label={ui.legend}>
66
+ <span><i class="sir-key sir-key-s"></i>{ui.susceptible}</span>
67
+ <span><i class="sir-key sir-key-i"></i>{ui.infected}</span>
68
+ <span><i class="sir-key sir-key-r"></i>{ui.recovered}</span>
69
+ <span><i class="sir-key sir-key-d"></i>{ui.deaths}</span>
70
+ </div>
71
+ </section>
72
+
73
+ <section class="sir-control-deck" aria-label={ui.controls}>
74
+ <div class="sir-control-header">
75
+ <div>
76
+ <p>{ui.controls}</p>
77
+ <strong>{ui.modelName}</strong>
78
+ </div>
79
+ <div class="sir-pill" id="sir-effective-r">{ui.reLabel} 0.00</div>
80
+ </div>
81
+
82
+ <div class="sir-metrics">
83
+ <div>
84
+ <span>{ui.peakInfected}</span>
85
+ <strong id="sir-peak">0</strong>
86
+ </div>
87
+ <div>
88
+ <span>{ui.peakDay}</span>
89
+ <strong id="sir-peak-day">0</strong>
90
+ </div>
91
+ <div>
92
+ <span>{ui.attackRate}</span>
93
+ <strong id="sir-attack">0%</strong>
94
+ </div>
95
+ <div>
96
+ <span>{ui.estimatedDeaths}</span>
97
+ <strong id="sir-deaths">0</strong>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="sir-sliders">
102
+ <label class="sir-slider" for="sir-r0">
103
+ <span>{ui.r0}</span>
104
+ <output id="sir-r0-output">2.4</output>
105
+ <input id="sir-r0" type="range" min="0.6" max="7" step="0.1" value="2.4" />
106
+ </label>
107
+
108
+ <label class="sir-slider" for="sir-lethality">
109
+ <span>{ui.lethality}</span>
110
+ <output id="sir-lethality-output">1.0%</output>
111
+ <input id="sir-lethality" type="range" min="0" max="12" step="0.1" value="1" />
112
+ </label>
113
+
114
+ <label class="sir-slider" for="sir-vaccination">
115
+ <span>{ui.vaccination}</span>
116
+ <output id="sir-vaccination-output">35%</output>
117
+ <input id="sir-vaccination" type="range" min="0" max="95" step="1" value="35" />
118
+ </label>
119
+
120
+ <label class="sir-slider" for="sir-efficacy">
121
+ <span>{ui.vaccineEffectiveness}</span>
122
+ <output id="sir-efficacy-output">85%</output>
123
+ <input id="sir-efficacy" type="range" min="0" max="100" step="1" value="85" />
124
+ </label>
125
+
126
+ <label class="sir-slider" for="sir-infectious-days">
127
+ <span>{ui.infectiousDays}</span>
128
+ <output id="sir-infectious-output">7 {ui.daysUnit}</output>
129
+ <input id="sir-infectious-days" type="range" min="2" max="21" step="1" value="7" />
130
+ </label>
131
+
132
+ <label class="sir-slider" for="sir-initial">
133
+ <span>{ui.initialInfected}</span>
134
+ <output id="sir-initial-output">0.5%</output>
135
+ <input id="sir-initial" type="range" min="0.1" max="8" step="0.1" value="0.5" />
136
+ </label>
137
+ </div>
138
+ </section>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <script>
144
+ import { runSirSimulation } from './logic';
145
+
146
+ const root = document.getElementById('sir-lab');
147
+ const r0Input = document.getElementById('sir-r0') as HTMLInputElement | null;
148
+ const lethalityInput = document.getElementById('sir-lethality') as HTMLInputElement | null;
149
+ const vaccinationInput = document.getElementById('sir-vaccination') as HTMLInputElement | null;
150
+ const efficacyInput = document.getElementById('sir-efficacy') as HTMLInputElement | null;
151
+ const infectiousInput = document.getElementById('sir-infectious-days') as HTMLInputElement | null;
152
+ const initialInput = document.getElementById('sir-initial') as HTMLInputElement | null;
153
+ const dayInput = document.getElementById('sir-day') as HTMLInputElement | null;
154
+ const chart = document.getElementById('sir-chart') as unknown as SVGSVGElement | null;
155
+ const grid = document.getElementById('sir-grid');
156
+ const readout = document.getElementById('sir-readout');
157
+ const marker = document.getElementById('sir-day-marker');
158
+ const markerLine = marker?.querySelector('line');
159
+ const markerCircle = marker?.querySelector('circle');
160
+ const numberFormat = new Intl.NumberFormat('en', { maximumFractionDigits: 0 });
161
+ const storageKey = 'epidemic-sir-simulator:last-state';
162
+ const labels = {
163
+ day: root?.dataset.dayLabel ?? '',
164
+ days: root?.dataset.daysUnit ?? '',
165
+ infectious: root?.dataset.infectiousLabel ?? '',
166
+ re: root?.dataset.reLabel ?? '',
167
+ };
168
+
169
+ const paths = {
170
+ susceptible: document.getElementById('sir-line-susceptible'),
171
+ infected: document.getElementById('sir-line-infected'),
172
+ recovered: document.getElementById('sir-line-recovered'),
173
+ deaths: document.getElementById('sir-line-deaths'),
174
+ susceptibleArea: document.getElementById('sir-area-susceptible'),
175
+ infectedArea: document.getElementById('sir-area-infected'),
176
+ recoveredArea: document.getElementById('sir-area-recovered'),
177
+ };
178
+
179
+ function getValue(input: HTMLInputElement | null, fallback: number) {
180
+ return input ? Number(input.value) : fallback;
181
+ }
182
+
183
+ function trackedInputs() {
184
+ return [r0Input, lethalityInput, vaccinationInput, efficacyInput, infectiousInput, initialInput, dayInput];
185
+ }
186
+
187
+ function restoreState() {
188
+ try {
189
+ const raw = window.localStorage.getItem(storageKey);
190
+ if (!raw) return;
191
+ const state = JSON.parse(raw) as Record<string, string>;
192
+
193
+ trackedInputs().forEach((input) => {
194
+ if (!input || typeof state[input.id] !== 'string') return;
195
+ input.value = state[input.id];
196
+ });
197
+ } catch {
198
+ window.localStorage.removeItem(storageKey);
199
+ }
200
+ }
201
+
202
+ function saveState() {
203
+ const state: Record<string, string> = {};
204
+ trackedInputs().forEach((input) => {
205
+ if (!input) return;
206
+ state[input.id] = input.value;
207
+ });
208
+ window.localStorage.setItem(storageKey, JSON.stringify(state));
209
+ }
210
+
211
+ function scalePoint(day: number, value: number, days: number, population: number) {
212
+ const width = 680;
213
+ const height = 330;
214
+ const x = 40 + day / days * width;
215
+ const y = 364 - value / population * height;
216
+ return { x, y };
217
+ }
218
+
219
+ function makePath(values: number[], days: number, population: number) {
220
+ return values.map((value, index) => {
221
+ const point = scalePoint(index, value, days, population);
222
+ return `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
223
+ }).join(' ');
224
+ }
225
+
226
+ function makeArea(values: number[], days: number, population: number) {
227
+ const line = makePath(values, days, population);
228
+ return `${line} L720 364 L40 364 Z`;
229
+ }
230
+
231
+ function currentConfig(days: number, population: number) {
232
+ return {
233
+ population,
234
+ initialInfectedPercent: getValue(initialInput, 0.5),
235
+ r0: getValue(r0Input, 2.4),
236
+ infectiousDays: getValue(infectiousInput, 7),
237
+ vaccinationPercent: getValue(vaccinationInput, 35),
238
+ vaccineEffectiveness: getValue(efficacyInput, 85),
239
+ fatalityPercent: getValue(lethalityInput, 1),
240
+ days,
241
+ };
242
+ }
243
+
244
+ function setText(id: string, text: string) {
245
+ const node = document.getElementById(id);
246
+ if (!node) return;
247
+ node.classList.toggle('sir-value-long', text.length >= 6);
248
+ node.classList.toggle('sir-value-extra-long', text.length >= 8);
249
+ if (node.textContent === text) return;
250
+ node.textContent = text;
251
+ node.classList.remove('sir-value-flash');
252
+ node.getBoundingClientRect();
253
+ node.classList.add('sir-value-flash');
254
+ }
255
+
256
+ function retraceCurves() {
257
+ Object.values(paths).forEach((path) => {
258
+ if (!path || path.id.includes('area')) return;
259
+ path.style.animation = 'none';
260
+ path.getBoundingClientRect();
261
+ path.style.animation = '';
262
+ });
263
+ }
264
+
265
+ function positionSliderValue(input: HTMLInputElement | null, outputId: string) {
266
+ const output = document.getElementById(outputId);
267
+ if (!input || !output) return;
268
+ const min = Number(input.min);
269
+ const max = Number(input.max);
270
+ const value = Number(input.value);
271
+ const progress = (value - min) / (max - min);
272
+ output.style.setProperty('--value-x', `${Math.max(0, Math.min(1, progress)) * 100}%`);
273
+ }
274
+
275
+ function drawGrid() {
276
+ if (!grid || grid.childElementCount > 0) return;
277
+ const ns = 'http://www.w3.org/2000/svg';
278
+
279
+ for (let row = 0; row <= 4; row += 1) {
280
+ const line = document.createElementNS(ns, 'line');
281
+ const y = 34 + row * 82.5;
282
+ line.setAttribute('x1', '40');
283
+ line.setAttribute('x2', '720');
284
+ line.setAttribute('y1', y.toString());
285
+ line.setAttribute('y2', y.toString());
286
+ grid.append(line);
287
+ }
288
+
289
+ for (let column = 0; column <= 5; column += 1) {
290
+ const line = document.createElementNS(ns, 'line');
291
+ const x = 40 + column * 136;
292
+ line.setAttribute('x1', x.toString());
293
+ line.setAttribute('x2', x.toString());
294
+ line.setAttribute('y1', '34');
295
+ line.setAttribute('y2', '364');
296
+ grid.append(line);
297
+ }
298
+ }
299
+
300
+ function drawCurves(result: ReturnType<typeof runSirSimulation>, days: number, population: number) {
301
+ const susceptible = result.points.map((point) => point.susceptible);
302
+ const infected = result.points.map((point) => point.infected);
303
+ const recovered = result.points.map((point) => point.recovered + point.vaccinated);
304
+ const deaths = result.points.map((point) => point.deaths);
305
+
306
+ paths.susceptible?.setAttribute('d', makePath(susceptible, days, population));
307
+ paths.infected?.setAttribute('d', makePath(infected, days, population));
308
+ paths.recovered?.setAttribute('d', makePath(recovered, days, population));
309
+ paths.deaths?.setAttribute('d', makePath(deaths, days, population));
310
+ paths.susceptibleArea?.setAttribute('d', makeArea(susceptible, days, population));
311
+ paths.infectedArea?.setAttribute('d', makeArea(infected, days, population));
312
+ paths.recoveredArea?.setAttribute('d', makeArea(recovered, days, population));
313
+ }
314
+
315
+ function updateSliderOutputs() {
316
+ setText('sir-r0-output', getValue(r0Input, 2.4).toFixed(1));
317
+ setText('sir-lethality-output', `${getValue(lethalityInput, 1).toFixed(1)}%`);
318
+ setText('sir-vaccination-output', `${getValue(vaccinationInput, 35).toFixed(0)}%`);
319
+ setText('sir-efficacy-output', `${getValue(efficacyInput, 85).toFixed(0)}%`);
320
+ setText('sir-infectious-output', `${getValue(infectiousInput, 7).toFixed(0)} ${labels.days}`);
321
+ setText('sir-initial-output', `${getValue(initialInput, 0.5).toFixed(1)}%`);
322
+ positionSliderValue(r0Input, 'sir-r0-output');
323
+ positionSliderValue(lethalityInput, 'sir-lethality-output');
324
+ positionSliderValue(vaccinationInput, 'sir-vaccination-output');
325
+ positionSliderValue(efficacyInput, 'sir-efficacy-output');
326
+ positionSliderValue(infectiousInput, 'sir-infectious-output');
327
+ positionSliderValue(initialInput, 'sir-initial-output');
328
+ }
329
+
330
+ function updateMetrics(result: ReturnType<typeof runSirSimulation>) {
331
+ setText('sir-peak', numberFormat.format(result.summary.peakInfected));
332
+ setText('sir-peak-day', result.summary.peakDay.toString());
333
+ setText('sir-attack', `${(result.summary.attackRate * 100).toFixed(1)}%`);
334
+ setText('sir-deaths', numberFormat.format(result.summary.finalDeaths));
335
+ setText('sir-effective-r', `${labels.re} ${result.summary.effectiveR0.toFixed(2)}`);
336
+ }
337
+
338
+ function updateInspection(result: ReturnType<typeof runSirSimulation>, days: number, population: number) {
339
+ const inspectedDay = Math.min(days, getValue(dayInput, 80));
340
+ const inspected = result.points[inspectedDay] ?? result.points[0];
341
+ const infectedPoint = scalePoint(inspectedDay, inspected.infected, days, population);
342
+ markerLine?.setAttribute('x1', infectedPoint.x.toFixed(2));
343
+ markerLine?.setAttribute('x2', infectedPoint.x.toFixed(2));
344
+ markerCircle?.setAttribute('cx', infectedPoint.x.toFixed(2));
345
+ markerCircle?.setAttribute('cy', infectedPoint.y.toFixed(2));
346
+ setText('sir-day-output', `${labels.day} ${inspectedDay}: ${numberFormat.format(inspected.infected)} ${labels.infectious}`);
347
+ setText('sir-readout-day', `${labels.day} ${inspectedDay}`);
348
+ setText('sir-readout-s', numberFormat.format(inspected.susceptible));
349
+ setText('sir-readout-i', numberFormat.format(inspected.infected));
350
+ setText('sir-readout-r', numberFormat.format(inspected.recovered + inspected.vaccinated));
351
+
352
+ if (readout) {
353
+ const x = infectedPoint.x / 760 * 100;
354
+ readout.style.setProperty('--readout-x', `${Math.max(4, Math.min(76, x))}%`);
355
+ readout.style.setProperty('--readout-y', `${Math.max(12, Math.min(82, infectedPoint.y / 430 * 100))}%`);
356
+ }
357
+ }
358
+
359
+ function update() {
360
+ saveState();
361
+ const days = 160;
362
+ const population = 100000;
363
+ const result = runSirSimulation(currentConfig(days, population));
364
+ drawCurves(result, days, population);
365
+ updateSliderOutputs();
366
+ updateMetrics(result);
367
+ updateInspection(result, days, population);
368
+ retraceCurves();
369
+ }
370
+
371
+ drawGrid();
372
+ restoreState();
373
+ [r0Input, lethalityInput, vaccinationInput, efficacyInput, infectiousInput, initialInput, dayInput].forEach((input) => {
374
+ input?.addEventListener('input', update);
375
+ });
376
+ chart?.addEventListener('pointermove', (event) => {
377
+ if (!dayInput) return;
378
+ const rect = chart.getBoundingClientRect();
379
+ const localX = Math.max(40, Math.min(720, (event.clientX - rect.left) / rect.width * 760));
380
+ dayInput.value = Math.round((localX - 40) / 680 * 160).toString();
381
+ update();
382
+ });
383
+ update();
384
+ </script>
@@ -0,0 +1,30 @@
1
+ import type { ScienceToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export interface EpidemicSirSimulatorUI {
4
+ [key: string]: string;
5
+ }
6
+
7
+ export type EpidemicSirSimulatorLocaleContent = ToolLocaleContent<EpidemicSirSimulatorUI>;
8
+
9
+ export const epidemicSirSimulator: ScienceToolEntry<EpidemicSirSimulatorUI> = {
10
+ id: 'epidemic-sir-simulator',
11
+ icons: { bg: 'mdi:virus-outline', fg: 'mdi:chart-bell-curve' },
12
+ i18n: {
13
+ de: () => import('./i18n/de').then((m) => m.content),
14
+ en: () => import('./i18n/en').then((m) => m.content),
15
+ es: () => import('./i18n/es').then((m) => m.content),
16
+ fr: () => import('./i18n/fr').then((m) => m.content),
17
+ id: () => import('./i18n/id').then((m) => m.content),
18
+ it: () => import('./i18n/it').then((m) => m.content),
19
+ ja: () => import('./i18n/ja').then((m) => m.content),
20
+ ko: () => import('./i18n/ko').then((m) => m.content),
21
+ nl: () => import('./i18n/nl').then((m) => m.content),
22
+ pl: () => import('./i18n/pl').then((m) => m.content),
23
+ pt: () => import('./i18n/pt').then((m) => m.content),
24
+ ru: () => import('./i18n/ru').then((m) => m.content),
25
+ sv: () => import('./i18n/sv').then((m) => m.content),
26
+ tr: () => import('./i18n/tr').then((m) => m.content),
27
+ zh: () => import('./i18n/zh').then((m) => m.content),
28
+ },
29
+ };
30
+