@jjlmoya/utils-chrono 1.11.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +6 -0
- package/src/entries.ts +10 -1
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/gmt-world-timer/bibliography.astro +11 -0
- package/src/tool/gmt-world-timer/bibliography.ts +7 -0
- package/src/tool/gmt-world-timer/client.ts +250 -0
- package/src/tool/gmt-world-timer/component.astro +13 -0
- package/src/tool/gmt-world-timer/components/GmtPanel.astro +18 -0
- package/src/tool/gmt-world-timer/entry.ts +34 -0
- package/src/tool/gmt-world-timer/gmt-world-timer.css +239 -0
- package/src/tool/gmt-world-timer/helpers.ts +28 -0
- package/src/tool/gmt-world-timer/i18n/de.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/en.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/es.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/fr.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/id.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/it.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/ja.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/ko.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/nl.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/pl.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/pt.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/ru.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/sv.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/tr.ts +72 -0
- package/src/tool/gmt-world-timer/i18n/zh.ts +72 -0
- package/src/tool/gmt-world-timer/index.ts +11 -0
- package/src/tool/gmt-world-timer/seo.astro +11 -0
- package/src/tool/perpetual-calendar/bibliography.astro +16 -0
- package/src/tool/perpetual-calendar/bibliography.ts +16 -0
- package/src/tool/perpetual-calendar/calendar.ts +24 -0
- package/src/tool/perpetual-calendar/client.ts +98 -0
- package/src/tool/perpetual-calendar/component.astro +17 -0
- package/src/tool/perpetual-calendar/components/CalendarPanel.astro +49 -0
- package/src/tool/perpetual-calendar/dial.ts +176 -0
- package/src/tool/perpetual-calendar/entry.ts +48 -0
- package/src/tool/perpetual-calendar/helpers.ts +49 -0
- package/src/tool/perpetual-calendar/i18n/de.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/en.ts +102 -0
- package/src/tool/perpetual-calendar/i18n/es.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/fr.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/id.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/it.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/ja.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/ko.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/nl.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/pl.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/pt.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/ru.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/sv.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/tr.ts +85 -0
- package/src/tool/perpetual-calendar/i18n/zh.ts +85 -0
- package/src/tool/perpetual-calendar/index.ts +11 -0
- package/src/tool/perpetual-calendar/perpetual-calendar.css +181 -0
- package/src/tool/perpetual-calendar/seo.astro +16 -0
- package/src/tool/perpetual-calendar/state.ts +26 -0
- package/src/tool/tourbillon-visualizer/bibliography.astro +11 -0
- package/src/tool/tourbillon-visualizer/bibliography.ts +7 -0
- package/src/tool/tourbillon-visualizer/client.ts +122 -0
- package/src/tool/tourbillon-visualizer/component.astro +126 -0
- package/src/tool/tourbillon-visualizer/components/TourbillonPanel.astro +66 -0
- package/src/tool/tourbillon-visualizer/entry.ts +51 -0
- package/src/tool/tourbillon-visualizer/helpers.ts +35 -0
- package/src/tool/tourbillon-visualizer/i18n/de.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/en.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/es.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/fr.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/id.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/it.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/ja.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/ko.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/nl.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/pl.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/pt.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/ru.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/sv.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/tr.ts +96 -0
- package/src/tool/tourbillon-visualizer/i18n/zh.ts +96 -0
- package/src/tool/tourbillon-visualizer/index.ts +11 -0
- package/src/tool/tourbillon-visualizer/renderer/base.ts +78 -0
- package/src/tool/tourbillon-visualizer/renderer/cage.ts +115 -0
- package/src/tool/tourbillon-visualizer/renderer/esc.ts +160 -0
- package/src/tool/tourbillon-visualizer/seo.astro +11 -0
- package/src/tool/tourbillon-visualizer/state.ts +21 -0
- package/src/tool/tourbillon-visualizer/tourbillon.ts +9 -0
- package/src/tools.ts +6 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function isLeapYear(y: number): boolean {
|
|
2
|
+
return new Date(y, 1, 29).getDate() === 29;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function daysInMonth(y: number, m: number): number {
|
|
6
|
+
return new Date(y, m + 1, 0).getDate();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const PHASES = ['New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous', 'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent'];
|
|
10
|
+
const PHASE_THRESHOLDS = [0.03, 0.22, 0.28, 0.47, 0.53, 0.72, 0.78, 0.97];
|
|
11
|
+
|
|
12
|
+
export function moonPhase(y: number, m: number, d: number, locale = 'en'): { phase: string; illumination: number } {
|
|
13
|
+
const jd = new Date(y, m, d).getTime() / 86400000 + 2440587.5 - 2451549.5;
|
|
14
|
+
const cycle = 29.53058867;
|
|
15
|
+
const progress = (((jd % cycle) + cycle) % cycle) / cycle;
|
|
16
|
+
const ill = Math.round((progress <= 0.5 ? progress * 2 : (1 - progress) * 2) * 100);
|
|
17
|
+
const pct = new Intl.NumberFormat(locale, { style: 'percent' }).format(ill / 100);
|
|
18
|
+
let phase = '';
|
|
19
|
+
for (let i = 0; i < PHASE_THRESHOLDS.length; i++) {
|
|
20
|
+
if (progress < PHASE_THRESHOLDS[i]) { phase = PHASES[i]; break; }
|
|
21
|
+
}
|
|
22
|
+
if (!phase) phase = PHASES[PHASES.length - 1];
|
|
23
|
+
return { phase: phase + ' ' + pct, illumination: ill };
|
|
24
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const canvas = document.getElementById('calendar-canvas') as HTMLCanvasElement;
|
|
2
|
+
const ctx = canvas.getContext('2d')!;
|
|
3
|
+
|
|
4
|
+
import { setCtx, detectTheme } from './state';
|
|
5
|
+
import { drawScene } from './dial';
|
|
6
|
+
import { isLeapYear, daysInMonth, moonPhase } from './calendar';
|
|
7
|
+
|
|
8
|
+
setCtx(ctx);
|
|
9
|
+
|
|
10
|
+
const REF_W = 600;
|
|
11
|
+
const REF_H = 600;
|
|
12
|
+
|
|
13
|
+
let curY = new Date().getFullYear();
|
|
14
|
+
let curM = new Date().getMonth();
|
|
15
|
+
let curD = new Date().getDate();
|
|
16
|
+
let smoothD = curD;
|
|
17
|
+
let autoPlaying = false;
|
|
18
|
+
|
|
19
|
+
function resizeCanvas() {
|
|
20
|
+
const parent = canvas.parentElement!;
|
|
21
|
+
const dw = parent.clientWidth;
|
|
22
|
+
const dh = dw * (REF_H / REF_W);
|
|
23
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
24
|
+
canvas.style.width = dw + 'px';
|
|
25
|
+
canvas.style.height = dh + 'px';
|
|
26
|
+
canvas.width = dw * dpr;
|
|
27
|
+
canvas.height = dh * dpr;
|
|
28
|
+
ctx.setTransform(dw / REF_W * dpr, 0, 0, dh / REF_H * dpr, 0, 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateUI() {
|
|
32
|
+
const setText = (id: string, v: string) => { const el = document.getElementById(id); if (el) el.textContent = v; };
|
|
33
|
+
const d = Math.round(smoothD);
|
|
34
|
+
const dt = new Date(curY, curM, d);
|
|
35
|
+
const loc = window.location.pathname.match(/^\/([a-z]{2})/)?.[1] || 'en';
|
|
36
|
+
setText('cal-date', d.toString());
|
|
37
|
+
setText('cal-weekday', new Intl.DateTimeFormat(loc, { weekday: 'long' }).format(dt));
|
|
38
|
+
setText('cal-month', new Intl.DateTimeFormat(loc, { month: 'long' }).format(dt));
|
|
39
|
+
setText('cal-year', curY.toString());
|
|
40
|
+
setText('cal-leap', isLeapYear(curY) ? 'Yes' : 'No');
|
|
41
|
+
const mp = moonPhase(curY, curM, d, loc);
|
|
42
|
+
setText('cal-moon', mp.phase);
|
|
43
|
+
setText('cal-weekday-header', new Intl.DateTimeFormat(loc, { weekday: 'long' }).format(dt));
|
|
44
|
+
setText('cal-date-full', new Intl.DateTimeFormat(loc, { month: 'long', day: 'numeric', year: 'numeric' }).format(dt));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render() {
|
|
48
|
+
detectTheme();
|
|
49
|
+
drawScene(curY, curM, curD, smoothD);
|
|
50
|
+
updateUI();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tick() {
|
|
54
|
+
if (autoPlaying) {
|
|
55
|
+
curD++;
|
|
56
|
+
if (curD > daysInMonth(curY, curM)) { curD = 1; curM++; }
|
|
57
|
+
if (curM > 11) { curM = 0; curY++; }
|
|
58
|
+
smoothD = curD;
|
|
59
|
+
}
|
|
60
|
+
render();
|
|
61
|
+
animationId = requestAnimationFrame(tick);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function advance(fn: () => void) {
|
|
65
|
+
if (autoPlaying) { autoPlaying = false; document.querySelector('[data-action="play"]')!.textContent = '\u25B6'; }
|
|
66
|
+
fn();
|
|
67
|
+
smoothD = curD;
|
|
68
|
+
render();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function initControls() {
|
|
72
|
+
document.querySelector('[data-action="day-next"]')?.addEventListener('click', () => advance(() => {
|
|
73
|
+
curD++; if (curD > daysInMonth(curY, curM)) { curD = 1; curM++; if (curM > 11) { curM = 0; curY++; } }
|
|
74
|
+
}));
|
|
75
|
+
document.querySelector('[data-action="day-prev"]')?.addEventListener('click', () => advance(() => {
|
|
76
|
+
curD--; if (curD < 1) { curM--; if (curM < 0) { curM = 11; curY--; } curD = daysInMonth(curY, curM); }
|
|
77
|
+
}));
|
|
78
|
+
document.querySelector('[data-action="month-next"]')?.addEventListener('click', () => advance(() => {
|
|
79
|
+
curD = 1; curM++; if (curM > 11) { curM = 0; curY++; }
|
|
80
|
+
}));
|
|
81
|
+
document.querySelector('[data-action="year-next"]')?.addEventListener('click', () => advance(() => {
|
|
82
|
+
const d = new Date(curY + 1, curM, curD);
|
|
83
|
+
curY = d.getFullYear(); curM = d.getMonth(); curD = d.getDate();
|
|
84
|
+
}));
|
|
85
|
+
document.querySelector('[data-action="play"]')?.addEventListener('click', (e) => {
|
|
86
|
+
autoPlaying = !autoPlaying;
|
|
87
|
+
(e.currentTarget as HTMLElement).textContent = autoPlaying ? '\u23F8' : '\u25B6';
|
|
88
|
+
});
|
|
89
|
+
document.querySelector('[data-action="reset"]')?.addEventListener('click', () => advance(() => {
|
|
90
|
+
const n = new Date();
|
|
91
|
+
curY = n.getFullYear(); curM = n.getMonth(); curD = n.getDate();
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
new ResizeObserver(resizeCanvas).observe(canvas.parentElement!);
|
|
96
|
+
resizeCanvas();
|
|
97
|
+
initControls();
|
|
98
|
+
tick();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CalendarPanel from './components/CalendarPanel.astro';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<link href="./perpetual-calendar.css" rel="stylesheet" />
|
|
12
|
+
|
|
13
|
+
<div class="tool-main-card" data-ui={JSON.stringify(ui)}>
|
|
14
|
+
<CalendarPanel labels={ui} />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<script src="./client.ts"></script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="calendar-layout">
|
|
10
|
+
<div class="dial-section">
|
|
11
|
+
<canvas id="calendar-canvas" width="600" height="600"></canvas>
|
|
12
|
+
<div class="date-overlay" id="cal-date-header">
|
|
13
|
+
<span class="date-weekday" id="cal-weekday-header">Tuesday</span>
|
|
14
|
+
<span class="date-full" id="cal-date-full">May 26, 2026</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="sidebar">
|
|
19
|
+
<div class="sidebar-group">
|
|
20
|
+
<div class="sidebar-label">{labels.dateLabel || 'Navigate'}</div>
|
|
21
|
+
<div class="sidebar-buttons">
|
|
22
|
+
<button data-action="day-prev" title="Day -1">◀D</button>
|
|
23
|
+
<button data-action="day-next" title="Day +1">D▶</button>
|
|
24
|
+
<button data-action="month-next" title="Month +1">M▶</button>
|
|
25
|
+
<button data-action="year-next" title="Year +1">Y▶</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="sidebar-group">
|
|
30
|
+
<div class="sidebar-label">{labels.autoPlay || 'Auto'}</div>
|
|
31
|
+
<div class="sidebar-buttons">
|
|
32
|
+
<button class="play-btn" data-action="play" title="Auto advance">▶</button>
|
|
33
|
+
<button data-action="reset" title="Reset to today">◉</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="sidebar-group">
|
|
38
|
+
<div class="sidebar-label">Info</div>
|
|
39
|
+
<div class="sidebar-info" id="cal-info">
|
|
40
|
+
<div class="sidebar-row"><span class="slbl">{labels.dateLabel || 'Date'}</span><span class="sval" id="cal-date">1</span></div>
|
|
41
|
+
<div class="sidebar-row"><span class="slbl">{labels.weekdayLabel || 'Day'}</span><span class="sval" id="cal-weekday">Mon</span></div>
|
|
42
|
+
<div class="sidebar-row"><span class="slbl">{labels.monthLabel || 'Month'}</span><span class="sval" id="cal-month">Jan</span></div>
|
|
43
|
+
<div class="sidebar-row"><span class="slbl">{labels.yearLabel || 'Year'}</span><span class="sval" id="cal-year">2024</span></div>
|
|
44
|
+
<div class="sidebar-row"><span class="slbl">{labels.leapYearLabel || 'Leap'}</span><span class="sval" id="cal-leap">No</span></div>
|
|
45
|
+
<div class="sidebar-row"><span class="slbl">{labels.moonPhaseLabel || 'Moon'}</span><span class="sval" id="cal-moon">New</span></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { getCtx, getFontFam, c, W, H, CX, CY, OUTER_R } from './state';
|
|
2
|
+
|
|
3
|
+
export function drawBg() {
|
|
4
|
+
const ctx = getCtx();
|
|
5
|
+
const grad = ctx.createRadialGradient(CX, CY, 0, CX, CY, 400);
|
|
6
|
+
grad.addColorStop(0, c('#1e1e3a', '#f5f0e8'));
|
|
7
|
+
grad.addColorStop(0.5, c('#16162e', '#eae4d8'));
|
|
8
|
+
grad.addColorStop(1, c('#0c0c18', '#ddd6c8'));
|
|
9
|
+
ctx.fillStyle = grad;
|
|
10
|
+
ctx.fillRect(0, 0, W, H);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function drawOuterRing() {
|
|
14
|
+
const ctx = getCtx();
|
|
15
|
+
ctx.beginPath();
|
|
16
|
+
ctx.arc(CX, CY, OUTER_R, 0, Math.PI * 2);
|
|
17
|
+
ctx.strokeStyle = c('rgba(212,175,55,0.3)', 'rgba(139,105,20,0.3)');
|
|
18
|
+
ctx.lineWidth = 2;
|
|
19
|
+
ctx.stroke();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function drawDateNumbers(currentDate: number, smoothDate: number) {
|
|
23
|
+
const ctx = getCtx();
|
|
24
|
+
const r = 245;
|
|
25
|
+
const ff = getFontFam();
|
|
26
|
+
ctx.textAlign = 'center';
|
|
27
|
+
ctx.textBaseline = 'middle';
|
|
28
|
+
for (let i = 1; i <= 31; i++) {
|
|
29
|
+
const ang = ((i - 1) / 31) * Math.PI * 2 - Math.PI / 2;
|
|
30
|
+
const x = CX + Math.cos(ang) * r;
|
|
31
|
+
const y = CY + Math.sin(ang) * r;
|
|
32
|
+
const isActive = Math.round(smoothDate) === i;
|
|
33
|
+
ctx.font = (isActive ? 'bold 13px ' : '10px ') + ff;
|
|
34
|
+
ctx.fillStyle = isActive ? c('#ffd700', '#8b6914') : c('rgba(180,180,200,0.5)', 'rgba(80,70,50,0.5)');
|
|
35
|
+
ctx.fillText(i.toString(), x, y);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function drawDateHand(smoothDate: number) {
|
|
40
|
+
const ctx = getCtx();
|
|
41
|
+
const ang = ((smoothDate - 1) / 31) * Math.PI * 2 - Math.PI / 2;
|
|
42
|
+
ctx.save();
|
|
43
|
+
ctx.translate(CX, CY);
|
|
44
|
+
ctx.rotate(ang);
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo(-4, 20);
|
|
47
|
+
ctx.lineTo(0, -200);
|
|
48
|
+
ctx.lineTo(4, 20);
|
|
49
|
+
ctx.closePath();
|
|
50
|
+
const grad = ctx.createLinearGradient(0, -200, 0, 20);
|
|
51
|
+
grad.addColorStop(0, c('#ffd700', '#b8860b'));
|
|
52
|
+
grad.addColorStop(1, c('#b8860b', '#8b6914'));
|
|
53
|
+
ctx.fillStyle = grad;
|
|
54
|
+
ctx.fill();
|
|
55
|
+
ctx.beginPath();
|
|
56
|
+
ctx.arc(0, 0, 6, 0, Math.PI * 2);
|
|
57
|
+
ctx.fillStyle = c('#d4af37', '#8b6914');
|
|
58
|
+
ctx.fill();
|
|
59
|
+
ctx.restore();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function drawMonthWindow(y: number, m: number) {
|
|
63
|
+
const ctx = getCtx();
|
|
64
|
+
const ff = getFontFam();
|
|
65
|
+
const mn = new Intl.DateTimeFormat(window.location.pathname.match(/^\/([a-z]{2})/)?.[1] || 'en', { month: 'long' }).format(new Date(y, m, 1));
|
|
66
|
+
ctx.save();
|
|
67
|
+
const x = CX, yPos = CY - 140;
|
|
68
|
+
ctx.fillStyle = c('rgba(30,30,58,0.9)', 'rgba(245,240,232,0.95)');
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
ctx.roundRect(x - 75, yPos - 12, 150, 28, 6);
|
|
71
|
+
ctx.fill();
|
|
72
|
+
ctx.strokeStyle = c('rgba(212,175,55,0.4)', 'rgba(139,105,20,0.4)');
|
|
73
|
+
ctx.lineWidth = 1;
|
|
74
|
+
ctx.stroke();
|
|
75
|
+
ctx.fillStyle = c('#d4af37', '#8b6914');
|
|
76
|
+
ctx.font = '600 13px ' + ff;
|
|
77
|
+
ctx.textAlign = 'center';
|
|
78
|
+
ctx.textBaseline = 'middle';
|
|
79
|
+
ctx.fillText(mn, x, yPos + 2);
|
|
80
|
+
ctx.restore();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function drawDaySubdial(y: number, m: number, d: number) {
|
|
84
|
+
const ctx = getCtx();
|
|
85
|
+
const ff = getFontFam();
|
|
86
|
+
const dow = new Date(y, m, d).getDay();
|
|
87
|
+
const names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
88
|
+
const sx = CX + 150, sy = CY - 20, sr = 45;
|
|
89
|
+
ctx.beginPath();
|
|
90
|
+
ctx.arc(sx, sy, sr, 0, Math.PI * 2);
|
|
91
|
+
ctx.fillStyle = c('rgba(30,30,58,0.8)', 'rgba(245,240,232,0.9)');
|
|
92
|
+
ctx.fill();
|
|
93
|
+
ctx.strokeStyle = c('rgba(212,175,55,0.3)', 'rgba(139,105,20,0.3)');
|
|
94
|
+
ctx.lineWidth = 1;
|
|
95
|
+
ctx.stroke();
|
|
96
|
+
ctx.fillStyle = c('rgba(180,180,200,0.4)', 'rgba(80,70,50,0.4)');
|
|
97
|
+
ctx.font = '7px ' + ff;
|
|
98
|
+
ctx.textAlign = 'center';
|
|
99
|
+
ctx.textBaseline = 'middle';
|
|
100
|
+
for (let i = 0; i < 7; i++) {
|
|
101
|
+
const a = (i / 7) * Math.PI * 2 - Math.PI / 2;
|
|
102
|
+
const nx = sx + Math.cos(a) * (sr - 10);
|
|
103
|
+
const ny = sy + Math.sin(a) * (sr - 10);
|
|
104
|
+
ctx.fillStyle = i === dow ? c('#ffd700', '#8b6914') : c('rgba(180,180,200,0.4)', 'rgba(80,70,50,0.4)');
|
|
105
|
+
ctx.font = (i === dow ? 'bold 8px ' : '7px ') + ff;
|
|
106
|
+
ctx.fillText(names[i], nx, ny);
|
|
107
|
+
}
|
|
108
|
+
ctx.fillStyle = c('#d4af37', '#8b6914');
|
|
109
|
+
ctx.font = 'bold 14px ' + ff;
|
|
110
|
+
ctx.fillText(names[dow], sx, sy + sr + 16);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function drawMoonSubdial(y: number, m: number, d: number) {
|
|
114
|
+
const ctx = getCtx();
|
|
115
|
+
const mx = CX - 150, my = CY + 30, mr = 42;
|
|
116
|
+
ctx.beginPath();
|
|
117
|
+
ctx.arc(mx, my, mr, 0, Math.PI * 2);
|
|
118
|
+
ctx.fillStyle = c('rgba(30,30,58,0.8)', 'rgba(245,240,232,0.9)');
|
|
119
|
+
ctx.fill();
|
|
120
|
+
ctx.strokeStyle = c('rgba(212,175,55,0.3)', 'rgba(139,105,20,0.3)');
|
|
121
|
+
ctx.lineWidth = 1;
|
|
122
|
+
ctx.stroke();
|
|
123
|
+
const jd = new Date(y, m, d).getTime() / 86400000 + 2440587.5 - 2451549.5;
|
|
124
|
+
const progress = (((jd % 29.53058867) + 29.53058867) % 29.53058867) / 29.53058867;
|
|
125
|
+
const mr2 = mr - 8;
|
|
126
|
+
ctx.save();
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.arc(mx, my, mr2, 0, Math.PI * 2);
|
|
129
|
+
ctx.fillStyle = c('#1a1a2e', '#ddd6c8');
|
|
130
|
+
ctx.fill();
|
|
131
|
+
ctx.clip();
|
|
132
|
+
ctx.beginPath();
|
|
133
|
+
ctx.arc(mx + (progress - 0.5) * mr2 * 2, my, mr2, 0, Math.PI * 2);
|
|
134
|
+
ctx.fillStyle = c('#e8d8a0', '#c8a850');
|
|
135
|
+
ctx.fill();
|
|
136
|
+
ctx.restore();
|
|
137
|
+
ctx.beginPath();
|
|
138
|
+
ctx.arc(mx, my, mr2, 0, Math.PI * 2);
|
|
139
|
+
ctx.strokeStyle = c('rgba(212,175,55,0.2)', 'rgba(139,105,20,0.2)');
|
|
140
|
+
ctx.lineWidth = 1;
|
|
141
|
+
ctx.stroke();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function drawLeapIndicator(y: number) {
|
|
145
|
+
const ctx = getCtx();
|
|
146
|
+
const ff = getFontFam();
|
|
147
|
+
const leap = new Date(y, 1, 29).getDate() === 29;
|
|
148
|
+
const lx = CX, ly = CY + 130;
|
|
149
|
+
ctx.fillStyle = c('rgba(30,30,58,0.8)', 'rgba(245,240,232,0.9)');
|
|
150
|
+
ctx.beginPath();
|
|
151
|
+
ctx.roundRect(lx - 50, ly - 10, 100, 24, 6);
|
|
152
|
+
ctx.fill();
|
|
153
|
+
ctx.strokeStyle = leap ? c('rgba(212,175,55,0.5)', 'rgba(139,105,20,0.5)') : c('rgba(60,60,90,0.3)', 'rgba(80,70,50,0.2)');
|
|
154
|
+
ctx.lineWidth = 1;
|
|
155
|
+
ctx.stroke();
|
|
156
|
+
ctx.fillStyle = leap ? c('#ffd700', '#8b6914') : c('rgba(160,160,184,0.4)', 'rgba(80,70,50,0.4)');
|
|
157
|
+
ctx.font = '600 10px ' + ff;
|
|
158
|
+
ctx.textAlign = 'center';
|
|
159
|
+
ctx.textBaseline = 'middle';
|
|
160
|
+
ctx.fillText(leap ? 'LEAP YEAR' : 'COMMON YEAR', lx, ly + 2);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function drawScene(y: number, m: number, d: number, smoothDate: number) {
|
|
164
|
+
const ctx = getCtx();
|
|
165
|
+
ctx.save();
|
|
166
|
+
ctx.clearRect(0, 0, W, H);
|
|
167
|
+
drawBg();
|
|
168
|
+
drawOuterRing();
|
|
169
|
+
drawDateNumbers(d, smoothDate);
|
|
170
|
+
drawDateHand(smoothDate);
|
|
171
|
+
drawMonthWindow(y, m);
|
|
172
|
+
drawDaySubdial(y, m, Math.round(smoothDate));
|
|
173
|
+
drawMoonSubdial(y, m, Math.round(smoothDate));
|
|
174
|
+
drawLeapIndicator(y);
|
|
175
|
+
ctx.restore();
|
|
176
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export type PerpetualCalendarUI = {
|
|
4
|
+
title: string;
|
|
5
|
+
dateLabel: string;
|
|
6
|
+
dayLabel: string;
|
|
7
|
+
monthLabel: string;
|
|
8
|
+
yearLabel: string;
|
|
9
|
+
leapYearLabel: string;
|
|
10
|
+
moonPhaseLabel: string;
|
|
11
|
+
weekdayLabel: string;
|
|
12
|
+
advanceDay: string;
|
|
13
|
+
advanceMonth: string;
|
|
14
|
+
advanceYear: string;
|
|
15
|
+
autoPlay: string;
|
|
16
|
+
resetBtn: string;
|
|
17
|
+
dayNames: string;
|
|
18
|
+
monthNames: string;
|
|
19
|
+
tipTitle: string;
|
|
20
|
+
tipContent: string;
|
|
21
|
+
step1: string;
|
|
22
|
+
step2: string;
|
|
23
|
+
step3: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type PerpetualCalendarLocaleContent = ToolLocaleContent<PerpetualCalendarUI>;
|
|
27
|
+
|
|
28
|
+
export const perpetualCalendar: ChronoToolEntry<PerpetualCalendarUI> = {
|
|
29
|
+
id: 'perpetual-calendar',
|
|
30
|
+
icons: { bg: 'mdi:calendar-text', fg: 'mdi:calendar-month' },
|
|
31
|
+
i18n: {
|
|
32
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
33
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
34
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
35
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
36
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
37
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
38
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
39
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
40
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
41
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
42
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
43
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
44
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
45
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
46
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WithContext, Thing } from 'schema-dts';
|
|
2
|
+
import type { FAQItem, HowToStep } from '../../types';
|
|
3
|
+
|
|
4
|
+
function buildFAQ(faq: FAQItem[]): WithContext<Thing> {
|
|
5
|
+
return {
|
|
6
|
+
'@context': 'https://schema.org',
|
|
7
|
+
'@type': 'FAQPage',
|
|
8
|
+
'mainEntity': faq.map((f) => ({
|
|
9
|
+
'@type': 'Question',
|
|
10
|
+
'name': f.question,
|
|
11
|
+
'acceptedAnswer': {
|
|
12
|
+
'@type': 'Answer',
|
|
13
|
+
'text': f.answer,
|
|
14
|
+
},
|
|
15
|
+
})),
|
|
16
|
+
} as unknown as WithContext<Thing>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildApp(title: string): WithContext<Thing> {
|
|
20
|
+
return {
|
|
21
|
+
'@context': 'https://schema.org',
|
|
22
|
+
'@type': 'SoftwareApplication',
|
|
23
|
+
'name': title,
|
|
24
|
+
'operatingSystem': 'All',
|
|
25
|
+
'applicationCategory': 'UtilitiesApplication',
|
|
26
|
+
'browserRequirements': 'Requires HTML5. Requires JavaScript.',
|
|
27
|
+
} as unknown as WithContext<Thing>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildHowTo(title: string, howTo: HowToStep[]): WithContext<Thing> {
|
|
31
|
+
return {
|
|
32
|
+
'@context': 'https://schema.org',
|
|
33
|
+
'@type': 'HowTo',
|
|
34
|
+
'name': title,
|
|
35
|
+
'step': howTo.map((h) => ({
|
|
36
|
+
'@type': 'HowToStep',
|
|
37
|
+
'name': h.name,
|
|
38
|
+
'text': h.text,
|
|
39
|
+
})),
|
|
40
|
+
} as unknown as WithContext<Thing>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildSchemas(
|
|
44
|
+
title: string,
|
|
45
|
+
faq: FAQItem[],
|
|
46
|
+
howTo: HowToStep[]
|
|
47
|
+
): WithContext<Thing>[] {
|
|
48
|
+
return [buildFAQ(faq), buildApp(title), buildHowTo(title, howTo)];
|
|
49
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { PerpetualCalendarUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{
|
|
8
|
+
question: 'Was ist ein ewiger Kalender in einer Uhr?',
|
|
9
|
+
answer: 'Ein ewiger Kalender ist eine mechanische Uhrenkomplikation, die automatisch das korrekte Datum, den Wochentag, den Monat und die Mondphase anzeigt und dabei unterschiedliche Monatslängen und Schaltjahre berücksichtigt. Er ist bis zum Jahr 2100 programmiert (dem nächsten Jahrhundertjahr, das nicht durch 400 teilbar ist).',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
question: 'Wie erkennt ein ewiger Kalender Schaltjahre?',
|
|
13
|
+
answer: 'Das mechanische Programm verwendet ein 48-Monats-Getriebe (4 Jahre) mit einer speziell geformten Kurvenscheibe, die den Schalttag am 29. Februar berücksichtigt. Der Mechanismus weiß, dass durch 100 teilbare Jahre keine Schaltjahre sind, es sei denn, sie sind auch durch 400 teilbar. Die meisten ewigen Kalender sind bis 2100 genau und benötigen dann eine Eintageskorrektur.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: 'Was ist der Unterschied zwischen einem ewigen und einem Jahreskalender?',
|
|
17
|
+
answer: 'Ein Jahreskalender benötigt eine manuelle Korrektur pro Jahr (Ende Februar), während ein ewiger Kalender Schaltjahre automatisch behandelt und jahrzehntelang korrekt läuft. Ewige Kalender sind mechanisch deutlich komplexer.',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const howTo = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Datum vorwärts bewegen',
|
|
24
|
+
text: 'Verwenden Sie die Tasten D (Tag), M (Monat) und Y (Jahr), um den Kalender vorwärzubewegen. Beobachten Sie, wie sich der Datumszeiger bewegt und sich das Monatsfenster ändert.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Schaltjahresübergänge beobachten',
|
|
28
|
+
text: 'Bewegen Sie sich durch den Februar eines Schaltjahres (z.B. 2024), um den Sprung vom 29. auf den 1. März zu sehen. Versuchen Sie es mit einem Nicht-Schaltjahr, um den Sprung vom 28. auf den 1. März zu beobachten.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Automatischen Ablauf nutzen',
|
|
32
|
+
text: 'Drücken Sie auf Play, um den Kalender automatisch vorlaufen zu lassen. So sehen Sie den gesamten Zyklus der Monatslängen und den Fortschritt der Mondphase.',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const title = 'Ewiger Kalender Simulator: Interaktive Uhrenkomplikation';
|
|
37
|
+
|
|
38
|
+
export const content: ToolLocaleContent<PerpetualCalendarUI> = {
|
|
39
|
+
slug: 'ewigerkalender',
|
|
40
|
+
title,
|
|
41
|
+
description: 'Entdecken Sie das mechanische Genie einer ewigen Kalender-Uhrenkomplikation. Visualisieren Sie Datum, Wochentag, Monat, Schaltjahrzyklus und Mondphase mit einem animierten Zifferblatt.',
|
|
42
|
+
ui: {
|
|
43
|
+
title: 'Ewiger Kalender Simulator',
|
|
44
|
+
dateLabel: 'Datum',
|
|
45
|
+
dayLabel: 'Tag',
|
|
46
|
+
monthLabel: 'Monat',
|
|
47
|
+
yearLabel: 'Jahr',
|
|
48
|
+
leapYearLabel: 'Schaltjahr',
|
|
49
|
+
moonPhaseLabel: 'Mondphase',
|
|
50
|
+
weekdayLabel: 'Wochentag',
|
|
51
|
+
advanceDay: 'Tag vor',
|
|
52
|
+
advanceMonth: 'Monat vor',
|
|
53
|
+
advanceYear: 'Jahr vor',
|
|
54
|
+
autoPlay: 'Auto',
|
|
55
|
+
resetBtn: 'Heute',
|
|
56
|
+
dayNames: 'Sonntag,Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag',
|
|
57
|
+
monthNames: 'Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember',
|
|
58
|
+
tipTitle: 'Tipp',
|
|
59
|
+
tipContent: 'Die meisten ewigen Kalenderuhren verwenden ein 48-Monats-Programmräderwerk mit Kerben unterschiedlicher Länge. Der Februar hat die kürzeste Kerbe (28 Tage in Normaljahren, 29 in Schaltjahren), während 30- und 31-Tage-Monate progressiv längere Kerben haben.',
|
|
60
|
+
step1: 'Bewegen Sie sich Tag für Tag durch den Februar, um zu sehen, wie der Mechanismus Monatsübergänge bewältigt.',
|
|
61
|
+
step2: 'Verfolgen Sie den Fortschritt der Mondphasenanziege durch ihren 29,5-Tage-Zyklus.',
|
|
62
|
+
step3: 'Vergleichen Sie die Februar-Übergänge von Schaltjahren und Normaljahren, um den 4-Jahres-Zyklus zu verstehen.',
|
|
63
|
+
},
|
|
64
|
+
seo: [
|
|
65
|
+
{ type: 'title', text: 'Ewiger Kalender Simulator: Interaktive Uhrenkomplikation', level: 2 },
|
|
66
|
+
{ type: 'paragraph', html: 'Der <strong>ewige Kalender</strong> ist eine der prestigeträchtigsten Komplikationen der Haute Horlogerie. Dieser interaktive Simulator visualisiert, wie ein mechanischer ewiger Kalender Datum, Wochentag, Monat, Schaltjahr und Mondphase verfolgt — jahrzehntelang ohne manuelle Korrektur. Erkunden Sie das 48-Monats-Getriebeprogramm, sehen Sie wie Februar-Übergänge funktionieren und verstehen Sie die in diesen Meisterwerken der Mikromechanik integrierte gregorianische Kalenderlogik.' },
|
|
67
|
+
{ type: 'title', text: 'Wie ein ewiger Kalender funktioniert', level: 3 },
|
|
68
|
+
{ type: 'paragraph', html: 'Ein mechanischer ewiger Kalender verwendet ein <strong>Programmräderwerk</strong> mit Kerben unterschiedlicher Tiefe, die die verschiedenen Monatslängen repräsentieren. Ein Abtasthebel fällt in jede Kerbe; eine tiefere Kerbe signalisiert einen kurzen Monat (28-29 Tage) und löst den Mechanismus aus, nach der korrekten Anzahl von Tagen auf den 1. des nächsten Monats vorzuspringen. Ein <strong>48-Monats-Getriebe</strong> bewältigt den 4-Jahres-Schaltjahrzyklus mit einer zusätzlichen Kerbe für den 29. Februar. Das Programm weiß, dass Jahrhundertjahre (z.B. 2100) das Schaltjahr auslassen, es sei denn, sie sind durch 400 teilbar.' },
|
|
69
|
+
{ type: 'title', text: 'Vergleich: Ewiger vs. Jahreskalender', level: 3 },
|
|
70
|
+
{
|
|
71
|
+
type: 'table', headers: ['Merkmal', 'Jahreskalender', 'Ewiger Kalender'], rows: [
|
|
72
|
+
['Erfordert Korrektur', 'Einmal pro Jahr (1. März)', 'Einmal pro Jahrhundert (2100)'],
|
|
73
|
+
['Schaltjahrhandhabung', 'Manuell', 'Automatisch (4-Jahres-Nocken)'],
|
|
74
|
+
['Monatserkennung', '30 vs 31 Tage', 'Vollständig 28/29/30/31'],
|
|
75
|
+
['Komplexität', 'Mäßig (~50 Teile)', 'Sehr hoch (~200+ Teile)'],
|
|
76
|
+
['Preisspanne', '3.000-15.000 €', '20.000-500.000+ €'],
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{ type: 'diagnostic', variant: 'info', title: 'Interaktiver Kalender Simulator', icon: 'mdi:calendar-month', badge: 'UHRMACHEREI', html: 'Dieses Tool simuliert den Kalendermechanismus einer ewigen Kalenderuhr. Das animierte Zifferblatt zeigt den Datumszeiger, das Monatsfenster, den Wochentag-Subdial, die Mondphase und den Schaltjahresindikator. Nutzen Sie die Steuerung, um durch Tage, Monate oder Jahre zu blättern und die mechanische Logik in Aktion zu beobachten.' },
|
|
80
|
+
],
|
|
81
|
+
faq,
|
|
82
|
+
bibliography,
|
|
83
|
+
howTo,
|
|
84
|
+
schemas: buildSchemas(title, faq, howTo),
|
|
85
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { PerpetualCalendarUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
import { buildSchemas } from '../helpers';
|
|
5
|
+
|
|
6
|
+
const faq = [
|
|
7
|
+
{
|
|
8
|
+
question: 'What is a perpetual calendar watch?',
|
|
9
|
+
answer: 'A perpetual calendar is a mechanical watch complication that displays the correct date, day, month, and moon phase automatically, accounting for months of different lengths and leap years. It is programmed to be accurate until the year 2100 (the next century year not divisible by 400).',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
question: 'How does a perpetual calendar know leap years?',
|
|
13
|
+
answer: 'The mechanical program uses a 48-month gear (4 years) with a specially designed cam that accounts for the February 29th leap day. The mechanism knows that years divisible by 100 are not leap years unless also divisible by 400. Most perpetual calendars are accurate until 2100, which will require a one-day correction.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: 'What is the difference between a perpetual and an annual calendar?',
|
|
17
|
+
answer: 'An annual calendar requires one manual correction per year (at the end of February), while a perpetual calendar automatically handles leap years and continues correctly for decades or centuries. Perpetual calendars are significantly more complex mechanically.',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const howTo = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Advance the date',
|
|
24
|
+
text: 'Use the D (day), M (month), and Y (year) buttons to advance the calendar forward. Watch the date hand sweep and the month window change.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Observe leap year transitions',
|
|
28
|
+
text: 'Advance through February of a leap year (e.g., 2024) to see the date jump from 29 to March 1. Try a non-leap year to see it skip from 28 to March 1.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Use auto-play',
|
|
32
|
+
text: 'Press play to watch the calendar automatically advance. This reveals the full cycle of month lengths and the moon phase progression.',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const title = 'Perpetual Calendar Simulator: Interactive Watch Complication';
|
|
37
|
+
|
|
38
|
+
export const content: ToolLocaleContent<PerpetualCalendarUI> = {
|
|
39
|
+
slug: 'perpetual-calendar',
|
|
40
|
+
title,
|
|
41
|
+
description: 'Explore the mechanical genius of a perpetual calendar watch complication. Visualize date, day, month, leap year cycle, and moon phase with an animated dial.',
|
|
42
|
+
ui: {
|
|
43
|
+
title: 'Perpetual Calendar Simulator',
|
|
44
|
+
dateLabel: 'Date',
|
|
45
|
+
dayLabel: 'Day',
|
|
46
|
+
monthLabel: 'Month',
|
|
47
|
+
yearLabel: 'Year',
|
|
48
|
+
leapYearLabel: 'Leap Year',
|
|
49
|
+
moonPhaseLabel: 'Moon Phase',
|
|
50
|
+
weekdayLabel: 'Weekday',
|
|
51
|
+
advanceDay: 'Advance Day',
|
|
52
|
+
advanceMonth: 'Advance Month',
|
|
53
|
+
advanceYear: 'Advance Year',
|
|
54
|
+
autoPlay: 'Auto Play',
|
|
55
|
+
resetBtn: 'Today',
|
|
56
|
+
dayNames: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday',
|
|
57
|
+
monthNames: 'January,February,March,April,May,June,July,August,September,October,November,December',
|
|
58
|
+
tipTitle: 'Tip',
|
|
59
|
+
tipContent: 'Most perpetual calendar watches use a 48-month program wheel with notches of varying lengths. February has the shortest notch (28 days in common years, 29 in leap years), while 30-day and 31-day months have progressively longer notches.',
|
|
60
|
+
step1: 'Advance day by day past February to see how the mechanism handles month-end transitions.',
|
|
61
|
+
step2: 'Watch the moon phase indicator progress through its 29.5-day cycle.',
|
|
62
|
+
step3: 'Compare leap year vs. common year February transitions to understand the 4-year cycle.',
|
|
63
|
+
},
|
|
64
|
+
seo: [
|
|
65
|
+
{ type: 'title', text: 'Perpetual Calendar Simulator: Interactive Watch Complication', level: 2 },
|
|
66
|
+
{ type: 'paragraph', html: 'The <strong>perpetual calendar</strong> is one of the most prestigious complications in haute horlogerie. This interactive simulator visualizes how a mechanical perpetual calendar tracks date, day, month, leap year, and moon phase — all without manual correction for decades. Explore the 48-month gear program, see how February transitions work, and understand the gregorian calendar logic built into these masterpieces of micro-mechanics.' },
|
|
67
|
+
{ type: 'title', text: 'How a Perpetual Calendar Works', level: 3 },
|
|
68
|
+
{ type: 'paragraph', html: 'A mechanical perpetual calendar uses a <strong>program wheel</strong> with notches of different depths representing months of different lengths. A sensing lever drops into each notch; a deeper notch signals a short month (28-29 days), triggering the mechanism to advance to the 1st of the next month after the correct number of days. A <strong>48-month gear</strong> handles the 4-year leap year cycle, with an extra notch for February 29th. The program knows that century years (e.g., 2100) skip leap year unless divisible by 400.' },
|
|
69
|
+
{ type: 'title', text: 'Calendar Logic Reference', level: 3 },
|
|
70
|
+
{
|
|
71
|
+
type: 'table', headers: ['Month', 'Days', 'Notch Depth', 'Leap Year Action'], rows: [
|
|
72
|
+
['January', '31', 'Deep', 'Normal'],
|
|
73
|
+
['February', '28 / 29', 'Shallowest', 'Extra day every 4 years'],
|
|
74
|
+
['March', '31', 'Deep', 'Normal'],
|
|
75
|
+
['April', '30', 'Medium', 'Normal'],
|
|
76
|
+
['May', '31', 'Deep', 'Normal'],
|
|
77
|
+
['June', '30', 'Medium', 'Normal'],
|
|
78
|
+
['July', '31', 'Deep', 'Normal'],
|
|
79
|
+
['August', '31', 'Deep', 'Normal'],
|
|
80
|
+
['September', '30', 'Medium', 'Normal'],
|
|
81
|
+
['October', '31', 'Deep', 'Normal'],
|
|
82
|
+
['November', '30', 'Medium', 'Normal'],
|
|
83
|
+
['December', '31', 'Deep', 'Normal'],
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{ type: 'title', text: 'Perpetual vs Annual Calendar', level: 3 },
|
|
87
|
+
{
|
|
88
|
+
type: 'table', headers: ['Feature', 'Annual Calendar', 'Perpetual Calendar'], rows: [
|
|
89
|
+
['Requires adjustment', 'Once per year (Mar 1)', 'Once per century (2100)'],
|
|
90
|
+
['Leap year handling', 'Manual', 'Automatic (4-year cam)'],
|
|
91
|
+
['Month recognition', '30 vs 31 days', 'Full 28/29/30/31'],
|
|
92
|
+
['Complexity', 'Moderate (~50 parts)', 'Very high (~200+ parts)'],
|
|
93
|
+
['Price range', '$3,000 - $15,000', '$20,000 - $500,000+'],
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{ type: 'diagnostic', variant: 'info', title: 'Interactive Calendar Simulator', icon: 'mdi:calendar-month', badge: 'HOROLOGY', html: 'This tool simulates the calendar mechanism of a perpetual calendar watch. The animated dial shows the date hand, month window, day subdial, moon phase, and leap year indicator. Use the controls to advance through days, months, or years and observe the mechanical logic in action.' },
|
|
97
|
+
],
|
|
98
|
+
faq,
|
|
99
|
+
bibliography,
|
|
100
|
+
howTo,
|
|
101
|
+
schemas: buildSchemas(title, faq, howTo),
|
|
102
|
+
};
|