@jjlmoya/utils-science 1.24.0 → 1.26.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 (60) 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 -1
  5. package/src/tests/locale_completeness.test.ts +2 -3
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/lorenz-attractor/i18n/es.ts +12 -4
  8. package/src/tool/lorenz-attractor/lorenz-attractor.css +56 -25
  9. package/src/tool/radioactive-decay/bibliography.astro +15 -0
  10. package/src/tool/radioactive-decay/bibliography.ts +17 -0
  11. package/src/tool/radioactive-decay/component.astro +346 -0
  12. package/src/tool/radioactive-decay/entry.ts +26 -0
  13. package/src/tool/radioactive-decay/i18n/de.ts +78 -0
  14. package/src/tool/radioactive-decay/i18n/en.ts +223 -0
  15. package/src/tool/radioactive-decay/i18n/es.ts +106 -0
  16. package/src/tool/radioactive-decay/i18n/fr.ts +78 -0
  17. package/src/tool/radioactive-decay/i18n/id.ts +66 -0
  18. package/src/tool/radioactive-decay/i18n/it.ts +79 -0
  19. package/src/tool/radioactive-decay/i18n/ja.ts +65 -0
  20. package/src/tool/radioactive-decay/i18n/ko.ts +65 -0
  21. package/src/tool/radioactive-decay/i18n/nl.ts +72 -0
  22. package/src/tool/radioactive-decay/i18n/pl.ts +65 -0
  23. package/src/tool/radioactive-decay/i18n/pt.ts +78 -0
  24. package/src/tool/radioactive-decay/i18n/ru.ts +66 -0
  25. package/src/tool/radioactive-decay/i18n/sv.ts +66 -0
  26. package/src/tool/radioactive-decay/i18n/tr.ts +66 -0
  27. package/src/tool/radioactive-decay/i18n/zh.ts +65 -0
  28. package/src/tool/radioactive-decay/index.ts +12 -0
  29. package/src/tool/radioactive-decay/logic.test.ts +20 -0
  30. package/src/tool/radioactive-decay/logic.ts +120 -0
  31. package/src/tool/radioactive-decay/radioactive-decay-half-life-calculator.css +435 -0
  32. package/src/tool/radioactive-decay/seo.astro +16 -0
  33. package/src/tool/stellar-habitability-zone/bibliography.astro +14 -0
  34. package/src/tool/stellar-habitability-zone/bibliography.ts +12 -0
  35. package/src/tool/stellar-habitability-zone/component.astro +123 -0
  36. package/src/tool/stellar-habitability-zone/dom-updater.ts +94 -0
  37. package/src/tool/stellar-habitability-zone/entry.ts +26 -0
  38. package/src/tool/stellar-habitability-zone/i18n/de.ts +189 -0
  39. package/src/tool/stellar-habitability-zone/i18n/en.ts +189 -0
  40. package/src/tool/stellar-habitability-zone/i18n/es.ts +189 -0
  41. package/src/tool/stellar-habitability-zone/i18n/fr.ts +189 -0
  42. package/src/tool/stellar-habitability-zone/i18n/id.ts +189 -0
  43. package/src/tool/stellar-habitability-zone/i18n/it.ts +189 -0
  44. package/src/tool/stellar-habitability-zone/i18n/ja.ts +189 -0
  45. package/src/tool/stellar-habitability-zone/i18n/ko.ts +189 -0
  46. package/src/tool/stellar-habitability-zone/i18n/nl.ts +189 -0
  47. package/src/tool/stellar-habitability-zone/i18n/pl.ts +189 -0
  48. package/src/tool/stellar-habitability-zone/i18n/pt.ts +189 -0
  49. package/src/tool/stellar-habitability-zone/i18n/ru.ts +189 -0
  50. package/src/tool/stellar-habitability-zone/i18n/sv.ts +189 -0
  51. package/src/tool/stellar-habitability-zone/i18n/tr.ts +189 -0
  52. package/src/tool/stellar-habitability-zone/i18n/zh.ts +189 -0
  53. package/src/tool/stellar-habitability-zone/index.ts +11 -0
  54. package/src/tool/stellar-habitability-zone/interaction.ts +45 -0
  55. package/src/tool/stellar-habitability-zone/logic/StellarHabitabilityEngine.ts +158 -0
  56. package/src/tool/stellar-habitability-zone/renderer.ts +241 -0
  57. package/src/tool/stellar-habitability-zone/script.ts +273 -0
  58. package/src/tool/stellar-habitability-zone/seo.astro +15 -0
  59. package/src/tool/stellar-habitability-zone/stellar-habitability-zone.css +375 -0
  60. package/src/tools.ts +4 -1
@@ -0,0 +1,189 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ const slug = 'stellar-habitability-zone';
5
+ const description = '使用恒星和行星配置计算并可视化不同类型恒星周围的宜居带(黄金带)。';
6
+ const title = '恒星宜居带模拟器:寻找黄金带';
7
+
8
+ const howTo = [
9
+ {
10
+ name: '选择恒星预设',
11
+ text: '从蓝巨星到红矮星,选择恒星类型以加载标准物理属性,如质量、光度和温度。',
12
+ },
13
+ {
14
+ name: '调整行星参数',
15
+ text: '使用交互式滑块修改行星的轨道距离(半长轴)、反照率和大气温室效应升温。',
16
+ },
17
+ {
18
+ name: '分析轨道与宜居性',
19
+ text: '实时观察行星绕恒星运行。检查平衡温度与表面温度是否支持液态水。',
20
+ },
21
+ ];
22
+
23
+ const faq = [
24
+ {
25
+ question: '什么是恒星宜居带?',
26
+ answer: '恒星宜居带(通常称为黄金带)是指恒星周围的一个区域,在该区域内,行星表面温度足以在给定大气压下维持液态水。它基于恒星光度和有效温度,由保守边界和乐观边界共同定义。',
27
+ },
28
+ {
29
+ question: '恒星光度如何影响宜居带?',
30
+ answer: '恒星光度决定了恒星的总能量输出。更热、更大质量的恒星(如O型、B型或A型星)极其明亮,使其宜居带位于更远的地方。更冷、质量更小的恒星(如K型或M型红矮星)光度较低,使其宜居带非常靠近恒星。',
31
+ },
32
+ {
33
+ question: '平衡温度和表面温度有什么区别?',
34
+ answer: '平衡温度是假设行星表现为吸收恒星辐射并将其重新辐射回太空的黑体时的理论温度。表面温度则包括了行星大气的温室效应,这种效应会捕获热量并进一步加热行星。',
35
+ },
36
+ {
37
+ question: '为什么反照率对行星宜居性很重要?',
38
+ answer: '反照率是行星表面反射率的度量。较高的反照率(接近1.0)意味着行星反射更多入射的恒星光线,从而冷却。较低的反照率意味着吸收更多辐射,从而提高整体温度。',
39
+ },
40
+ ];
41
+
42
+ export const content: ToolLocaleContent = {
43
+ slug,
44
+ title,
45
+ description,
46
+ ui: {
47
+ title: '恒星宜居带模拟器',
48
+ starPresetsLabel: '光谱预设',
49
+ customStarHeader: '恒星参数',
50
+ starTemperature: '有效温度 (K)',
51
+ starLuminosity: '光度 (L/L⊙)',
52
+ starMass: '质量 (M/M⊙)',
53
+ starRadius: '半径 (R/R⊙)',
54
+ planetHeader: '行星参数',
55
+ planetDistance: '轨道距离 (AU)',
56
+ planetAlbedo: '邦德反照率',
57
+ greenhouseDelta: '温室效应升温 (K)',
58
+ resultsHeader: '模拟结果',
59
+ stellarFluxResult: '接收到的恒星通量',
60
+ eqTempResult: '平衡温度',
61
+ surfTempResult: '估计表面温度',
62
+ orbitPeriodResult: '轨道周期',
63
+ orbitVelocityResult: '轨道速度',
64
+ hzLimitsHeader: '宜居带边界',
65
+ innerLimit: '保守内边界',
66
+ outerLimit: '保守外边界',
67
+ optInnerLimit: '乐观内边界',
68
+ optOuterLimit: '乐观外边界',
69
+ orbitCanvasTitle: '交互式轨道可视化器',
70
+ statusLabel: '宜居状态',
71
+ statusTooHot: '太热(水蒸发)',
72
+ statusHabitable: '宜居(可能存在液态水)',
73
+ statusTooCold: '太冷(水结冰)',
74
+ statusExplanation: '基于保守边界,该行星位于指定的宜居带状态内。',
75
+ unitsLabel: '单位制',
76
+ unitsScientific: '科学单位',
77
+ unitsImperial: '英制单位',
78
+ },
79
+ seo: [
80
+ {
81
+ type: 'title',
82
+ text: '天体生物学:恒星宜居带的物理学',
83
+ level: 2,
84
+ },
85
+ {
86
+ type: 'paragraph',
87
+ html: '寻找地球以外的生命始于理解液态水所需的物理条件。天体生物学家使用数学模型来绘制不同类型恒星周围宜居带的边界。该模拟器使用Kopparapu等人(2013)的模型来估计行星接收到的能量通量,并判断它们是否位于黄金带内。宜居带定义为具有CO2-H2O-N2大气的类地行星能够在其表面维持液态水的区域。',
88
+ },
89
+ {
90
+ type: 'title',
91
+ text: '数学公式与大气物理学',
92
+ level: 3,
93
+ },
94
+ {
95
+ type: 'paragraph',
96
+ html: '宜居带的边界通过计算引发失控温室或最大温室条件所需的有效恒星通量(Seff)来确定。Seff的方程取决于恒星的有效温度(Teff):<br><br>Seff = SeffSun + a * T* + b * T*^2 + c * T*^3 + d * T*^4<br><br>其中T* = Teff - 5780 K,系数(a, b, c, d)由一维辐射对流气候模型经验推导得出。计算出Seff后,以天文单位(AU)为单位的轨道距离d由下式给出:<br><br>d = sqrt(L / Seff)<br><br>其中L是恒星相对于太阳的光度。行星的平衡温度(Teq)假设为处于热平衡的球形黑体进行计算:<br><br>Teq = Teff * sqrt(R* / 2d) * (1 - A)^0.25 = 278.5 * (S * (1 - A))^0.25<br><br>其中R*是恒星半径,A是行星邦德反照率,S是以地球太阳常数单位接收到的恒星通量。',
97
+ },
98
+ {
99
+ type: 'title',
100
+ text: '光谱分类与宜居边界',
101
+ level: 3,
102
+ },
103
+ {
104
+ type: 'paragraph',
105
+ html: '恒星特征在不同光谱类型之间差异很大。以下是典型特性和HZ边界的总结:',
106
+ },
107
+ {
108
+ type: 'table',
109
+ headers: [
110
+ '光谱类型',
111
+ '温度 (K)',
112
+ '光度 (L/L⊙)',
113
+ 'HZ内边界 (AU)',
114
+ 'HZ外边界 (AU)',
115
+ ],
116
+ rows: [
117
+ ['O型巨星', '40,000', '100,000', '300.0', '530.0'],
118
+ ['B型巨星', '20,000', '1,000', '30.1', '53.2'],
119
+ ['A型(天狼星)', '8,500', '20.0', '4.2', '7.4'],
120
+ ['F型(南河三)', '6,500', '2.5', '1.5', '2.6'],
121
+ ['G型(太阳)', '5,778', '1.0', '0.95', '1.67'],
122
+ ['K型矮星', '4,500', '0.15', '0.37', '0.65'],
123
+ ['M型矮星', '3,200', '0.01', '0.09', '0.17'],
124
+ ],
125
+ },
126
+ {
127
+ type: 'title',
128
+ text: '光谱类型对宜居性的影响',
129
+ level: 3,
130
+ },
131
+ {
132
+ type: 'paragraph',
133
+ html: '每个光谱类型都为其行星创造了独特的辐射和引力环境:<br><br><strong>O型和B型星:</strong>这些大质量的蓝色恒星发出强烈的紫外线辐射,寿命极短(数千万年)。液态水可能存在于它们的外部世界,但在恒星发生超新星爆发之前,生命没有足够的时间进化。<br><br><strong>A型和F型星:</strong>这些恒星比太阳更亮、更热。它们的宜居带宽阔且遥远,最大限度地减少了潮汐锁定的影响。然而,如果没有保护性臭氧层,高强度的近紫外辐射会对有机分子造成严重损害。<br><br><strong>G型星(类太阳):</strong>提供数十亿年的稳定光通量,这些恒星是生命搜索的主要目标。它们的辐射输出符合标准生物化学的要求。<br><br><strong>K型星(橙矮星):</strong>被许多天体生物学家认为是"超级宜居"的宿主,橙矮星的寿命长达数百亿年,发出的有害紫外线比G型星少,也不像年轻的M型矮星那样容易产生强烈的耀斑。<br><br><strong>M型星(红矮星):</strong>银河系中最常见的恒星。由于它们的宜居带非常接近(通常 < 0.2 AU),行星容易发生潮汐锁定,即一面永久朝向恒星。此外,活跃的M型矮星会产生高能恒星风和耀斑,可能剥离行星大气。',
134
+ },
135
+ {
136
+ type: 'title',
137
+ text: '行星宜居环境的关键因素',
138
+ level: 3,
139
+ },
140
+ {
141
+ type: 'paragraph',
142
+ html: '行星的物理环境由多个变量塑造,而不仅仅是与主恒星的距离:',
143
+ },
144
+ {
145
+ type: 'list',
146
+ items: [
147
+ '<strong>大气温室效应:</strong>自然温室气体将表面温度提升至黑体平衡水平以上。类地行星需要碳硅酸盐循环来稳定大气中的CO2,并在地质时间尺度上调节温度。',
148
+ '<strong>行星邦德反照率:</strong>高反射率(由于云层、冰盖或硫酸盐气溶胶)会冷却行星,而低反射率(深色土壤、水体)则会捕获更多恒星能量。',
149
+ '<strong>磁场:</strong>强大的行星磁层保护大气免受恒星风的影响,防止非热大气逃逸和水分流失。',
150
+ '<strong>水陷阱动力学:</strong>上层大气中的冷阱效应阻止水蒸气到达高海拔地区,避免太阳紫外辐射将其解离为氢和氧。',
151
+ ],
152
+ },
153
+ ],
154
+ faq,
155
+ bibliography,
156
+ howTo,
157
+ schemas: [
158
+ {
159
+ '@context': 'https://schema.org',
160
+ '@type': 'SoftwareApplication',
161
+ name: title,
162
+ description: description,
163
+ applicationCategory: 'ScientificApplication',
164
+ operatingSystem: 'Any',
165
+ },
166
+ {
167
+ '@context': 'https://schema.org',
168
+ '@type': 'FAQPage',
169
+ mainEntity: faq.map((item) => ({
170
+ '@type': 'Question',
171
+ name: item.question,
172
+ acceptedAnswer: {
173
+ '@type': 'Answer',
174
+ text: item.answer,
175
+ },
176
+ })),
177
+ },
178
+ {
179
+ '@context': 'https://schema.org',
180
+ '@type': 'HowTo',
181
+ name: title,
182
+ step: howTo.map((step) => ({
183
+ '@type': 'HowToStep',
184
+ name: step.name,
185
+ text: step.text,
186
+ })),
187
+ },
188
+ ],
189
+ };
@@ -0,0 +1,11 @@
1
+ import { stellarHabitabilityZone } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+
4
+ export * from './entry';
5
+
6
+ export const STELLAR_HABITABILITY_ZONE_TOOL: ToolDefinition = {
7
+ entry: stellarHabitabilityZone,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,45 @@
1
+ interface InteractionParams {
2
+ canvasContainer: HTMLElement;
3
+ distInput: HTMLInputElement;
4
+ presetBtns: NodeListOf<Element>;
5
+ getCurDistanceAu: () => number;
6
+ getCurMaxLimit: () => number;
7
+ getCurRunawayLimit: () => number;
8
+ onUpdate: () => void;
9
+ }
10
+
11
+ function handleInteraction(clientX: number, clientY: number, p: InteractionParams) {
12
+ const rect = p.canvasContainer.getBoundingClientRect();
13
+ const distPx = Math.sqrt(Math.pow(clientX - rect.left - p.canvasContainer.clientWidth / 2, 2) + Math.pow(clientY - rect.top - p.canvasContainer.clientHeight / 2, 2));
14
+ const maxDist = Math.max(p.getCurDistanceAu() * 1.3, p.getCurMaxLimit() * 1.25);
15
+ const scale = (Math.min(p.canvasContainer.clientWidth, p.canvasContainer.clientHeight) * 0.4) / maxDist;
16
+ p.distInput.value = Math.log10(Math.max(0.01, Math.min(1000, distPx / scale))).toFixed(2);
17
+ p.presetBtns.forEach(b => b.classList.remove('active'));
18
+ p.onUpdate();
19
+ }
20
+
21
+ export function setupCanvasInteraction(p: InteractionParams) {
22
+ let isDrawing = false;
23
+ const add = (ev: string, cb: (e: Event) => void) => p.canvasContainer.addEventListener(ev, cb as EventListener);
24
+
25
+ add('mousedown', (e) => {
26
+ isDrawing = true;
27
+ handleInteraction((e as MouseEvent).clientX, (e as MouseEvent).clientY, p);
28
+ });
29
+ add('mousemove', (e) => {
30
+ if (isDrawing) handleInteraction((e as MouseEvent).clientX, (e as MouseEvent).clientY, p);
31
+ });
32
+ window.addEventListener('mouseup', () => { isDrawing = false; });
33
+ add('touchstart', (e) => {
34
+ const te = e as TouchEvent;
35
+ if (te.touches.length > 0) {
36
+ isDrawing = true;
37
+ handleInteraction(te.touches[0].clientX, te.touches[0].clientY, p);
38
+ }
39
+ });
40
+ add('touchmove', (e) => {
41
+ const te = e as TouchEvent;
42
+ if (isDrawing && te.touches.length > 0) handleInteraction(te.touches[0].clientX, te.touches[0].clientY, p);
43
+ });
44
+ add('touchend', () => { isDrawing = false; });
45
+ }
@@ -0,0 +1,158 @@
1
+ export interface StellarPreset {
2
+ type: string;
3
+ name: string;
4
+ temperature: number;
5
+ luminosity: number;
6
+ mass: number;
7
+ radius: number;
8
+ color: string;
9
+ }
10
+
11
+ export interface HabitabilityLimits {
12
+ recentVenus: number;
13
+ runawayGreenhouse: number;
14
+ maximumGreenhouse: number;
15
+ earlyMars: number;
16
+ }
17
+
18
+ export interface SimulationResult {
19
+ hzLimits: HabitabilityLimits;
20
+ equilibriumTemperature: number;
21
+ surfaceTemperature: number;
22
+ orbitalPeriod: number;
23
+ orbitalVelocity: number;
24
+ stellarFlux: number;
25
+ status: 'too-hot' | 'habitable' | 'too-cold';
26
+ }
27
+
28
+ export const STELLAR_PRESETS: StellarPreset[] = [
29
+ {
30
+ type: 'O',
31
+ name: 'O-Type (Blue Hypergiant)',
32
+ temperature: 40000,
33
+ luminosity: 100000,
34
+ mass: 50,
35
+ radius: 15,
36
+ color: '#00bfff',
37
+ },
38
+ {
39
+ type: 'B',
40
+ name: 'B-Type (Blue Giant)',
41
+ temperature: 20000,
42
+ luminosity: 1000,
43
+ mass: 8,
44
+ radius: 4,
45
+ color: '#87cefa',
46
+ },
47
+ {
48
+ type: 'A',
49
+ name: 'A-Type (Sirius-like)',
50
+ temperature: 8500,
51
+ luminosity: 20,
52
+ mass: 2.1,
53
+ radius: 1.7,
54
+ color: '#e0ffff',
55
+ },
56
+ {
57
+ type: 'F',
58
+ name: 'F-Type (Procyon-like)',
59
+ temperature: 6500,
60
+ luminosity: 2.5,
61
+ mass: 1.4,
62
+ radius: 1.3,
63
+ color: '#f0fff0',
64
+ },
65
+ {
66
+ type: 'G',
67
+ name: 'G-Type (Sun-like)',
68
+ temperature: 5778,
69
+ luminosity: 1.0,
70
+ mass: 1.0,
71
+ radius: 1.0,
72
+ color: '#ffff00',
73
+ },
74
+ {
75
+ type: 'K',
76
+ name: 'K-Type (Orange Dwarf)',
77
+ temperature: 4500,
78
+ luminosity: 0.15,
79
+ mass: 0.7,
80
+ radius: 0.7,
81
+ color: '#ffa500',
82
+ },
83
+ {
84
+ type: 'M',
85
+ name: 'M-Type (Red Dwarf)',
86
+ temperature: 3200,
87
+ luminosity: 0.01,
88
+ mass: 0.2,
89
+ radius: 0.2,
90
+ color: '#ff4500',
91
+ },
92
+ ];
93
+
94
+ export interface SeffParams {
95
+ tEff: number;
96
+ seffSun: number;
97
+ a: number;
98
+ b: number;
99
+ c: number;
100
+ d: number;
101
+ }
102
+
103
+ export interface SimulateParams {
104
+ luminosity: number;
105
+ temperature: number;
106
+ mass: number;
107
+ distanceAu: number;
108
+ albedo: number;
109
+ greenhouseDeltaK: number;
110
+ }
111
+
112
+ export class StellarHabitabilityEngine {
113
+ private calculateSeff(params: SeffParams): number {
114
+ const tClamp = Math.max(2600, Math.min(7200, params.tEff));
115
+ const tStar = tClamp - 5780;
116
+ return params.seffSun + params.a * tStar + params.b * Math.pow(tStar, 2) + params.c * Math.pow(tStar, 3) + params.d * Math.pow(tStar, 4);
117
+ }
118
+
119
+ public calculateLimits(luminosity: number, temperature: number): HabitabilityLimits {
120
+ const rvSeff = this.calculateSeff({ tEff: temperature, seffSun: 1.776, a: 2.136e-4, b: 2.533e-8, c: -1.332e-11, d: -3.097e-15 });
121
+ const rgSeff = this.calculateSeff({ tEff: temperature, seffSun: 1.107, a: 1.332e-4, b: 1.587e-8, c: -8.308e-12, d: -1.931e-16 });
122
+ const mgSeff = this.calculateSeff({ tEff: temperature, seffSun: 0.356, a: 6.171e-5, b: 7.389e-9, c: -3.865e-12, d: -9.000e-17 });
123
+ const emSeff = this.calculateSeff({ tEff: temperature, seffSun: 0.320, a: 5.547e-5, b: 6.641e-9, c: -3.474e-12, d: -8.087e-17 });
124
+
125
+ return {
126
+ recentVenus: Math.sqrt(luminosity / rvSeff),
127
+ runawayGreenhouse: Math.sqrt(luminosity / rgSeff),
128
+ maximumGreenhouse: Math.sqrt(luminosity / mgSeff),
129
+ earlyMars: Math.sqrt(luminosity / emSeff),
130
+ };
131
+ }
132
+
133
+ public simulate(params: SimulateParams): SimulationResult {
134
+ const hzLimits = this.calculateLimits(params.luminosity, params.temperature);
135
+ const stellarFlux = params.luminosity / Math.pow(params.distanceAu, 2);
136
+ const equilibriumTemperature = 278.5 * Math.pow(stellarFlux * (1.0 - params.albedo), 0.25);
137
+ const surfaceTemperature = equilibriumTemperature + params.greenhouseDeltaK;
138
+ const orbitalPeriod = Math.sqrt(Math.pow(params.distanceAu, 3) / params.mass) * 365.255;
139
+ const orbitalVelocity = 29.78 / Math.sqrt(params.distanceAu);
140
+
141
+ let status: 'too-hot' | 'habitable' | 'too-cold' = 'habitable';
142
+ if (params.distanceAu < hzLimits.runawayGreenhouse) {
143
+ status = 'too-hot';
144
+ } else if (params.distanceAu > hzLimits.maximumGreenhouse) {
145
+ status = 'too-cold';
146
+ }
147
+
148
+ return {
149
+ hzLimits,
150
+ equilibriumTemperature,
151
+ surfaceTemperature,
152
+ orbitalPeriod,
153
+ orbitalVelocity,
154
+ stellarFlux,
155
+ status,
156
+ };
157
+ }
158
+ }
@@ -0,0 +1,241 @@
1
+ export interface RenderState {
2
+ cx: number;
3
+ cy: number;
4
+ runawayGreenhousePx: number;
5
+ maximumGreenhousePx: number;
6
+ planetDistPx: number;
7
+ isTooHot: boolean;
8
+ isTooCold: boolean;
9
+ curMass: number;
10
+ curLuminosity: number;
11
+ curTemp: number;
12
+ curDistanceAu: number;
13
+ }
14
+
15
+ interface BackgroundStar {
16
+ x: number;
17
+ y: number;
18
+ size: number;
19
+ speed: number;
20
+ offset: number;
21
+ baseOpacity: number;
22
+ }
23
+
24
+ interface CosmicDust {
25
+ distanceFactor: number;
26
+ angle: number;
27
+ speed: number;
28
+ size: number;
29
+ opacity: number;
30
+ }
31
+
32
+ export class StellarHabitabilityRenderer {
33
+ private canvas: HTMLCanvasElement;
34
+ private canvasContainer: HTMLElement;
35
+ private ctx: CanvasRenderingContext2D;
36
+ private bgStars: BackgroundStar[];
37
+ private dustParticles: CosmicDust[];
38
+ private orbitAngle = 0;
39
+
40
+ constructor(canvas: HTMLCanvasElement, canvasContainer: HTMLElement) {
41
+ this.canvas = canvas;
42
+ this.canvasContainer = canvasContainer;
43
+ this.ctx = canvas.getContext('2d')!;
44
+
45
+ this.bgStars = Array.from({ length: 45 }, () => ({
46
+ x: Math.random(),
47
+ y: Math.random(),
48
+ size: 0.6 + Math.random() * 0.9,
49
+ speed: 0.001 + Math.random() * 0.003,
50
+ offset: Math.random() * Math.PI * 2,
51
+ baseOpacity: 0.2 + Math.random() * 0.5,
52
+ }));
53
+
54
+ this.dustParticles = Array.from({ length: 60 }, () => ({
55
+ distanceFactor: 0.15 + Math.random() * 2.0,
56
+ angle: Math.random() * Math.PI * 2,
57
+ speed: 0.005 + Math.random() * 0.015,
58
+ size: 0.4 + Math.random() * 0.8,
59
+ opacity: 0.15 + Math.random() * 0.45,
60
+ }));
61
+ }
62
+
63
+ public getContext(): CanvasRenderingContext2D {
64
+ return this.ctx;
65
+ }
66
+
67
+ public resize(): { w: number; h: number } {
68
+ const dpr = window.devicePixelRatio || 1;
69
+ const container = this.canvas.parentElement as HTMLElement;
70
+ this.canvas.width = container.clientWidth * dpr;
71
+ this.canvas.height = container.clientHeight * dpr;
72
+ this.ctx.scale(dpr, dpr);
73
+ return {
74
+ w: container.clientWidth,
75
+ h: container.clientHeight,
76
+ };
77
+ }
78
+
79
+ public draw(state: RenderState, w: number, h: number) {
80
+ this.ctx.clearRect(0, 0, w, h);
81
+ this.drawBackground(w, h);
82
+ this.drawStars(w, h);
83
+ this.drawNebulae(w, h);
84
+ this.drawHabitableZone(state);
85
+ this.drawDust(state);
86
+ this.drawOrbitLine(state);
87
+ this.drawStar(state);
88
+ this.drawPlanet(state);
89
+ }
90
+
91
+ private drawBackground(w: number, h: number) {
92
+ this.ctx.fillStyle = '#050507';
93
+ this.ctx.fillRect(0, 0, w, h);
94
+ }
95
+
96
+ private drawStars(w: number, h: number) {
97
+ this.ctx.fillStyle = '#ffffff';
98
+ this.bgStars.forEach(s => {
99
+ const sx = s.x * w;
100
+ const sy = s.y * h;
101
+ const alpha = s.baseOpacity + Math.sin(Date.now() * s.speed + s.offset) * 0.25;
102
+ this.ctx.globalAlpha = Math.max(0.1, Math.min(1.0, alpha));
103
+ this.ctx.beginPath();
104
+ this.ctx.arc(sx, sy, s.size, 0, Math.PI * 2);
105
+ this.ctx.fill();
106
+ });
107
+ this.ctx.globalAlpha = 1.0;
108
+ }
109
+
110
+ private drawNebulae(w: number, h: number) {
111
+ const nebula1 = this.ctx.createRadialGradient(w * 0.25, h * 0.3, 0, w * 0.25, h * 0.3, 160);
112
+ nebula1.addColorStop(0, 'rgba(139, 92, 246, 0.04)');
113
+ nebula1.addColorStop(0.5, 'rgba(139, 92, 246, 0.01)');
114
+ nebula1.addColorStop(1, 'rgba(139, 92, 246, 0)');
115
+ this.ctx.fillStyle = nebula1;
116
+ this.ctx.beginPath();
117
+ this.ctx.arc(w * 0.25, h * 0.3, 160, 0, Math.PI * 2);
118
+ this.ctx.fill();
119
+
120
+ const nebula2 = this.ctx.createRadialGradient(w * 0.7, h * 0.65, 0, w * 0.7, h * 0.65, 200);
121
+ nebula2.addColorStop(0, 'rgba(14, 165, 233, 0.03)');
122
+ nebula2.addColorStop(0.5, 'rgba(14, 165, 233, 0.01)');
123
+ nebula2.addColorStop(1, 'rgba(14, 165, 233, 0)');
124
+ this.ctx.fillStyle = nebula2;
125
+ this.ctx.beginPath();
126
+ this.ctx.arc(w * 0.7, h * 0.65, 200, 0, Math.PI * 2);
127
+ this.ctx.fill();
128
+ }
129
+
130
+ private drawHabitableZone(state: RenderState) {
131
+ const fluctuation = Math.sin(Date.now() * 0.002) * 0.03;
132
+ const runawayFluct = state.runawayGreenhousePx * (1 + fluctuation);
133
+ const maxFluct = state.maximumGreenhousePx * (1 + fluctuation);
134
+
135
+ const grad = this.ctx.createRadialGradient(state.cx, state.cy, runawayFluct, state.cx, state.cy, maxFluct);
136
+ grad.addColorStop(0, 'rgba(16, 185, 129, 0.0)');
137
+ grad.addColorStop(0.2, 'rgba(16, 185, 129, 0.12)');
138
+ grad.addColorStop(0.5, 'rgba(16, 185, 129, 0.18)');
139
+ grad.addColorStop(0.8, 'rgba(16, 185, 129, 0.12)');
140
+ grad.addColorStop(1, 'rgba(16, 185, 129, 0.0)');
141
+
142
+ this.ctx.fillStyle = grad;
143
+ this.ctx.beginPath();
144
+ this.ctx.arc(state.cx, state.cy, maxFluct * 1.1, 0, Math.PI * 2);
145
+ this.ctx.fill();
146
+ }
147
+
148
+ private drawDust(state: RenderState) {
149
+ this.ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
150
+ const dustSpeedFactor = 0.5 / (Math.sqrt(state.curMass) || 1);
151
+ this.dustParticles.forEach(p => {
152
+ p.angle += p.speed * dustSpeedFactor;
153
+ const pxFactor = p.distanceFactor * (state.runawayGreenhousePx + state.maximumGreenhousePx) * 0.5;
154
+ const dx = state.cx + Math.cos(p.angle) * pxFactor;
155
+ const dy = state.cy + Math.sin(p.angle) * pxFactor;
156
+ this.ctx.globalAlpha = p.opacity * Math.min(1.0, state.curLuminosity);
157
+ this.ctx.beginPath();
158
+ this.ctx.arc(dx, dy, p.size, 0, Math.PI * 2);
159
+ this.ctx.fill();
160
+ });
161
+ this.ctx.globalAlpha = 1.0;
162
+ }
163
+
164
+ private drawOrbitLine(state: RenderState) {
165
+ this.ctx.lineWidth = 1.0;
166
+ if (state.isTooHot) {
167
+ const warningFlash = Math.floor(Date.now() / 250) % 2 === 0;
168
+ this.ctx.strokeStyle = warningFlash ? 'rgba(239, 68, 68, 0.8)' : 'rgba(239, 68, 68, 0.25)';
169
+ this.ctx.setLineDash([2, 2]);
170
+ } else if (state.isTooCold) {
171
+ this.ctx.strokeStyle = 'rgba(59, 130, 246, 0.45)';
172
+ this.ctx.setLineDash([1, 4]);
173
+ } else {
174
+ this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
175
+ this.ctx.setLineDash([]);
176
+ }
177
+
178
+ this.ctx.beginPath();
179
+ this.ctx.arc(state.cx, state.cy, state.planetDistPx, 0, Math.PI * 2);
180
+ this.ctx.stroke();
181
+ this.ctx.setLineDash([]);
182
+ }
183
+
184
+ private getStellarColor(temp: number): string {
185
+ if (temp >= 30000) return '#00bfff';
186
+ if (temp >= 10000) return '#87cefa';
187
+ if (temp >= 7500) return '#e0ffff';
188
+ if (temp >= 6000) return '#fffacd';
189
+ if (temp >= 5200) return '#ffff00';
190
+ if (temp >= 3700) return '#ffa500';
191
+ return '#ff4500';
192
+ }
193
+
194
+ private drawStar(state: RenderState) {
195
+ const starColor = this.getStellarColor(state.curTemp);
196
+ const baseRadius = Math.max(4, Math.min(24, 10 * Math.pow(state.curTemp / 5778, 0.5)));
197
+ const pulseSpeed = 0.003 * state.curTemp;
198
+ const pulseAmp = 0.06 * Math.min(2.5, Math.max(0.1, state.curLuminosity));
199
+ const pulse = 1 + Math.sin(Date.now() * pulseSpeed) * pulseAmp;
200
+ const starRadius = baseRadius * pulse;
201
+
202
+ this.ctx.save();
203
+ const starGrad = this.ctx.createRadialGradient(state.cx, state.cy, 0, state.cx, state.cy, starRadius * 2.5);
204
+ starGrad.addColorStop(0, starColor);
205
+ starGrad.addColorStop(0.3, starColor);
206
+ starGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
207
+ this.ctx.fillStyle = starGrad;
208
+ this.ctx.beginPath();
209
+ this.ctx.arc(state.cx, state.cy, starRadius * 2.5, 0, Math.PI * 2);
210
+ this.ctx.fill();
211
+ this.ctx.restore();
212
+ }
213
+
214
+ private drawPlanet(state: RenderState) {
215
+ const period = Math.sqrt(Math.pow(state.curDistanceAu, 3) / state.curMass) * 365.255;
216
+ const speedFactor = 0.5;
217
+ this.orbitAngle += (360 / (period || 1)) * speedFactor * (Math.PI / 180);
218
+
219
+ const px = state.cx + Math.cos(this.orbitAngle) * state.planetDistPx;
220
+ const py = state.cy + Math.sin(this.orbitAngle) * state.planetDistPx;
221
+
222
+ this.ctx.save();
223
+ if (state.isTooHot) {
224
+ this.ctx.fillStyle = '#ef4444';
225
+ } else if (state.isTooCold) {
226
+ this.ctx.fillStyle = '#93c5fd';
227
+ this.ctx.shadowBlur = 12;
228
+ this.ctx.shadowColor = '#3b82f6';
229
+ } else {
230
+ this.ctx.fillStyle = '#10b981';
231
+ }
232
+
233
+ this.ctx.strokeStyle = '#ffffff';
234
+ this.ctx.lineWidth = 1.5;
235
+ this.ctx.beginPath();
236
+ this.ctx.arc(px, py, 4, 0, Math.PI * 2);
237
+ this.ctx.fill();
238
+ this.ctx.stroke();
239
+ this.ctx.restore();
240
+ }
241
+ }