<style>
/* ============== ТЕМАТИЧЕСКИЕ ПЕРЕМЕННЫЕ ============== */
:root {
--bg-body: #f5f5f5;
--bg-card: #ffffff;
--bg-even: #fafafa;
--bg-key: #fff8f8;
--bg-sol: #f0f7ff;
--text-primary: #2c3e50;
--text-secondary: #555555;
--text-muted: #888888;
--border: #e0e0e0;
--border-header: #f0e0e0;
--banner-grad: linear-gradient(135deg, #c41e3a, #e74c3c);
--banner-text: #ffffff;
--th-bg: #c41e3a;
--th-text: #ffffff;
--tooltip-bg: #2c3e50;
--tooltip-text: #ecf0f1;
--tooltip-title: #ffd700;
--info-bg: #e8f5e9;
--info-text: #2e7d32;
--shadow: 0 2px 6px rgba(0,0,0,0.06);
--input-bg: #ffffff;
--input-text: #333333;
--btn-bg: #c41e3a;
--btn-text: #ffffff;
--btn-hover: #a01830;
}
[data-theme="dark"] {
--bg-body: #121212;
--bg-card: #1e1e1e;
--bg-even: #252525;
--bg-key: #2a1f1f;
--bg-sol: #1f2530;
--text-primary: #e0e0e0;
--text-secondary: #aaaaaa;
--text-muted: #777777;
--border: #333333;
--border-header: #2a2a2a;
--banner-grad: linear-gradient(135deg, #8b0000, #b71c1c);
--banner-text: #ffffff;
--th-bg: #a41623;
--th-text: #ffffff;
--tooltip-bg: #2d2d2d;
--tooltip-text: #e0e0e0;
--tooltip-title: #ffd700;
--info-bg: #1b3320;
--info-text: #66bb6a;
--shadow: 0 4px 12px rgba(0,0,0,0.4);
--input-bg: #2a2a2a;
--input-text: #e0e0e0;
--btn-bg: #b71c1c;
--btn-text: #ffffff;
--btn-hover: #d32f2f;
}
/* ============== БАЗОВЫЕ СТИЛИ ============== */
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 900px; margin: 0 auto; padding: 20px;
background: var(--bg-body); color: var(--text-primary);
transition: background 0.3s ease, color 0.3s ease;
}
h1 { color: var(--th-bg); text-align: center; margin-bottom: 5px; transition: color 0.3s; }
.subtitle { text-align: center; color: var(--text-secondary); font-size: 14px; margin-bottom: 20px; transition: color 0.3s; }
.input-group { display: flex; gap: 10px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }
input {
padding: 10px 15px; font-size: 16px; border: 2px solid var(--th-bg);
border-radius: 5px; width: 120px; text-align: center;
background: var(--input-bg); color: var(--input-text); transition: all 0.3s;
}
button {
padding: 10px 16px; font-size: 16px; background: var(--btn-bg); color: var(--btn-text);
border: none; border-radius: 5px; cursor: pointer; transition: background 0.3s, transform 0.1s;
}
button:hover { background: var(--btn-hover); }
button:active { transform: scale(0.97); }
.theme-btn {
background: transparent; border: 2px solid var(--th-bg); font-size: 20px; padding: 8px 12px;
}
.theme-btn:hover { background: var(--th-bg); color: white; }
.banner {
background: var(--banner-grad); color: var(--banner-text); padding: 20px;
border-radius: 15px; text-align: center; margin-bottom: 20px;
transition: background 0.3s;
}
.banner h2 { margin: 0 0 5px; }
.banner .method { font-size: 13px; opacity: 0.9; }
.section {
background: var(--bg-card); border-radius: 10px; padding: 15px; margin-bottom: 15px;
box-shadow: var(--shadow); transition: background 0.3s, box-shadow 0.3s;
}
.section h2 {
color: var(--th-bg); margin: 0 0 10px; border-bottom: 2px solid var(--border-header);
padding-bottom: 8px; font-size: 18px; transition: color 0.3s, border-color 0.3s;
}
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th {
background: var(--th-bg); color: var(--th-text); padding: 10px;
text-align: left; font-weight: 500; transition: background 0.3s;
}
th:last-child { text-align: right; }
td {
padding: 9px 10px; border-bottom: 1px solid var(--border); vertical-align: top;
transition: border-color 0.3s;
}
td:last-child { text-align: right; }
tr:nth-child(even) { background: var(--bg-even); transition: background 0.3s; }
tr.key { background: var(--bg-key); transition: background 0.3s; }
tr.sol { background: var(--bg-sol); transition: background 0.3s; }
.event-cell { display: flex; align-items: center; gap: 8px; cursor: help; }
.icon { font-size: 20px; line-height: 1; }
.zh { font-size: 16px; color: var(--text-primary); font-weight: 500; }
.ru { color: var(--text-secondary); font-size: 13px; }
.en { color: var(--text-muted); font-size: 12px; font-style: italic; }
.time { color: var(--text-secondary); font-size: 12px; margin-left: 4px; }
/* Tooltip */
.tooltip { position: relative; display: inline-block; }
.tooltip:hover .tooltip-content { visibility: visible; opacity: 1; transform: translateY(0); }
.tooltip-content {
visibility: hidden; opacity: 0;
position: absolute; bottom: 125%; left: 50%; transform: translateY(10px);
background: var(--tooltip-bg); color: var(--tooltip-text);
padding: 12px 16px; border-radius: 8px; font-size: 13px; line-height: 1.5;
max-width: 350px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.25);
transition: all 0.2s ease; text-align: left;
}
.tooltip-content::after {
content: ""; position: absolute; top: 100%; left: 50%; margin-left: -6px;
border: 6px solid transparent; border-top-color: var(--tooltip-bg);
}
.tooltip-title { font-weight: bold; margin-bottom: 6px; color: var(--tooltip-title); font-size: 14px; }
.tooltip-desc { margin-bottom: 6px; }
.tooltip-tradition { font-style: italic; color: var(--text-muted); font-size: 12px; }
.info {
background: var(--info-bg); padding: 15px; border-radius: 10px; margin-top: 20px;
font-size: 13px; color: var(--info-text); line-height: 1.5; transition: all 0.3s;
}
.total { text-align: center; color: var(--text-secondary); margin-top: 15px; font-size: 13px; transition: color 0.3s; }
@media (max-width: 600px) {
.tooltip-content { left: auto; right: 0; max-width: 280px; }
.tooltip-content::after { left: auto; right: 20px; margin-left: 0; }
}
</style>
<h1>🧧 Китайский календарь</h1>
<div class="subtitle">Солнечные термины (Meeus) + Лунные праздники</div>
<div class="input-group">
<input type="number" id="yearInput" placeholder="Год" min="1900" max="2100" value="2026">
<button onclick="show()">Рассчитать</button>
<button id="themeToggle" class="theme-btn" title="Переключить тему">🌙</button>
</div>
<div id="output"></div>
<script>
// ============== 1. Meeus Solar ==============
var Meeus = (function() {
const J2000 = 2451545.0;
const JC = 36525.0;
function rad(deg) { return deg * Math.PI / 180; }
function apparentLongitudeDeg(T) {
let L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
let M = 357.52911 + 35999.05029 * T - 0.0001537 * T * T;
let C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(rad(M))
+ (0.019993 - 0.000101 * T) * Math.sin(rad(2 * M))
+ 0.000289 * Math.sin(rad(3 * M));
let lonGeo = L0 + C;
let omega = 125.04 - 1934.136 * T;
let corr = -0.00569 - 0.00478 * Math.sin(rad(omega));
return lonGeo + corr;
}
function findTermDate(year, targetDeg) {
let startJD = (new Date(Date.UTC(year, 0, 1)).getTime() / 86400000) + 2440587.5;
let T0 = (startJD - J2000) / JC;
let lon0 = apparentLongitudeDeg(T0);
let targetAbs = targetDeg;
while (targetAbs <= lon0) targetAbs += 360;
let low = 0, high = 366;
for (let i = 0; i < 50; i++) {
let mid = (low + high) / 2;
let T = (startJD + mid - J2000) / JC;
let lon = apparentLongitudeDeg(T);
if (lon < targetAbs) low = mid;
else high = mid;
}
let resJD = startJD + (low + high) / 2;
return new Date((resJD - 2440587.5) * 86400000);
}
return { findTermDate };
})();
// ============== 2. Lunar Calendar ==============
(function() {
'use strict';
const lunarData = [1887,0x1694,0x16aa,0x4ad5,0xab6,0xc4b7,0x4ae,0xa56,0xb52a,0x1d2a,0xd54,0x75aa,0x156a,0x1096d,0x95c,0x14ae,0xaa4d,0x1a4c,0x1b2a,0x8d55,0xad4,0x135a,0x495d,0x95c,0xd49b,0x149a,0x1a4a,0xbaa5,0x16a8,0x1ad4,0x52da,0x12b6,0xe937,0x92e,0x1496,0xb64b,0xd4a,0xda8,0x95b5,0x56c,0x12ae,0x492f,0x92e,0xcc96,0x1a94,0x1d4a,0xada9,0xb5a,0x56c,0x726e,0x125c,0xf92d,0x192a,0x1a94,0xdb4a,0x16aa,0xad4,0x955b,0x4ba,0x125a,0x592b,0x152a,0xf695,0xd94,0x16aa,0xaab5,0x9b4,0x14b6,0x6a57,0xa56,0x1152a,0x1d2a,0xd54,0xd5aa,0x156a,0x96c,0x94ae,0x14ae,0xa4c,0x7d26,0x1b2a,0xeb55,0xad4,0x12da,0xa95d,0x95a,0x149a,0x9a4d,0x1a4a,0x11aa5,0x16a8,0x16d4,0xd2da,0x12b6,0x936,0x9497,0x1496,0x1564b,0xd4a,0xda8,0xd5b4,0x156c,0x12ae,0xa92f,0x92e,0xc96,0x6d4a,0x1d4a,0x10d65,0xb58,0x156c,0xb26d,0x125c,0x192c,0x9a95,0x1a94,0x1b4a,0x4b55,0xad4,0xf55b,0x4ba,0x125a,0xb92b,0x152a,0x1694,0x96aa,0x15aa,0x12ab5,0x974,0x14b6,0xca57,0xa56,0x1526,0x8e95,0xd54,0x15aa,0x49b5,0x96c,0xd4ae,0x149c,0x1a4c,0xbd26,0x1aa6,0xb54,0x6d6a,0x12da,0x1695d,0x95a,0x149a,0xda4b,0x1a4a,0x1aa4,0xbb54,0x16b4,0xada,0x495b,0x936,0xf497,0x1496,0x154a,0xb6a5,0xda4,0x15b4,0x6ab6,0x126e,0x1092f,0x92e,0xc96,0xcd4a,0x1d4a,0xd64,0x956c,0x155c,0x125c,0x792e,0x192c,0xfa95,0x1a94,0x1b4a,0xab55,0xad4,0x14da,0x8a5d,0xa5a,0x1152b,0x152a,0x1694,0xd6aa,0x15aa,0xab4,0x94ba,0x14b6,0xa56,0x7527,0xd26,0xee53,0xd54,0x15aa,0xa9b5,0x96c,0x14ae,0x8a4e,0x1a4c,0x11d26,0x1aa4,0x1b54,0xcd6a,0xada,0x95c,0x949d,0x149a,0x1a2a,0x5b25,0x1aa4,0xfb52,0x16b4,0xaba,0xa95b,0x936,0x1496,0x9a4b,0x154a,0x136a5,0xda4,0x15ac];
const solar11 = [1887,0xec04c,0xec23f,0xec435,0xec649,0xec83e,0xeca51,0xecc46,0xece3a,0xed04d,0xed242,0xed436,0xed64a,0xed83f,0xeda53,0xedc48,0xede3d,0xee050,0xee244,0xee439,0xee64d,0xee842,0xeea36,0xeec4a,0xeee3e,0xef052,0xef246,0xef43a,0xef64e,0xef843,0xefa37,0xefc4b,0xefe41,0xf0054,0xf0248,0xf043c,0xf0650,0xf0845,0xf0a38,0xf0c4d,0xf0e42,0xf1037,0xf124a,0xf143e,0xf1651,0xf1846,0xf1a3a,0xf1c4e,0xf1e44,0xf2038,0xf224b,0xf243f,0xf2653,0xf2848,0xf2a3b,0xf2c4f,0xf2e45,0xf3039,0xf324d,0xf3442,0xf3636,0xf384a,0xf3a3d,0xf3c51,0xf3e46,0xf403b,0xf424e,0xf4443,0xf4638,0xf484c,0xf4a3f,0xf4c52,0xf4e48,0xf503c,0xf524f,0xf5445,0xf5639,0xf584d,0xf5a42,0xf5c35,0xf5e49,0xf603e,0xf6251,0xf6446,0xf663b,0xf684f,0xf6a43,0xf6c37,0xf6e4b,0xf703f,0xf7252,0xf7447,0xf763c,0xf7850,0xf7a45,0xf7c39,0xf7e4d,0xf8042,0xf8254,0xf8449,0xf863d,0xf8851,0xf8a46,0xf8c3b,0xf8e4f,0xf9044,0xf9237,0xf944a,0xf963f,0xf9853,0xf9a47,0xf9c3c,0xf9e50,0xfa045,0xfa238,0xfa44c,0xfa641,0xfa836,0xfaa49,0xfac3d,0xfae52,0xfb047,0xfb23a,0xfb44e,0xfb643,0xfb837,0xfba4a,0xfbc3f,0xfbe53,0xfc048,0xfc23c,0xfc450,0xfc645,0xfc839,0xfca4c,0xfcc41,0xfce36,0xfd04a,0xfd23d,0xfd451,0xfd646,0xfd83a,0xfda4d,0xfdc43,0xfde37,0xfe04b,0xfe23f,0xfe453,0xfe648,0xfe83c,0xfea4f,0xfec44,0xfee38,0xff04c,0xff241,0xff436,0xff64a,0xff83e,0xffa51,0xffc46,0xffe3a,0x10004e,0x100242,0x100437,0x10064b,0x100841,0x100a53,0x100c48,0x100e3c,0x10104f,0x101244,0x101438,0x10164c,0x101842,0x101a35,0x101c49,0x101e3d,0x102051,0x102245,0x10243a,0x10264e,0x102843,0x102a37,0x102c4b,0x102e3f,0x103053,0x103247,0x10343b,0x10364f,0x103845,0x103a38,0x103c4c,0x103e42,0x104036,0x104249,0x10443d,0x104651,0x104846,0x104a3a,0x104c4e,0x104e43,0x105038,0x10524a,0x10543e,0x105652,0x105847,0x105a3b,0x105c4f,0x105e45,0x106039,0x10624c,0x106441,0x106635,0x106849,0x106a3d,0x106c51,0x106e47,0x10703c,0x10724f,0x107444,0x107638,0x10784c,0x107a3f,0x107c53,0x107e48];
const solarTerms = [
{ zh: '小寒', icon: '🧊', en: 'Minor Cold', ru: 'Малый холод', deg: 285,
desc: 'Начало сильных холодов. Температура опускается ниже нуля.',
tradition: 'Время готовить запасы на зиму, есть согревающую пищу' },
{ zh: '大寒', icon: '❄️', en: 'Major Cold', ru: 'Большой холод', deg: 300,
desc: 'Самый холодный период года. Пик зимних морозов.',
tradition: 'Защита от холода, употребление имбирного чая' },
{ zh: '立春', icon: '🌱', en: 'Start of Spring', ru: 'Начало весны', deg: 315,
desc: 'Астрономическое начало весны. Природа начинает пробуждаться.',
tradition: 'Весенние ритуалы, поедание весенних блинчиков' },
{ zh: '雨水', icon: '🌧️', en: 'Rain Water', ru: 'Дождевая вода', deg: 330,
desc: 'Увеличение осадков, таяние снега. Дожди вместо снега.',
tradition: 'Время сеять ранние культуры' },
{ zh: '惊蛰', icon: '🐛', en: 'Awakening of Insects', ru: 'Пробуждение насекомых', deg: 345,
desc: 'Грозы пробуждают насекомых от зимней спячки.',
tradition: 'Время изгнания вредителей, уборка дома' },
{ zh: '春分', icon: '⚖️', en: 'Spring Equinox', ru: 'Весеннее равноденствие', deg: 0, key: true,
desc: 'День равен ночи. Официальное начало весны.',
tradition: 'Почитание предков, запуск воздушных змеев' },
{ zh: '清明', icon: '🪴', en: 'Clear and Bright', ru: 'Чистый и ясный', deg: 15, key: true,
desc: 'Ясная погода, природа обновляется. Важный день поминовения.',
tradition: 'Уборка могил предков, весенние прогулки, запуск змеев' },
{ zh: '谷雨', icon: '🌾', en: 'Grain Rain', ru: 'Зерновой дождь', deg: 30,
desc: 'Дожди способствуют росту зерновых культур.',
tradition: 'Посадка риса, сбор чая' },
{ zh: '立夏', icon: '🌞', en: 'Start of Summer', ru: 'Начало лета', deg: 45,
desc: 'Астрономическое начало лета. Температура растёт.',
tradition: 'Взвешивание детей (на удачу), поедание яиц' },
{ zh: '小满', icon: '🌾', en: 'Grain Buds', ru: 'Зерно в початках', deg: 60,
desc: 'Зерно наполняется, но ещё не созрело.',
tradition: 'Уход за посевами, защита от вредителей' },
{ zh: '芒种', icon: '🌾', en: 'Grain in Ear', ru: 'Зерно в колосьях', deg: 75,
desc: 'Зерновые образуют колосья. Время сбора урожая.',
tradition: 'Посадка поздних культур, сбор пшеницы' },
{ zh: '夏至', icon: '☀️', en: 'Summer Solstice', ru: 'Летнее солнцестояние', deg: 90, key: true,
desc: 'Самый длинный день года. Пик янской энергии.',
tradition: 'Поедание лапши (символ долголетия)' },
{ zh: '小暑', icon: '🔥', en: 'Minor Heat', ru: 'Малый зной', deg: 105,
desc: 'Начало жаркого периода. Становится душно.',
tradition: 'Употребление охлаждающих продуктов, лотоса' },
{ zh: '大暑', icon: '🌡️', en: 'Major Heat', ru: 'Большой зной', deg: 120,
desc: 'Самая жаркая пора года. Пик летнего зноя.',
tradition: 'Защита от жары, употребление травяного чая' },
{ zh: '立秋', icon: '🍂', en: 'Start of Autumn', ru: 'Начало осени', deg: 135,
desc: 'Астрономическое начало осени. Первые признаки увядания.',
tradition: '«Пощипывание осени» — поедание дынь' },
{ zh: '处暑', icon: '🍃', en: 'End of Heat', ru: 'Конец зноя', deg: 150,
desc: 'Жара спадает. Становится прохладнее.',
tradition: 'Время сбора урожая, подготовка к осени' },
{ zh: '白露', icon: '💧', en: 'White Dew', ru: 'Белая роса', deg: 165,
desc: 'Появление росы по утрам. Ночи становятся холодными.',
tradition: 'Сбор винограда, груш, защита от простуды' },
{ zh: '秋分', icon: '⚖️', en: 'Autumn Equinox', ru: 'Осеннее равноденствие', deg: 180, key: true,
desc: 'День равен ночи. Официальное начало осени.',
tradition: 'Праздник урожая, почитание луны' },
{ zh: '寒露', icon: '🥶', en: 'Cold Dew', ru: 'Холодная роса', deg: 195,
desc: 'Роса становится холодной. Приближаются заморозки.',
tradition: 'Уборка позднего урожая, хризантемы' },
{ zh: '霜降', icon: '❄️', en: 'Frost Descent', ru: 'Морозный спуск', deg: 210,
desc: 'Первые заморозки. Появление инея.',
tradition: 'Последний сбор урожая, поедание хурмы' },
{ zh: '立冬', icon: '🌨️', en: 'Start of Winter', ru: 'Начало зимы', deg: 225,
desc: 'Астрономическое начало зимы. Природа засыпает.',
tradition: 'Запасание продуктов на зиму, тонизирующая пища' },
{ zh: '小雪', icon: '🌨️', en: 'Minor Snow', ru: 'Малый снег', deg: 240,
desc: 'Первый снег. Температура опускается ниже нуля.',
tradition: 'Заготовка солений, вяление мяса' },
{ zh: '大雪', icon: '⛄', en: 'Major Snow', ru: 'Большой снег', deg: 255,
desc: 'Обильные снегопады. Зима вступает в силу.',
tradition: 'Защита от холода, употребление согревающей пищи' },
{ zh: '冬至', icon: '🌑', en: 'Winter Solstice', ru: 'Зимнее солнцестояние', deg: 270, key: true,
desc: 'Самый короткий день года. Пик иньской энергии.',
tradition: 'Поедание танъюаней (рисовых шариков), семейный ужин' }
];
const festivals = {
'1-1': {
zh: '春节', icon: '🧧', ru: 'Китайский Новый год', en: 'Chinese New Year', key: true,
desc: 'Главный праздник Китая. Начало нового лунного года.',
tradition: 'Семейный ужин, красные конверты (хунбао), фейерверки'
},
'1-15': {
zh: '元宵节', icon: '🏮', ru: 'Фонари', en: 'Lantern Festival', key: true,
desc: 'Праздник фонарей. Завершение новогодних торжеств.',
tradition: 'Запуск фонарей, загадывание загадок, юаньсяо'
},
'2-2': {
zh: '龙抬头', icon: '🐉', ru: 'Драконья голова', en: 'Dragon Head Raising',
desc: 'Дракон поднимает голову. Начало весны для земледелия.',
tradition: 'Стрижка волос (на удачу), поедание блинов'
},
'5-5': {
zh: '端午节', icon: '🛶', ru: 'Лодка-дракон', en: 'Dragon Boat Festival', key: true,
desc: 'Праздник драконьих лодок. Поминовение поэта Цюй Юаня.',
tradition: 'Гонки на лодках, цзунцзы (рис в бамбуке), защита от злых духов'
},
'7-7': {
zh: '七夕节', icon: '💕', ru: 'Семёрка', en: 'Double Seventh',
desc: 'Китайский день влюблённых. Встреча牛郎 и 织女.',
tradition: 'Просьбы о любви и мастерстве, созерцание звёзд'
},
'7-15': {
zh: '中元节', icon: '🕯️', ru: 'День духов', en: 'Ghost Festival',
desc: 'День поминовения усопших. Врата ада открыты.',
tradition: 'Подношения духам, сжигание бумажных денег'
},
'8-15': {
zh: '中秋节', icon: '🥮', ru: 'Середина осени', en: 'Mid-Autumn Festival', key: true,
desc: 'Праздник середины осени. Полная луна — символ единства.',
tradition: 'Лунные пряники (юэбины), любование луной, семейный ужин'
},
'9-9': {
zh: '重阳节', icon: '🏔️', ru: 'Двойная девятка', en: 'Double Ninth',
desc: 'Праздник долголетия. Подъём на высоту.',
tradition: 'Подъём в горы, хризантемовый чай, почитание старших'
},
'12-8': {
zh: '腊八节', icon: '🥣', ru: 'Лаба', en: 'Laba Festival',
desc: 'Праздник Лаба. Приготовление ритуальной каши.',
tradition: 'Лаба чжоу (каша из 8 ингредиентов), подготовка к Новому году'
},
'12-23': {
zh: '小年', icon: '🧹', ru: 'Малый Новый год', en: 'Little New Year',
desc: 'Малый Новый год. Отправление Бога очага на небеса.',
tradition: 'Генеральная уборка, подношения Богу очага'
}
};
function getBitInt(data, length, shift) {
return (data & (((1 << length) - 1) << shift)) >> shift;
}
function solarToInt(y, m, d) {
m = (m + 9) % 12; y -= Math.floor(m / 10);
return 365 * y + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400) + Math.floor((m*306+5)/10) + (d - 1);
}
function solarFromInt(g) {
let y = Math.floor((10000*g + 14780) / 3652425);
let ddd = g - (365*y + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400));
if (ddd < 0) { y -= 1; ddd = g - (365*y + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400)); }
let mi = Math.floor((100*ddd + 52) / 3060);
let mm = (mi + 2) % 12 + 1;
y += Math.floor((mi + 2) / 12);
let dd = ddd - Math.floor((mi*306 + 5)/10) + 1;
return { year: y, month: mm, day: dd };
}
function lunarToSolar(year, month, day) {
const idx = year - 1887;
if (idx < 0 || idx >= lunarData.length) return null;
const days = lunarData[idx];
const leap = getBitInt(days, 4, 13);
let offset = 0;
let loopend = month <= leap || leap === 0 ? month - 1 : month;
for (let i = 0; i < loopend; i++) offset += getBitInt(days, 1, 12 - i) === 1 ? 30 : 29;
offset += day;
const s11 = solar11[idx];
const y = getBitInt(s11, 12, 9), m = getBitInt(s11, 4, 5), d = getBitInt(s11, 5, 0);
const res = solarFromInt(solarToInt(y, m, d) + offset - 1);
return new Date(Date.UTC(res.year, res.month - 1, res.day));
}
function getFestivals(year) {
let res = [];
for (let y = year - 1; y <= year + 1; y++) {
for (let key in festivals) {
let [m, d] = key.split('-').map(Number);
try {
let sol = lunarToSolar(y, m, d);
if (sol && sol.getUTCFullYear() === year) {
let f = festivals[key];
res.push({
date: sol,
icon: f.icon, zh: f.zh, ru: f.ru, en: f.en, key: f.key,
desc: f.desc, tradition: f.tradition
});
}
} catch(e) {}
}
}
try {
let ny = lunarToSolar(year, 1, 1);
if (ny) {
let eve = new Date(ny.getTime() - 86400000);
if (eve.getUTCFullYear() === year) {
res.push({
date: eve,
icon: '', zh: '除夕', ru: 'Канун Нового года', en: 'New Year\'s Eve', key: true,
desc: 'Последний день старого года. Время семейного воссоединения.',
tradition: 'Праздничный ужин, ожидание Нового года, красные конверты'
});
}
}
} catch(e) {}
return res.sort((a,b) => a.date - b.date);
}
function getSolarTerms(year) {
let res = [];
for (let st of solarTerms) {
try {
let d = Meeus.findTermDate(year, st.deg);
if (d && d.getUTCFullYear() === year) {
res.push({
...st,
date: d,
desc: st.desc,
tradition: st.tradition
});
}
} catch(e) {}
}
return res;
}
function show() {
let year = parseInt(document.getElementById('yearInput').value);
if (!year || year < 1900 || year > 2100) { alert('Допустимый диапазон: 1900–2100'); return; }
const fests = getFestivals(year);
const sterms = getSolarTerms(year);
const all = [...fests, ...sterms].sort((a,b) => a.date - b.date);
let html = '<div class="banner"><h2>📅 Календарь ' + year + '</h2><div class="method">Солнечные термины (Meeus) + Лунные праздники</div></div>';
html += '<div class="section"><table><tr><th>Дата (UTC)</th><th>Событие</th></tr>';
all.forEach(item => {
let isTerm = item.deg !== undefined;
let cls = item.key ? 'key' : (isTerm ? 'sol' : '');
let dateStr = item.date.toLocaleDateString('ru-RU', { timeZone: 'UTC' });
let timeStr = isTerm ? ' <span class="time">' + item.date.toLocaleTimeString('ru-RU', { hour:'2-digit', minute:'2-digit', timeZone:'UTC' }) + '</span>' : '';
let zh = item.zh || '';
let ru = item.ru || '';
let en = item.en || '';
let icon = item.icon || '📅';
let tooltipHtml = `
<div class="tooltip-content">
<div class="tooltip-title">${icon} ${zh} (${ru})</div>
<div class="tooltip-desc">${item.desc || ''}</div>
${item.tradition ? `<div class="tooltip-tradition">🎋 ${item.tradition}</div>` : ''}
</div>
`;
html += `<tr class="${cls}">
<td>${dateStr}${timeStr}</td>
<td>
<div class="tooltip">
<div class="event-cell">
<span class="icon">${icon}</span>
<div>
<span class="zh">${zh}</span> ${ru} <span class="en">(${en})</span>
</div>
</div>
${tooltipHtml}
</div>
</td>
</tr>`;
});
html += '</table></div>';
html += '<div class="info"><strong>💡 Подсказка:</strong> Наведите на событие, чтобы узнать подробности о традициях и значении. <br><strong>Метод:</strong> Алгоритмы Миуса (Astronomical Algorithms, Ch. 25). Точность: ±5 мин. Все расчёты в UTC.</div>';
html += '<div class="total">' + sterms.length + ' солнечных термина • ' + fests.length + ' лунных праздников</div>';
document.getElementById('output').innerHTML = html;
}
window.show = show;
show();
})();
// ============== 3. Theme Toggle Logic ==============
(function() {
const toggleBtn = document.getElementById('themeToggle');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
toggleBtn.textContent = theme === 'dark' ? '☀️' : '🌙';
}
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
applyTheme(savedTheme);
} else {
applyTheme(prefersDark ? 'dark' : 'light');
}
toggleBtn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
})();
</script>