@jjlmoya/utils-chrono 1.10.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.
Files changed (117) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +8 -0
  3. package/src/entries.ts +13 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tool/gear-train-explorer/bibliography.astro +16 -0
  7. package/src/tool/gear-train-explorer/bibliography.ts +12 -0
  8. package/src/tool/gear-train-explorer/client.ts +146 -0
  9. package/src/tool/gear-train-explorer/component.astro +17 -0
  10. package/src/tool/gear-train-explorer/components/GearPanel.astro +102 -0
  11. package/src/tool/gear-train-explorer/entry.ts +53 -0
  12. package/src/tool/gear-train-explorer/gear-train-explorer.css +172 -0
  13. package/src/tool/gear-train-explorer/gears.ts +148 -0
  14. package/src/tool/gear-train-explorer/helpers.ts +49 -0
  15. package/src/tool/gear-train-explorer/i18n/de.ts +99 -0
  16. package/src/tool/gear-train-explorer/i18n/en.ts +98 -0
  17. package/src/tool/gear-train-explorer/i18n/es.ts +99 -0
  18. package/src/tool/gear-train-explorer/i18n/fr.ts +99 -0
  19. package/src/tool/gear-train-explorer/i18n/id.ts +98 -0
  20. package/src/tool/gear-train-explorer/i18n/it.ts +99 -0
  21. package/src/tool/gear-train-explorer/i18n/ja.ts +98 -0
  22. package/src/tool/gear-train-explorer/i18n/ko.ts +98 -0
  23. package/src/tool/gear-train-explorer/i18n/nl.ts +99 -0
  24. package/src/tool/gear-train-explorer/i18n/pl.ts +99 -0
  25. package/src/tool/gear-train-explorer/i18n/pt.ts +99 -0
  26. package/src/tool/gear-train-explorer/i18n/ru.ts +99 -0
  27. package/src/tool/gear-train-explorer/i18n/sv.ts +99 -0
  28. package/src/tool/gear-train-explorer/i18n/tr.ts +98 -0
  29. package/src/tool/gear-train-explorer/i18n/zh.ts +98 -0
  30. package/src/tool/gear-train-explorer/index.ts +11 -0
  31. package/src/tool/gear-train-explorer/movements.ts +61 -0
  32. package/src/tool/gear-train-explorer/scene.ts +120 -0
  33. package/src/tool/gear-train-explorer/seo.astro +16 -0
  34. package/src/tool/gear-train-explorer/state.ts +30 -0
  35. package/src/tool/gmt-world-timer/bibliography.astro +11 -0
  36. package/src/tool/gmt-world-timer/bibliography.ts +7 -0
  37. package/src/tool/gmt-world-timer/client.ts +250 -0
  38. package/src/tool/gmt-world-timer/component.astro +13 -0
  39. package/src/tool/gmt-world-timer/components/GmtPanel.astro +18 -0
  40. package/src/tool/gmt-world-timer/entry.ts +34 -0
  41. package/src/tool/gmt-world-timer/gmt-world-timer.css +239 -0
  42. package/src/tool/gmt-world-timer/helpers.ts +28 -0
  43. package/src/tool/gmt-world-timer/i18n/de.ts +72 -0
  44. package/src/tool/gmt-world-timer/i18n/en.ts +72 -0
  45. package/src/tool/gmt-world-timer/i18n/es.ts +72 -0
  46. package/src/tool/gmt-world-timer/i18n/fr.ts +72 -0
  47. package/src/tool/gmt-world-timer/i18n/id.ts +72 -0
  48. package/src/tool/gmt-world-timer/i18n/it.ts +72 -0
  49. package/src/tool/gmt-world-timer/i18n/ja.ts +72 -0
  50. package/src/tool/gmt-world-timer/i18n/ko.ts +72 -0
  51. package/src/tool/gmt-world-timer/i18n/nl.ts +72 -0
  52. package/src/tool/gmt-world-timer/i18n/pl.ts +72 -0
  53. package/src/tool/gmt-world-timer/i18n/pt.ts +72 -0
  54. package/src/tool/gmt-world-timer/i18n/ru.ts +72 -0
  55. package/src/tool/gmt-world-timer/i18n/sv.ts +72 -0
  56. package/src/tool/gmt-world-timer/i18n/tr.ts +72 -0
  57. package/src/tool/gmt-world-timer/i18n/zh.ts +72 -0
  58. package/src/tool/gmt-world-timer/index.ts +11 -0
  59. package/src/tool/gmt-world-timer/seo.astro +11 -0
  60. package/src/tool/perpetual-calendar/bibliography.astro +16 -0
  61. package/src/tool/perpetual-calendar/bibliography.ts +16 -0
  62. package/src/tool/perpetual-calendar/calendar.ts +24 -0
  63. package/src/tool/perpetual-calendar/client.ts +98 -0
  64. package/src/tool/perpetual-calendar/component.astro +17 -0
  65. package/src/tool/perpetual-calendar/components/CalendarPanel.astro +49 -0
  66. package/src/tool/perpetual-calendar/dial.ts +176 -0
  67. package/src/tool/perpetual-calendar/entry.ts +48 -0
  68. package/src/tool/perpetual-calendar/helpers.ts +49 -0
  69. package/src/tool/perpetual-calendar/i18n/de.ts +85 -0
  70. package/src/tool/perpetual-calendar/i18n/en.ts +102 -0
  71. package/src/tool/perpetual-calendar/i18n/es.ts +85 -0
  72. package/src/tool/perpetual-calendar/i18n/fr.ts +85 -0
  73. package/src/tool/perpetual-calendar/i18n/id.ts +85 -0
  74. package/src/tool/perpetual-calendar/i18n/it.ts +85 -0
  75. package/src/tool/perpetual-calendar/i18n/ja.ts +85 -0
  76. package/src/tool/perpetual-calendar/i18n/ko.ts +85 -0
  77. package/src/tool/perpetual-calendar/i18n/nl.ts +85 -0
  78. package/src/tool/perpetual-calendar/i18n/pl.ts +85 -0
  79. package/src/tool/perpetual-calendar/i18n/pt.ts +85 -0
  80. package/src/tool/perpetual-calendar/i18n/ru.ts +85 -0
  81. package/src/tool/perpetual-calendar/i18n/sv.ts +85 -0
  82. package/src/tool/perpetual-calendar/i18n/tr.ts +85 -0
  83. package/src/tool/perpetual-calendar/i18n/zh.ts +85 -0
  84. package/src/tool/perpetual-calendar/index.ts +11 -0
  85. package/src/tool/perpetual-calendar/perpetual-calendar.css +181 -0
  86. package/src/tool/perpetual-calendar/seo.astro +16 -0
  87. package/src/tool/perpetual-calendar/state.ts +26 -0
  88. package/src/tool/tourbillon-visualizer/bibliography.astro +11 -0
  89. package/src/tool/tourbillon-visualizer/bibliography.ts +7 -0
  90. package/src/tool/tourbillon-visualizer/client.ts +122 -0
  91. package/src/tool/tourbillon-visualizer/component.astro +126 -0
  92. package/src/tool/tourbillon-visualizer/components/TourbillonPanel.astro +66 -0
  93. package/src/tool/tourbillon-visualizer/entry.ts +51 -0
  94. package/src/tool/tourbillon-visualizer/helpers.ts +35 -0
  95. package/src/tool/tourbillon-visualizer/i18n/de.ts +96 -0
  96. package/src/tool/tourbillon-visualizer/i18n/en.ts +96 -0
  97. package/src/tool/tourbillon-visualizer/i18n/es.ts +96 -0
  98. package/src/tool/tourbillon-visualizer/i18n/fr.ts +96 -0
  99. package/src/tool/tourbillon-visualizer/i18n/id.ts +96 -0
  100. package/src/tool/tourbillon-visualizer/i18n/it.ts +96 -0
  101. package/src/tool/tourbillon-visualizer/i18n/ja.ts +96 -0
  102. package/src/tool/tourbillon-visualizer/i18n/ko.ts +96 -0
  103. package/src/tool/tourbillon-visualizer/i18n/nl.ts +96 -0
  104. package/src/tool/tourbillon-visualizer/i18n/pl.ts +96 -0
  105. package/src/tool/tourbillon-visualizer/i18n/pt.ts +96 -0
  106. package/src/tool/tourbillon-visualizer/i18n/ru.ts +96 -0
  107. package/src/tool/tourbillon-visualizer/i18n/sv.ts +96 -0
  108. package/src/tool/tourbillon-visualizer/i18n/tr.ts +96 -0
  109. package/src/tool/tourbillon-visualizer/i18n/zh.ts +96 -0
  110. package/src/tool/tourbillon-visualizer/index.ts +11 -0
  111. package/src/tool/tourbillon-visualizer/renderer/base.ts +78 -0
  112. package/src/tool/tourbillon-visualizer/renderer/cage.ts +115 -0
  113. package/src/tool/tourbillon-visualizer/renderer/esc.ts +160 -0
  114. package/src/tool/tourbillon-visualizer/seo.astro +11 -0
  115. package/src/tool/tourbillon-visualizer/state.ts +21 -0
  116. package/src/tool/tourbillon-visualizer/tourbillon.ts +9 -0
  117. package/src/tools.ts +8 -0
@@ -0,0 +1,72 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { GMTWorldTimerUI } from '../entry';
3
+ import { bibliography } from '../bibliography';
4
+ import { buildSchemas } from '../helpers';
5
+
6
+ const faq = [
7
+ {
8
+ question: 'Hur vet jag om en klocka är en äkta GMT eller en caller GMT?',
9
+ answer: 'En <strong>äkta GMT</strong> (även kallad "flyer"-GMT) låter dig hoppa den lokala timvisaren oberoende — idealisk för resenärer som ofta byter tidszon. En <strong>caller GMT</strong> justerar GMT-visaren separat medan huvudtimvisaren är stilla, vilket är billigare att tillverka. Så här skiljer du dem åt: dra ut kronan till tidsinställningsläget och vrid. Om timvisaren hoppar i steg om en timme utan att stanna är det en äkta GMT. Om GMT-visaren rör sig istället är det en caller. Rolex använder ett äkta GMT-kaliber (3285) i GMT-Master II, medan många prisvärda mikromärken använder caller-verk som Seiko NH34.',
10
+ },
11
+ {
12
+ question: 'Vad är skillnaden mellan en GMT-klocka och en World Timer?',
13
+ answer: 'En <strong>GMT-klocka</strong> håller vanligtvis koll på två tidszoner — din lokala tid och en referens (oftast UTC) — med hjälp av en 24-timmarsvisare och en 24-timmarsring. En <strong>World Timer</strong> visar alla 24 tidszoner på en gång: den har en stadsring runt urtavlan och en roterande 24-timmarsskiva. World Timers som Patek Philippe 5230P eller JLC Geophysic Universal Time låter dig läsa av tiden i vilken stad som helst direkt. GMT-klockor är enklare och mer prisvärda; World Timers är mekaniskt mer komplexa och vanligtvis dyrare. Det här verktyget fungerar som en digital World Timer och låter dig lägga till så många städer du behöver.',
14
+ },
15
+ {
16
+ question: 'Vilken tidszon visar min klocka när det står "GMT"?',
17
+ answer: 'När en klocka har "GMT" på urtavlan pekar <strong>GMT-visaren</strong> (vanligtvis en fjärde visare med en färgad pilspets) på tiden i 24-timmarsformat. De flesta ägare ställer in denna visare på UTC (koordinerad universell tid) eftersom alla tidszoner definieras som avvikelser från UTC. Den roterande 24-timmarsringen kan sedan justeras för att läsa av vilken annan tidszon som helst. Om GMT-visaren till exempel pekar på 14 (kl 14) och dina ringmarkeringar är inställda på UTC+2, läser du av östeuropeisk tid. Det här verktyget hjälper dig att visualisera precis det förhållandet.',
18
+ },
19
+ ];
20
+
21
+ const howTo = [
22
+ {
23
+ name: 'Lägg till valfri stad på din instrumentpanel',
24
+ text: 'Skriv ett stadsnamn eller en tidszon i sökfältet. Klicka på ett resultat för att lägga till det direkt. Varje stad visas som ett liveklockkort med aktuell lokal tid.',
25
+ },
26
+ {
27
+ name: 'Ta bort städer när du inte behöver dem',
28
+ text: 'Hovra över ett klockkort och klicka på ×-knappen för att ta bort det. Ditt urval sparas automatiskt i din webbläsare — stäng och kom tillbaka senare, din instrumentpanel är precis som du lämnade den.',
29
+ },
30
+ {
31
+ name: 'Använd den som GMT-referens för din klockkollektion',
32
+ text: 'Ange din hemstad och lägg till de tidszoner du följer med dina GMT-klockor. Använd live-offset-etiketterna för att kontrollera om din ringinställning är korrekt för varje zon.',
33
+ },
34
+ ];
35
+
36
+ const title = 'Världsklocka: Live Dashboard med Flera Tidszoner';
37
+
38
+ export const content: ToolLocaleContent<GMTWorldTimerUI> = {
39
+ slug: 'gmt-varldsklocka',
40
+ title,
41
+ description: 'Följ flera tidszoner live. Lägg till valfri stad och se den aktuella tiden uppdateras varje sekund. Perfekt för klockentusiaster med GMT- eller World Timer-klockor.',
42
+ ui: {
43
+ title: 'Världsklocka',
44
+ searchPlaceholder: 'Sök stad eller tidszon...',
45
+ addLabel: 'Lägg till',
46
+ removeLabel: 'Ta bort',
47
+ noResults: 'Inga städer hittades',
48
+ yourZones: 'Dina tidszoner',
49
+ },
50
+ seo: [
51
+ { type: 'title', text: 'Världsklocka — Live Dashboard för Tidszoner för Klockentusiaster', level: 2 },
52
+ { type: 'paragraph', html: 'Oavsett om du äger en <strong>GMT-Master II</strong>, en <strong>World Timer</strong>, eller helt enkelt behöver hålla koll på flera tidszoner för arbete eller resor, visar denna live-instrumentpanel den aktuella tiden i varje stad du bryr dig om — allt på en gång. Lägg till New York, London, Tokyo eller vilken stad som helst, och tiden uppdateras varje sekund. Dina zoner sparas i webbläsaren så att du aldrig behöver konfigurera om.' },
53
+ { type: 'title', text: 'Varför klockentusiaster behöver en världsklocka', level: 3 },
54
+ { type: 'paragraph', html: 'Om du samlar på <strong>GMT-klockor</strong> känner du till problemet: du ställer in ringen för att följa en andra tidszon, men offset ändras med sommartid, eller så har du flera GMT och vill jämföra hur var och en följer en annan stad. Det här verktyget löser det. Lägg till de städer dina klockor följer och se omedelbart deras aktuella offset och tid. Ingen huvudräkning — titta bara på kortet och vet exakt var din GMT-visare ska peka.' },
55
+ { type: 'title', text: 'GMT vs World Timer — Vilken passar din stil?', level: 3 },
56
+ { type: 'paragraph', html: 'En <strong>GMT-klocka</strong> (som Rolex GMT-Master II "Pepsi" eller Tudor Black Bay Pro) använder en 24-timmarsvisare och en roterande ring för att följa två tidszoner. En <strong>World Timer</strong> (som Nomos Zürich Weltzeit eller Omega Seamaster Worldtimer) visar alla 24 zoner samtidigt med en stadsring och en 24-timmarsskiva. Denna instrumentpanel fungerar som en World Timer: du kan se alla städer samtidigt. Använd den för att bestämma vilken komplikation som passar din livsstil innan du köper.' },
57
+ { type: 'title', text: 'Praktisk användning utöver klockinsamling', level: 3 },
58
+ {
59
+ type: 'list', items: [
60
+ 'Distansarbetare som schemalägger möten över tidszoner utan förvirring',
61
+ 'Frekventa resenärer som håller koll på hemmet och destinationen samtidigt',
62
+ 'Traders som följer marknadsöppningar i New York, London, Tokyo och Sydney',
63
+ 'Alla som vill veta "vad är klockan i..." utan att googla',
64
+ ]
65
+ },
66
+ { type: 'diagnostic', variant: 'info', title: 'Live Världsklocka', icon: 'mdi:clock-time-eight', badge: 'TID', html: 'Tiderna uppdateras live varje sekund med hjälp av webbläsarens inbyggda tidszondatabas. Sommartidsövergångar hanteras automatiskt. Inga data skickas till någon server.' },
67
+ ],
68
+ faq,
69
+ bibliography,
70
+ howTo,
71
+ schemas: buildSchemas(title, faq, howTo),
72
+ };
@@ -0,0 +1,72 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { GMTWorldTimerUI } from '../entry';
3
+ import { bibliography } from '../bibliography';
4
+ import { buildSchemas } from '../helpers';
5
+
6
+ const faq = [
7
+ {
8
+ question: 'Bir saatin gerçek GMT mi yoksa caller GMT mi olduğunu nasıl anlarım?',
9
+ answer: '<strong>Gerçek GMT</strong> ("flyer" GMT olarak da bilinir) yerel saat akrebinin bağımsız olarak ayarlanmasına olanak tanır — sık sık saat dilimi değiştiren gezginler için idealdir. <strong>Caller GMT</strong>, ana akrep sabit kalırken GMT kolunun ayrı olarak ayarlandığı, üretimi daha ucuz olan bir mekanizmadır. Ayırt etmek için: kurma kolunu saat ayar konumuna çekin ve çevirin. Akrep bir saatlik artışlarla durmadan atlıyorsa gerçek GMT\'dir. GMT kolu hareket ediyorsa caller\'dır. Rolex, GMT-Master II\'de gerçek GMT kalibresi (3285) kullanırken, birçok uygun fiyatlı mikromarka Seiko NH34 gibi caller mekanizmaları tercih eder.',
10
+ },
11
+ {
12
+ question: 'GMT saat ile World Timer arasındaki fark nedir?',
13
+ answer: 'Bir <strong>GMT saati</strong> genellikle iki saat dilimini takip eder — yerel saatiniz ve bir referans (genellikle UTC) — 24 saat kolu ve 24 saat bezeli kullanarak. Bir <strong>World Timer</strong> ise 24 saat dilimini aynı anda gösterir: kadranın etrafında bir şehir halkası ve dönen bir 24 saat diski bulunur. Patek Philippe 5230P veya JLC Geophysic Universal Time gibi World Timer\'lar, herhangi bir şehirdeki saati anında okumanızı sağlar. GMT\'ler daha basit ve daha uygun fiyatlıdır; World Timer\'lar mekanik olarak daha karmaşık ve genellikle daha pahalıdır. Bu araç, ihtiyacınız kadar şehir eklemenize izin veren dijital bir World Timer gibi çalışır.',
14
+ },
15
+ {
16
+ question: 'Kadranında "GMT" yazan bir saat hangi saat dilimini gösterir?',
17
+ answer: 'Bir saatin kadranında "GMT" yazdığında, <strong>GMT kolu</strong> (genellikle renkli bir ok ucuna sahip dördüncü kol) saati 24 saat formatında gösterir. Çoğu kullanıcı bu kolu UTC\'ye (Eşgüdümlü Evrensel Zaman) ayarlar çünkü tüm saat dilimleri UTC\'den fark olarak tanımlanır. Dönen 24 saat bezeli daha sonra herhangi bir başka saat dilimini okuyacak şekilde hizalanabilir. Örneğin, GMT kolu 14\'ü (öğleden sonra 2) gösteriyorsa ve bezel işaretleriniz UTC+2\'ye hizalanmışsa, Doğu Avrupa Saati\'ni okuyorsunuzdur. Bu araç, tam olarak bu ilişkiyi görselleştirmenize yardımcı olur.',
18
+ },
19
+ ];
20
+
21
+ const howTo = [
22
+ {
23
+ name: 'Panonuza istediğiniz şehri ekleyin',
24
+ text: 'Arama çubuğuna bir şehir adı veya saat dilimi yazın. Bir sonuca tıklayarak anında ekleyin. Her şehir, geçerli yerel saati gösteren canlı bir saat kartı olarak görünür.',
25
+ },
26
+ {
27
+ name: 'İhtiyacınız olmayan şehirleri kaldırın',
28
+ text: 'Herhangi bir saat kartının üzerine gelin ve kaldırmak için × düğmesine tıklayın. Seçiminiz tarayıcınıza otomatik olarak kaydedilir — kapatın ve daha sonra geri gelin, panonuz tam olarak bıraktığınız gibi durur.',
29
+ },
30
+ {
31
+ name: 'Saat koleksiyonunuz için GMT referansı olarak kullanın',
32
+ text: 'Ana şehrinizi ayarlayın ve GMT saatlerinizle takip ettiğiniz saat dilimlerini ekleyin. Her bir bölge için bezel hizalamanızın doğru olup olmadığını kontrol etmek için canlı fark etiketlerini kullanın.',
33
+ },
34
+ ];
35
+
36
+ const title = 'Dünya Saati: Canlı Çoklu Saat Dilimi Panosu';
37
+
38
+ export const content: ToolLocaleContent<GMTWorldTimerUI> = {
39
+ slug: 'gmt-dunya-saati',
40
+ title,
41
+ description: 'Birden fazla saat dilimini canlı olarak takip edin. Herhangi bir şehri ekleyin ve geçerli saatin her saniye güncellendiğini görün. GMT veya World Timer saati olan saat meraklıları için mükemmel.',
42
+ ui: {
43
+ title: 'Dünya Saati',
44
+ searchPlaceholder: 'Şehir veya saat dilimi ara...',
45
+ addLabel: 'Ekle',
46
+ removeLabel: 'Kaldır',
47
+ noResults: 'Şehir bulunamadı',
48
+ yourZones: 'Saat Dilimleriniz',
49
+ },
50
+ seo: [
51
+ { type: 'title', text: 'Dünya Saati — Saat Meraklıları için Canlı Saat Dilimi Panosu', level: 2 },
52
+ { type: 'paragraph', html: 'İster bir <strong>GMT-Master II</strong>\'niz, ister bir <strong>World Timer</strong>\'ınız olsun, ya da sadece iş veya seyahat için birden fazla saat dilimini takip etmeniz gereksin, bu canlı pano önemsediğiniz her şehirdeki güncel saati bir bakışta gösterir. New York, Londra, Tokyo veya herhangi bir şehri ekleyin ve saat her saniye güncellensin. Bölgeleriniz tarayıcınıza kaydedilir, böylece asla yeniden yapılandırmanız gerekmez.' },
53
+ { type: 'title', text: 'Saat Meraklıları Neden Bir Dünya Saatine İhtiyaç Duyar?', level: 3 },
54
+ { type: 'paragraph', html: 'Eğer <strong>GMT saatleri</strong> koleksiyonu yapıyorsanız, zorluğu bilirsiniz: ikinci bir saat dilimini takip etmek için bezeli ayarlarsınız, ancak farklar Yaz Saati ile değişir veya birden fazla GMT\'niz vardır ve her birinin farklı bir şehri nasıl takip ettiğini karşılaştırmak istersiniz. Bu araç bunu çözer. Saatlerinizin takip ettiği şehirleri ekleyin ve anlık farklarını ve saatlerini görün. Artık zihinsel hesaplama yok — sadece karta bakın ve GMT kolunuzun tam olarak nereyi göstermesi gerektiğini bilin.' },
55
+ { type: 'title', text: 'GMT vs World Timer — Hangisi Tarzınıza Uyuyor?', level: 3 },
56
+ { type: 'paragraph', html: '<strong>GMT saati</strong> (Rolex GMT-Master II "Pepsi" veya Tudor Black Bay Pro gibi) iki saat dilimini takip etmek için 24 saat kolu ve dönen bir bezel kullanır. <strong>World Timer</strong> (Nomos Zürich Weltzeit veya Omega Seamaster Worldtimer gibi) bir şehir halkası ve 24 saat diski kullanarak 24 bölgenin tamamını aynı anda gösterir. Bu pano bir World Timer\'ı taklit eder: tüm şehirleri aynı anda görebilirsiniz. Satın almadan önce hangi komplikasyonun yaşam tarzınıza uyduğuna karar vermek için kullanın.' },
57
+ { type: 'title', text: 'Saat Koleksiyonculuğunun Ötesinde Pratik Kullanımlar', level: 3 },
58
+ {
59
+ type: 'list', items: [
60
+ 'Saat dilimleri arasında toplantı planlayan uzaktan çalışanlar',
61
+ 'Evi ve varış noktasını aynı anda takip eden sık seyahat edenler',
62
+ 'New York, Londra, Tokyo ve Sidney\'deki piyasa açılışlarını takip eden yatırımcılar',
63
+ 'Google\'a sormadan "... şu anda saat kaç?" bilmek isteyen herkes',
64
+ ]
65
+ },
66
+ { type: 'diagnostic', variant: 'info', title: 'Canlı Dünya Saati', icon: 'mdi:clock-time-eight', badge: 'SAAT', html: 'Saatler, tarayıcınızın yerleşik saat dilimi veritabanını kullanarak her saniye canlı olarak güncellenir. Yaz Saati geçişleri otomatik olarak işlenir. Hiçbir veri herhangi bir sunucuya gönderilmez.' },
67
+ ],
68
+ faq,
69
+ bibliography,
70
+ howTo,
71
+ schemas: buildSchemas(title, faq, howTo),
72
+ };
@@ -0,0 +1,72 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { GMTWorldTimerUI } from '../entry';
3
+ import { bibliography } from '../bibliography';
4
+ import { buildSchemas } from '../helpers';
5
+
6
+ const faq = [
7
+ {
8
+ question: '如何判断手表是真GMT还是呼叫器GMT?',
9
+ answer: '<strong>真GMT</strong>(又称"飞行者"GMT)可以独立调整本地时针——适合频繁跨时区旅行的用户。<strong>呼叫器GMT</strong>则是在主时针不动的情况下单独调整GMT指针,制造成本较低。区分方法:将表冠拉出到时间设定位置并旋转。如果时针以一小时为单位跳动而不停顿,就是真GMT;如果GMT指针随之移动,就是呼叫器GMT。劳力士在GMT-Master II中使用真GMT机芯(3285),而许多平价微品牌则采用精工NH34等呼叫器GMT机芯。',
10
+ },
11
+ {
12
+ question: 'GMT手表和世界时手表有什么区别?',
13
+ answer: '<strong>GMT手表</strong>通常通过24小时指针和24小时表圈来追踪两个时区——您的本地时间和一个参考时间(通常是UTC)。<strong>世界时手表</strong>则同时显示全部24个时区:表盘周围有城市圈,并有一个旋转的24小时盘。百达翡丽5230P或积家地球物理天文台世界时等世界时手表可以让您即时读取任何城市的时间。GMT更简单、更实惠;世界时机芯更复杂,价格通常更高。本工具就像一个数字版世界时手表,您可以根据需要添加任意数量的城市。',
14
+ },
15
+ {
16
+ question: '手表表盘上写着"GMT"时,显示的是哪个时区?',
17
+ answer: '当手表表盘上标有"GMT"时,<strong>GMT指针</strong>(通常是带有彩色箭头的第四根指针)以24小时制指示时间。大多数用户将此指针设置为UTC(协调世界时),因为所有时区都定义为相对于UTC的偏移量。旋转式24小时表圈可以对齐以读取任何其他时区。例如,如果GMT指针指向14(下午2点),而您的表圈标记对齐到UTC+2,那么您读出的就是东欧时间。本工具可以帮助您直观地理解这种对应关系。',
18
+ },
19
+ ];
20
+
21
+ const howTo = [
22
+ {
23
+ name: '将任意城市添加到您的仪表盘',
24
+ text: '在搜索栏中输入城市名称或时区。点击搜索结果即可立即添加。每个城市都会以实时时钟卡片的形式显示当前的当地时间。',
25
+ },
26
+ {
27
+ name: '移除不需要的城市',
28
+ text: '将鼠标悬停在任意时钟卡片上,点击×按钮即可移除。您的选择会自动保存在浏览器中——关闭后再次打开,仪表盘将保持原样。',
29
+ },
30
+ {
31
+ name: '将其用作手表收藏的GMT参考',
32
+ text: '设置您的家乡城市,并添加您通过GMT手表追踪的时区。使用实时偏移标签检查每个追踪时区的表圈对齐是否正确。',
33
+ },
34
+ ];
35
+
36
+ const title = '世界时钟:多时区实时仪表盘';
37
+
38
+ export const content: ToolLocaleContent<GMTWorldTimerUI> = {
39
+ slug: 'gmt-world-timer',
40
+ title,
41
+ description: '实时追踪多个时区。添加任意城市,实时查看每秒更新的当前时间。适合拥有GMT或世界时手表的钟表爱好者。',
42
+ ui: {
43
+ title: '世界时钟',
44
+ searchPlaceholder: '搜索城市或时区...',
45
+ addLabel: '添加',
46
+ removeLabel: '移除',
47
+ noResults: '未找到城市',
48
+ yourZones: '我的时区',
49
+ },
50
+ seo: [
51
+ { type: 'title', text: '世界时钟 — 为钟表爱好者打造的实时时区仪表盘', level: 2 },
52
+ { type: 'paragraph', html: '无论您拥有<strong>GMT-Master II</strong>、<strong>世界时手表</strong>,还是仅仅需要为工作或旅行追踪多个时区,这个实时仪表盘都能让您一目了然地看到每个关注城市的当前时间。添加纽约、伦敦、东京或任意城市,时间每秒更新。您的时区保存在浏览器中,无需重复配置。' },
53
+ { type: 'title', text: '为什么钟表爱好者需要世界时钟', level: 3 },
54
+ { type: 'paragraph', html: '如果您收藏<strong>GMT手表</strong>,一定遇到过这样的困扰:您调好表圈追踪第二个时区,但夏令时导致偏移变化,或者您拥有多块GMT手表,想比较它们如何追踪不同城市。本工具可以解决这个问题。添加您手表追踪的城市,立即查看它们的当前偏移和时间。无需心算——只需看一眼卡片,就能确切知道您的GMT指针应该指向哪里。' },
55
+ { type: 'title', text: 'GMT与世界时 — 哪种风格适合您?', level: 3 },
56
+ { type: 'paragraph', html: '<strong>GMT手表</strong>(如劳力士GMT-Master II"百事圈"或帝舵Black Bay Pro)使用24小时指针和旋转表圈来追踪两个时区。<strong>世界时手表</strong>(如诺莫斯苏黎世世界时或欧米茄海马世界时)通过城市圈和24小时盘同时显示所有24个时区。本仪表盘模拟了世界时手表:您可以同时查看所有城市。在购买之前,用它来帮助您决定哪种复杂功能更适合您的生活方式。' },
57
+ { type: 'title', text: '超越手表收藏的实用功能', level: 3 },
58
+ {
59
+ type: 'list', items: [
60
+ '远程工作者跨时区安排会议,不再混淆',
61
+ '频繁旅行者同时关注家乡和目的地的时间',
62
+ '交易者追踪纽约、伦敦、东京和悉尼的市场开盘时间',
63
+ '任何人想快速知道"...现在几点?"而无需搜索',
64
+ ]
65
+ },
66
+ { type: 'diagnostic', variant: 'info', title: '实时世界时钟', icon: 'mdi:clock-time-eight', badge: '时间', html: '时间使用浏览器内置的时区数据库每秒实时更新。夏令时转换自动处理。不会向任何服务器发送任何数据。' },
67
+ ],
68
+ faq,
69
+ bibliography,
70
+ howTo,
71
+ schemas: buildSchemas(title, faq, howTo),
72
+ };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { gmtWorldTimer } from './entry';
3
+
4
+ export * from './entry';
5
+
6
+ export const GMT_WORLD_TIMER_TOOL: ToolDefinition = {
7
+ entry: gmtWorldTimer,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,11 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { gmtWorldTimer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+ interface Props { locale?: KnownLocale; }
6
+ const { locale = 'en' } = Astro.props;
7
+ const loader = gmtWorldTimer.i18n[locale] || gmtWorldTimer.i18n.en;
8
+ const content = await loader?.();
9
+ if (!content) return null;
10
+ ---
11
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { perpetualCalendar } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props as Props;
11
+ const loader = perpetualCalendar.i18n[locale] || perpetualCalendar.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Perpetual Calendar - Wikipedia',
6
+ url: 'https://en.wikipedia.org/wiki/Perpetual_calendar',
7
+ },
8
+ {
9
+ name: 'Patek Philippe Perpetual Calendar',
10
+ url: 'https://www.patek.com/en/manufacture/quality-and-fine-workmanship/calendar-watches',
11
+ },
12
+ {
13
+ name: 'How a Perpetual Calendar Works - Hodinkee',
14
+ url: 'https://www.hodinkee.com/watch101/perpetual-calendar',
15
+ },
16
+ ];
@@ -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">&#9664;D</button>
23
+ <button data-action="day-next" title="Day +1">D&#9654;</button>
24
+ <button data-action="month-next" title="Month +1">M&#9654;</button>
25
+ <button data-action="year-next" title="Year +1">Y&#9654;</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">&#9654;</button>
33
+ <button data-action="reset" title="Reset to today">&#9673;</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
+ }