@jjlmoya/utils-science 1.37.0 → 1.39.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 (83) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +3 -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/conway-life-rule-lab/bibliography.astro +14 -0
  8. package/src/tool/conway-life-rule-lab/bibliography.ts +16 -0
  9. package/src/tool/conway-life-rule-lab/component.astro +132 -0
  10. package/src/tool/conway-life-rule-lab/conway-life-rule-lab.css +603 -0
  11. package/src/tool/conway-life-rule-lab/entry.ts +26 -0
  12. package/src/tool/conway-life-rule-lab/i18n/de.ts +50 -0
  13. package/src/tool/conway-life-rule-lab/i18n/en.ts +174 -0
  14. package/src/tool/conway-life-rule-lab/i18n/es.ts +50 -0
  15. package/src/tool/conway-life-rule-lab/i18n/fr.ts +50 -0
  16. package/src/tool/conway-life-rule-lab/i18n/id.ts +50 -0
  17. package/src/tool/conway-life-rule-lab/i18n/it.ts +50 -0
  18. package/src/tool/conway-life-rule-lab/i18n/ja.ts +50 -0
  19. package/src/tool/conway-life-rule-lab/i18n/ko.ts +50 -0
  20. package/src/tool/conway-life-rule-lab/i18n/nl.ts +50 -0
  21. package/src/tool/conway-life-rule-lab/i18n/pl.ts +50 -0
  22. package/src/tool/conway-life-rule-lab/i18n/pt.ts +50 -0
  23. package/src/tool/conway-life-rule-lab/i18n/ru.ts +50 -0
  24. package/src/tool/conway-life-rule-lab/i18n/sv.ts +50 -0
  25. package/src/tool/conway-life-rule-lab/i18n/tr.ts +50 -0
  26. package/src/tool/conway-life-rule-lab/i18n/zh.ts +50 -0
  27. package/src/tool/conway-life-rule-lab/index.ts +11 -0
  28. package/src/tool/conway-life-rule-lab/logic/LifeAchievements.ts +85 -0
  29. package/src/tool/conway-life-rule-lab/logic/LifeCanvasRenderer.ts +104 -0
  30. package/src/tool/conway-life-rule-lab/logic/LifeLabDom.ts +55 -0
  31. package/src/tool/conway-life-rule-lab/logic/LifeLabRuntime.ts +253 -0
  32. package/src/tool/conway-life-rule-lab/logic/LifePatterns.ts +35 -0
  33. package/src/tool/conway-life-rule-lab/logic/LifeRules.ts +60 -0
  34. package/src/tool/conway-life-rule-lab/logic/LifeUniverse.ts +165 -0
  35. package/src/tool/conway-life-rule-lab/logic.ts +6 -0
  36. package/src/tool/conway-life-rule-lab/seo.astro +15 -0
  37. package/src/tool/dyson-sphere-energy-capture/bibliography.astro +14 -0
  38. package/src/tool/dyson-sphere-energy-capture/bibliography.ts +16 -0
  39. package/src/tool/dyson-sphere-energy-capture/component.astro +253 -0
  40. package/src/tool/dyson-sphere-energy-capture/dyson-sphere-energy-capture.css +502 -0
  41. package/src/tool/dyson-sphere-energy-capture/entry.ts +26 -0
  42. package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +195 -0
  43. package/src/tool/dyson-sphere-energy-capture/i18n/en.ts +195 -0
  44. package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +195 -0
  45. package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +195 -0
  46. package/src/tool/dyson-sphere-energy-capture/i18n/id.ts +195 -0
  47. package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +195 -0
  48. package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +71 -0
  49. package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +71 -0
  50. package/src/tool/dyson-sphere-energy-capture/i18n/nl.ts +197 -0
  51. package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +197 -0
  52. package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +195 -0
  53. package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +195 -0
  54. package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +195 -0
  55. package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +195 -0
  56. package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +71 -0
  57. package/src/tool/dyson-sphere-energy-capture/index.ts +11 -0
  58. package/src/tool/dyson-sphere-energy-capture/logic.ts +120 -0
  59. package/src/tool/dyson-sphere-energy-capture/seo.astro +15 -0
  60. package/src/tool/global-albedo-snowball-simulator/bibliography.astro +14 -0
  61. package/src/tool/global-albedo-snowball-simulator/bibliography.ts +16 -0
  62. package/src/tool/global-albedo-snowball-simulator/component.astro +278 -0
  63. package/src/tool/global-albedo-snowball-simulator/entry.ts +26 -0
  64. package/src/tool/global-albedo-snowball-simulator/global-albedo-snowball-simulator.css +530 -0
  65. package/src/tool/global-albedo-snowball-simulator/i18n/de.ts +169 -0
  66. package/src/tool/global-albedo-snowball-simulator/i18n/en.ts +169 -0
  67. package/src/tool/global-albedo-snowball-simulator/i18n/es.ts +169 -0
  68. package/src/tool/global-albedo-snowball-simulator/i18n/fr.ts +169 -0
  69. package/src/tool/global-albedo-snowball-simulator/i18n/id.ts +169 -0
  70. package/src/tool/global-albedo-snowball-simulator/i18n/it.ts +87 -0
  71. package/src/tool/global-albedo-snowball-simulator/i18n/ja.ts +87 -0
  72. package/src/tool/global-albedo-snowball-simulator/i18n/ko.ts +169 -0
  73. package/src/tool/global-albedo-snowball-simulator/i18n/nl.ts +169 -0
  74. package/src/tool/global-albedo-snowball-simulator/i18n/pl.ts +169 -0
  75. package/src/tool/global-albedo-snowball-simulator/i18n/pt.ts +169 -0
  76. package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +169 -0
  77. package/src/tool/global-albedo-snowball-simulator/i18n/sv.ts +169 -0
  78. package/src/tool/global-albedo-snowball-simulator/i18n/tr.ts +169 -0
  79. package/src/tool/global-albedo-snowball-simulator/i18n/zh.ts +169 -0
  80. package/src/tool/global-albedo-snowball-simulator/index.ts +11 -0
  81. package/src/tool/global-albedo-snowball-simulator/logic.ts +88 -0
  82. package/src/tool/global-albedo-snowball-simulator/seo.astro +15 -0
  83. package/src/tools.ts +6 -0
@@ -0,0 +1,50 @@
1
+ import { content as enContent } from './en';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ export const content: ToolLocaleContent = {
5
+ ...enContent,
6
+ slug: 'laboratoriya-pravil-igra-zhizn-konveya',
7
+ title: 'Лаборатория правил игры Жизнь Конвея',
8
+ description: 'Запускайте, редактируйте и сравнивайте клеточные автоматы типа Конвея с правилами B/S, начальными паттернами, живыми метриками и адаптивным полем.',
9
+ ui: {
10
+ boardLabel: 'Поле клеточного автомата типа Life',
11
+ play: 'Пуск',
12
+ pause: 'Пауза',
13
+ step: 'Шаг',
14
+ clear: 'Пустое поле',
15
+ randomize: 'Случайно',
16
+ ruleLabel: 'Нотация правила',
17
+ ruleHelp: 'Рождение / выживание',
18
+ speedLabel: 'Темп',
19
+ densityLabel: 'Начальная плотность',
20
+ patternLabel: 'Паттерн',
21
+ placePattern: 'Разместить паттерн',
22
+ generation: 'Поколение',
23
+ population: 'Популяция',
24
+ density: 'Плотность',
25
+ stability: 'Стабильность',
26
+ births: 'Рождения',
27
+ deaths: 'Смерти',
28
+ achievementsLabel: 'Журнал лаборатории',
29
+ achievementPulsar: 'Пульсар',
30
+ achievementPulsarDescription: 'Обнаружена осцилляция периода 2',
31
+ achievementImmortal: 'Бессмертный',
32
+ achievementImmortalDescription: 'Поколение 500 достигнуто при полной стабильности',
33
+ achievementBigBang: 'Большой взрыв',
34
+ achievementBigBangDescription: 'Редкое случайное зерно превысило 1 000 живых клеток',
35
+ presetClassic: 'Классика Конвея',
36
+ presetHighlife: 'HighLife',
37
+ presetSeeds: 'Seeds',
38
+ presetDayNight: 'День и ночь',
39
+ patternGlider: 'Глайдер',
40
+ patternGosper: 'Ружьё Госпера',
41
+ patternPulsar: 'Пульсар',
42
+ patternRPentomino: 'R-пентамино',
43
+ colonyStatus: 'Сигнал колонии',
44
+ statusFrozen: 'стабильно',
45
+ statusGrowing: 'растёт',
46
+ statusFading: 'угасает',
47
+ statusChaotic: 'нестабильно',
48
+ invalidRule: 'Используйте нотацию B/S, например B3/S23.',
49
+ },
50
+ };
@@ -0,0 +1,50 @@
1
+ import { content as enContent } from './en';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ export const content: ToolLocaleContent = {
5
+ ...enContent,
6
+ slug: 'conways-game-of-life-regellabb',
7
+ title: 'Conways Game of Life Regellabb',
8
+ description: 'Spela, redigera och jämför Conway-liknande cellulära automater med B/S-regler, mönsterfrön, livevärden och responsiv simuleringsyta.',
9
+ ui: {
10
+ boardLabel: 'Bräde för Life-liknande cellulära automater',
11
+ play: 'Spela',
12
+ pause: 'Pausa',
13
+ step: 'Steg',
14
+ clear: 'Tom duk',
15
+ randomize: 'Slumpa',
16
+ ruleLabel: 'Regelnotation',
17
+ ruleHelp: 'Födelse / överlevnad',
18
+ speedLabel: 'Tempo',
19
+ densityLabel: 'Startdensitet',
20
+ patternLabel: 'Mönster',
21
+ placePattern: 'Placera mönster',
22
+ generation: 'Generation',
23
+ population: 'Population',
24
+ density: 'Densitet',
25
+ stability: 'Stabilitet',
26
+ births: 'Födslar',
27
+ deaths: 'Dödsfall',
28
+ achievementsLabel: 'Laboratorielogg',
29
+ achievementPulsar: 'Pulsar',
30
+ achievementPulsarDescription: 'Period 2 oscillation upptäckt',
31
+ achievementImmortal: 'Odödlig',
32
+ achievementImmortalDescription: 'Generation 500 nådd med full stabilitet',
33
+ achievementBigBang: 'Big Bang',
34
+ achievementBigBangDescription: 'Ett glest slumpfrö passerade 1 000 levande celler',
35
+ presetClassic: 'Klassisk Conway',
36
+ presetHighlife: 'HighLife',
37
+ presetSeeds: 'Seeds',
38
+ presetDayNight: 'Dag och natt',
39
+ patternGlider: 'Glidare',
40
+ patternGosper: 'Gosper-kanon',
41
+ patternPulsar: 'Pulsar',
42
+ patternRPentomino: 'R-pentomino',
43
+ colonyStatus: 'Kolonisignal',
44
+ statusFrozen: 'stabil',
45
+ statusGrowing: 'växer',
46
+ statusFading: 'avtar',
47
+ statusChaotic: 'volatil',
48
+ invalidRule: 'Använd B/S-notation som B3/S23.',
49
+ },
50
+ };
@@ -0,0 +1,50 @@
1
+ import { content as enContent } from './en';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ export const content: ToolLocaleContent = {
5
+ ...enContent,
6
+ slug: 'conway-hayat-oyunu-kural-laboratuvari',
7
+ title: 'Conway Hayat Oyunu Kural Laboratuvarı',
8
+ description: 'B/S kuralları, desen tohumları, canlı metrikler ve duyarlı simülasyon tahtasıyla Conway tarzı hücresel otomatları oynayın, düzenleyin ve karşılaştırın.',
9
+ ui: {
10
+ boardLabel: 'Life benzeri hücresel otomat tahtası',
11
+ play: 'Oynat',
12
+ pause: 'Duraklat',
13
+ step: 'Adım',
14
+ clear: 'Boş tuval',
15
+ randomize: 'Rastgele',
16
+ ruleLabel: 'Kural gösterimi',
17
+ ruleHelp: 'Doğum / hayatta kalma',
18
+ speedLabel: 'Tempo',
19
+ densityLabel: 'Başlangıç yoğunluğu',
20
+ patternLabel: 'Desen',
21
+ placePattern: 'Deseni yerleştir',
22
+ generation: 'Nesil',
23
+ population: 'Popülasyon',
24
+ density: 'Yoğunluk',
25
+ stability: 'Kararlılık',
26
+ births: 'Doğumlar',
27
+ deaths: 'Ölümler',
28
+ achievementsLabel: 'Laboratuvar kaydı',
29
+ achievementPulsar: 'Pulsar',
30
+ achievementPulsarDescription: 'Periyot 2 salınım algılandı',
31
+ achievementImmortal: 'Ölümsüz',
32
+ achievementImmortalDescription: 'Tam kararlılıkla 500. nesle ulaşıldı',
33
+ achievementBigBang: 'Big Bang',
34
+ achievementBigBangDescription: 'Seyrek rastgele tohum 1.000 canlı hücreyi aştı',
35
+ presetClassic: 'Klasik Conway',
36
+ presetHighlife: 'HighLife',
37
+ presetSeeds: 'Seeds',
38
+ presetDayNight: 'Gündüz ve gece',
39
+ patternGlider: 'Planör',
40
+ patternGosper: 'Gosper topu',
41
+ patternPulsar: 'Pulsar',
42
+ patternRPentomino: 'R-pentomino',
43
+ colonyStatus: 'Koloni sinyali',
44
+ statusFrozen: 'kararlı',
45
+ statusGrowing: 'genişliyor',
46
+ statusFading: 'azalıyor',
47
+ statusChaotic: 'oynak',
48
+ invalidRule: 'B3/S23 gibi B/S gösterimi kullanın.',
49
+ },
50
+ };
@@ -0,0 +1,50 @@
1
+ import { content as enContent } from './en';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ export const content: ToolLocaleContent = {
5
+ ...enContent,
6
+ slug: 'conway-life-rule-lab',
7
+ title: '康威生命游戏规则实验室',
8
+ description: '使用 B/S 规则、图案种子、实时指标和响应式模拟棋盘,运行、编辑并比较康威式细胞自动机。',
9
+ ui: {
10
+ boardLabel: '类生命游戏细胞自动机棋盘',
11
+ play: '播放',
12
+ pause: '暂停',
13
+ step: '单步',
14
+ clear: '空白画布',
15
+ randomize: '随机',
16
+ ruleLabel: '规则记法',
17
+ ruleHelp: '诞生 / 存活计数',
18
+ speedLabel: '节奏',
19
+ densityLabel: '初始密度',
20
+ patternLabel: '图案',
21
+ placePattern: '放置图案',
22
+ generation: '世代',
23
+ population: '数量',
24
+ density: '密度',
25
+ stability: '稳定性',
26
+ births: '诞生',
27
+ deaths: '死亡',
28
+ achievementsLabel: '实验日志',
29
+ achievementPulsar: '脉冲星',
30
+ achievementPulsarDescription: '检测到周期 2 振荡',
31
+ achievementImmortal: '不朽',
32
+ achievementImmortalDescription: '在完全稳定下达到第 500 代',
33
+ achievementBigBang: '大爆炸',
34
+ achievementBigBangDescription: '低密度随机种子超过 1,000 个活细胞',
35
+ presetClassic: '经典康威',
36
+ presetHighlife: 'HighLife',
37
+ presetSeeds: 'Seeds',
38
+ presetDayNight: '昼夜',
39
+ patternGlider: '滑翔机',
40
+ patternGosper: 'Gosper 枪',
41
+ patternPulsar: '脉冲星',
42
+ patternRPentomino: 'R-五连块',
43
+ colonyStatus: '群落信号',
44
+ statusFrozen: '稳定',
45
+ statusGrowing: '扩张中',
46
+ statusFading: '衰退中',
47
+ statusChaotic: '波动',
48
+ invalidRule: '请使用 B3/S23 这样的 B/S 记法。',
49
+ },
50
+ };
@@ -0,0 +1,11 @@
1
+ import { conwayLifeRuleLab } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+
4
+ export * from './entry';
5
+
6
+ export const CONWAY_LIFE_RULE_LAB_TOOL: ToolDefinition = {
7
+ entry: conwayLifeRuleLab,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,85 @@
1
+ import type { LifeSnapshot } from './LifeUniverse';
2
+
3
+ interface AchievementOutputs {
4
+ pulsar: HTMLElement | null;
5
+ immortal: HTMLElement | null;
6
+ bigBang: HTMLElement | null;
7
+ }
8
+
9
+ export class LifeAchievements {
10
+ private readonly active = { pulsar: false, immortal: false, bigBang: false };
11
+ private readonly history: Uint8Array[] = [];
12
+ private readonly storageKey = 'conway-life-rule-lab-achievements';
13
+
14
+ public constructor(private readonly outputs: AchievementOutputs) {
15
+ this.load();
16
+ }
17
+
18
+ public resetHistory(cells: Uint8Array): void {
19
+ this.history.length = 0;
20
+ this.record(cells);
21
+ }
22
+
23
+ public evaluate(snapshot: LifeSnapshot, cells: Uint8Array, randomSeedDensity: number): void {
24
+ const previous = this.history.length >= 1 ? this.history[this.history.length - 1] : null;
25
+ const twoBack = this.history.length >= 2 ? this.history[this.history.length - 2] : null;
26
+ this.evaluatePulsar(cells, previous, twoBack);
27
+ this.evaluateImmortal(snapshot);
28
+ this.evaluateBigBang(snapshot, randomSeedDensity);
29
+ this.record(cells);
30
+ }
31
+
32
+ private evaluatePulsar(cells: Uint8Array, previous: Uint8Array | null, twoBack: Uint8Array | null): void {
33
+ if (this.active.pulsar || !previous || !twoBack) return;
34
+ if (!this.arraysEqual(cells, twoBack) || this.arraysEqual(cells, previous)) return;
35
+ this.active.pulsar = true;
36
+ this.activate('pulsar');
37
+ }
38
+
39
+ private evaluateImmortal(snapshot: LifeSnapshot): void {
40
+ if (this.active.immortal || snapshot.stats.generation < 500 || snapshot.stats.stability !== 1) return;
41
+ this.activate('immortal');
42
+ }
43
+
44
+ private evaluateBigBang(snapshot: LifeSnapshot, randomSeedDensity: number): void {
45
+ if (this.active.bigBang || randomSeedDensity > 0.12 || snapshot.stats.population <= 1000) return;
46
+ this.activate('bigBang');
47
+ }
48
+
49
+ private activate(key: keyof typeof this.active): void {
50
+ this.active[key] = true;
51
+ this.outputs[key]?.classList.add('life-lab-achievement-active');
52
+ this.save();
53
+ }
54
+
55
+ private load(): void {
56
+ try {
57
+ const saved = window.localStorage.getItem(this.storageKey);
58
+ if (!saved) return;
59
+ const parsed = JSON.parse(saved) as Partial<Record<keyof typeof this.active, boolean>>;
60
+ Object.keys(this.active).forEach((key) => {
61
+ const achievement = key as keyof typeof this.active;
62
+ if (parsed[achievement]) this.activate(achievement);
63
+ });
64
+ } catch {
65
+ window.localStorage.removeItem(this.storageKey);
66
+ }
67
+ }
68
+
69
+ private save(): void {
70
+ window.localStorage.setItem(this.storageKey, JSON.stringify(this.active));
71
+ }
72
+
73
+ private record(cells: Uint8Array): void {
74
+ this.history.push(new Uint8Array(cells));
75
+ if (this.history.length > 3) this.history.shift();
76
+ }
77
+
78
+ private arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
79
+ if (a.length !== b.length) return false;
80
+ for (let index = 0; index < a.length; index += 1) {
81
+ if (a[index] !== b[index]) return false;
82
+ }
83
+ return true;
84
+ }
85
+ }
@@ -0,0 +1,104 @@
1
+ import type { LifeUniverse } from './LifeUniverse';
2
+
3
+ interface RenderMetrics {
4
+ dpr: number;
5
+ cell: number;
6
+ boardWidth: number;
7
+ boardHeight: number;
8
+ offsetX: number;
9
+ offsetY: number;
10
+ }
11
+
12
+ interface GridSize {
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export class LifeCanvasRenderer {
18
+ public constructor(
19
+ private readonly root: HTMLElement,
20
+ private readonly canvas: HTMLCanvasElement,
21
+ private readonly universe: LifeUniverse,
22
+ private readonly grid: GridSize,
23
+ ) {}
24
+
25
+ public render(latestPopulation: number): void {
26
+ this.resizeCanvas();
27
+ const context = this.canvas.getContext('2d');
28
+ if (!context) return;
29
+ this.paintBoard(context, this.getRenderMetrics(), latestPopulation);
30
+ }
31
+
32
+ public canvasToCell(event: PointerEvent): { x: number; y: number } | null {
33
+ const rect = this.canvas.getBoundingClientRect();
34
+ const inset = this.boardInset();
35
+ const size = Math.min((rect.width - inset * 2) / this.grid.width, (rect.height - inset * 2) / this.grid.height);
36
+ const x = Math.floor((event.clientX - rect.left - (rect.width - size * this.grid.width) / 2) / size);
37
+ const y = Math.floor((event.clientY - rect.top - (rect.height - size * this.grid.height) / 2) / size);
38
+ if (x < 0 || y < 0 || x >= this.grid.width || y >= this.grid.height) return null;
39
+ return { x, y };
40
+ }
41
+
42
+ private resizeCanvas(): void {
43
+ const rect = this.canvas.getBoundingClientRect();
44
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
45
+ this.canvas.width = Math.floor(rect.width * dpr);
46
+ this.canvas.height = Math.floor(rect.height * dpr);
47
+ }
48
+
49
+ private getRenderMetrics(): RenderMetrics {
50
+ const dpr = this.canvas.width / this.canvas.clientWidth;
51
+ const inset = this.boardInset() * dpr;
52
+ const cell = Math.min((this.canvas.width - inset * 2) / this.grid.width, (this.canvas.height - inset * 2) / this.grid.height);
53
+ return {
54
+ dpr,
55
+ cell,
56
+ boardWidth: cell * this.grid.width,
57
+ boardHeight: cell * this.grid.height,
58
+ offsetX: (this.canvas.width - cell * this.grid.width) / 2,
59
+ offsetY: (this.canvas.height - cell * this.grid.height) / 2,
60
+ };
61
+ }
62
+
63
+ private paintBoard(context: CanvasRenderingContext2D, metrics: RenderMetrics, latestPopulation: number): void {
64
+ const styles = getComputedStyle(this.root);
65
+ context.clearRect(0, 0, this.canvas.width, this.canvas.height);
66
+ context.fillStyle = styles.getPropertyValue('--life-board-canvas').trim();
67
+ context.fillRect(0, 0, this.canvas.width, this.canvas.height);
68
+ this.paintGrid(context, metrics, styles.getPropertyValue('--life-grid-canvas').trim());
69
+ this.paintCells(context, metrics, styles, latestPopulation);
70
+ }
71
+
72
+ private paintGrid(context: CanvasRenderingContext2D, metrics: RenderMetrics, color: string): void {
73
+ context.strokeStyle = color;
74
+ context.lineWidth = Math.max(1, metrics.dpr);
75
+ for (let x = 0; x <= this.grid.width; x += 1) this.strokeGridLine(context, metrics, x, true);
76
+ for (let y = 0; y <= this.grid.height; y += 1) this.strokeGridLine(context, metrics, y, false);
77
+ }
78
+
79
+ private strokeGridLine(context: CanvasRenderingContext2D, metrics: RenderMetrics, index: number, vertical: boolean): void {
80
+ const position = vertical ? metrics.offsetX + index * metrics.cell : metrics.offsetY + index * metrics.cell;
81
+ context.beginPath();
82
+ context.moveTo(vertical ? position : metrics.offsetX, vertical ? metrics.offsetY : position);
83
+ context.lineTo(vertical ? position : metrics.offsetX + metrics.boardWidth, vertical ? metrics.offsetY + metrics.boardHeight : position);
84
+ context.stroke();
85
+ }
86
+
87
+ private paintCells(context: CanvasRenderingContext2D, metrics: RenderMetrics, styles: CSSStyleDeclaration, latestPopulation: number): void {
88
+ const cells = this.universe.getCells();
89
+ context.fillStyle = latestPopulation > this.grid.width * this.grid.height * 0.238 ? styles.getPropertyValue('--life-cell-high').trim() : styles.getPropertyValue('--life-cell-low').trim();
90
+ for (let y = 0; y < this.grid.height; y += 1) {
91
+ for (let x = 0; x < this.grid.width; x += 1) this.paintCell({ context, metrics, cells, x, y });
92
+ }
93
+ }
94
+
95
+ private paintCell(cell: { context: CanvasRenderingContext2D; metrics: RenderMetrics; cells: Uint8Array; x: number; y: number }): void {
96
+ const { context, metrics, cells, x, y } = cell;
97
+ if (!cells[y * this.grid.width + x]) return;
98
+ context.fillRect(metrics.offsetX + x * metrics.cell + metrics.dpr, metrics.offsetY + y * metrics.cell + metrics.dpr, Math.max(1, metrics.cell - metrics.dpr * 2), Math.max(1, metrics.cell - metrics.dpr * 2));
99
+ }
100
+
101
+ private boardInset(): number {
102
+ return parseFloat(getComputedStyle(this.root).getPropertyValue('--life-board-inset')) || 0;
103
+ }
104
+ }
@@ -0,0 +1,55 @@
1
+ import { LIFE_PATTERNS } from './LifePatterns';
2
+
3
+ export interface RuntimeLabels {
4
+ play: string;
5
+ pause: string;
6
+ frozen: string;
7
+ growing: string;
8
+ fading: string;
9
+ chaotic: string;
10
+ invalidRule: string;
11
+ }
12
+
13
+ export function getOutputs(byId: (id: string) => HTMLElement): Record<string, HTMLElement | null> {
14
+ return {
15
+ generation: byId('life-lab-generation'),
16
+ population: byId('life-lab-population'),
17
+ density: byId('life-lab-density'),
18
+ stability: byId('life-lab-stability'),
19
+ births: byId('life-lab-births'),
20
+ deaths: byId('life-lab-deaths'),
21
+ status: byId('life-lab-status'),
22
+ pulsar: byId('life-lab-achievement-pulsar'),
23
+ immortal: byId('life-lab-achievement-immortal'),
24
+ bigBang: byId('life-lab-achievement-big-bang'),
25
+ };
26
+ }
27
+
28
+ export function getLabels(root: HTMLElement, playButton: HTMLButtonElement): RuntimeLabels {
29
+ const data = root.dataset;
30
+ return {
31
+ play: playButton.getAttribute('aria-label') || '',
32
+ pause: data.pause || '',
33
+ frozen: data.statusFrozen || '',
34
+ growing: data.statusGrowing || '',
35
+ fading: data.statusFading || '',
36
+ chaotic: data.statusChaotic || '',
37
+ invalidRule: data.invalidRule || '',
38
+ };
39
+ }
40
+
41
+ export function fillPatternSelect(root: HTMLElement, patternSelect: HTMLSelectElement): void {
42
+ const data = root.dataset;
43
+ const names: Record<string, string> = {
44
+ glider: data.patternGlider || '',
45
+ gosper: data.patternGosper || '',
46
+ pulsar: data.patternPulsar || '',
47
+ 'r-pentomino': data.patternRPentomino || '',
48
+ };
49
+ LIFE_PATTERNS.forEach((pattern) => {
50
+ const option = document.createElement('option');
51
+ option.value = pattern.id;
52
+ option.textContent = names[pattern.id] || pattern.id;
53
+ patternSelect.append(option);
54
+ });
55
+ }