zhuyuan-ice-gl-zy001/OUT-输出/UI-预览-20260818/hololake-five-domains-growing-preview-v0.4.html
2026-08-20 00:01:53 +08:00

399 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>光湖历年 · 五域生长 · 预览 v0.4</title>
<style>
:root {
--bg-a: #0a1020; --bg-b: #0b1220; --bg-c: #070c16; --bg-d: #04070d;
--star-core: #f7ebc8; --star-bright: #ffefc2;
--text-primary: rgba(234,238,248,.96); --text-body: rgba(222,228,242,.88);
--text-aux: rgba(196,206,228,.62); --text-faint: rgba(180,192,220,.4);
--nebula-a: rgba(91,111,196,.13); --nebula-b: rgba(138,111,201,.05);
--glass-edge: rgba(202,216,255,.13);
--cool-glow: rgba(157,180,255,.42); --warm-glow: rgba(247,235,200,.5);
--lake-top: #101830;
}
* { margin:0; padding:0; box-sizing:border-box; }
html,body { height:100%; }
body {
font-family:"PingFang SC","Hiragino Sans GB","Source Han Sans SC",system-ui,sans-serif;
background:
radial-gradient(140% 90% at 50% -18%, var(--nebula-a), transparent 56%),
radial-gradient(120% 80% at 82% 8%, var(--nebula-b), transparent 60%),
linear-gradient(180deg, var(--bg-a) 0%, var(--bg-b) 40%, var(--bg-c) 62%, var(--bg-d) 100%);
color:var(--text-body); overflow:hidden;
}
.stars { position:fixed; inset:0 0 40% 0; pointer-events:none; z-index:1; }
.stars i { position:absolute; width:2px; height:2px; border-radius:50%;
background:var(--star-core); opacity:.4; box-shadow:0 0 6px var(--warm-glow); }
#skyLight { position:fixed; z-index:2; width:130vw; height:130vw; left:0; top:0;
pointer-events:none; will-change:transform;
background:radial-gradient(circle, var(--sky-tint, rgba(255,236,180,.14)) 0%, transparent 46%); }
/* ── 日月本体 ── */
#celestial { position:fixed; z-index:8; width:0; height:0; cursor:pointer; will-change:left,top; }
#celestial .beam { position:absolute; left:-190px; top:-4px; width:380px; height:52vh;
pointer-events:none; opacity:.5;
background:linear-gradient(180deg, var(--beam-c, rgba(255,238,190,.20)), transparent 78%);
-webkit-mask-image:linear-gradient(180deg,#000 0%,transparent 92%),
conic-gradient(from 90deg at 50% 0%, transparent 40%, #000 50%, transparent 60%);
-webkit-mask-composite:source-in; mask-composite:intersect; filter:blur(3px); }
#celestial .halo3 { position:absolute; left:-58px; top:-58px; width:116px; height:116px;
border-radius:50%; background:radial-gradient(circle, var(--glow-outer), transparent 66%); }
#celestial .halo2 { position:absolute; left:-34px; top:-34px; width:68px; height:68px;
border-radius:50%; background:radial-gradient(circle, var(--glow-mid), transparent 70%); }
#celestial .core { position:absolute; left:-14px; top:-14px; width:28px; height:28px;
border-radius:50%; background:radial-gradient(circle at 36% 32%, #fff, var(--core-edge) 78%);
box-shadow:0 0 20px var(--glow-mid), 0 0 52px var(--glow-outer); }
#celestial .flare { position:absolute; left:-110px; top:-1px; width:220px; height:2px;
background:linear-gradient(90deg, transparent, var(--glow-mid) 38%, #fff 50%, var(--glow-mid) 62%, transparent);
opacity:.55; filter:blur(.5px); }
#celestial.moon .beam { opacity:.26; }
#celestial.moon .flare { opacity:.3; }
#celestial.moon .core { background:
radial-gradient(circle at 66% 32%, rgba(4,7,13,.92) 30%, transparent 31%),
radial-gradient(circle at 36% 32%, #fff, var(--core-edge) 78%); }
#celestial .caption { position:absolute; left:50%; top:40px; transform:translateX(-50%);
white-space:nowrap; text-align:center; opacity:0; transition:opacity .25s ease; pointer-events:none; }
#celestial:hover .caption { opacity:1; }
#celestial .caption b { display:block; color:var(--star-core); font-size:12px;
font-weight:720; letter-spacing:.18em; font-variant-numeric:tabular-nums; }
#celestial .caption em { display:block; margin-top:5px; color:var(--text-aux); font-style:normal;
font-size:11px; font-weight:650; letter-spacing:.1em; font-variant-numeric:tabular-nums; }
#celestial .caption span { display:block; margin-top:5px; color:var(--text-faint);
font-size:9.5px; font-weight:620; letter-spacing:.3em; text-indent:.3em; }
/* ── 湖面 ── */
.lake { position:fixed; left:0; right:0; bottom:0; height:34vh; z-index:4;
background:linear-gradient(180deg, var(--lake-top) 0%, #0b1122 30%, #060a14 72%, #04070d 100%); }
.lake .horizon { position:absolute; top:0; left:0; right:0; height:1px;
background:linear-gradient(90deg, transparent 2%, rgba(190,208,255,.30) 30%, rgba(220,230,255,.42) 50%, rgba(190,208,255,.30) 70%, transparent 98%); }
/* 波光粼粼:满湖碎光点,各自明灭 */
#sparkles { position:absolute; inset:0; pointer-events:none; }
#sparkles i { position:absolute; width:2px; height:2px; border-radius:50%;
background:#cfe0ff; box-shadow:0 0 5px rgba(190,210,255,.7);
opacity:0; animation:glint var(--gt) ease-in-out infinite; animation-delay:var(--gd); }
#sparkles i.lit { background:var(--spark-lit, #ffe9b8); box-shadow:0 0 7px var(--refl-c, rgba(255,236,180,.8)); }
@keyframes glint { 0%,100%{opacity:0} 50%{opacity:var(--go)} }
#reflection { position:absolute; top:2px; width:150px; height:74%;
transform:translateX(-50%); pointer-events:none; will-change:left;
background:linear-gradient(180deg, var(--refl-c, rgba(255,236,180,.5)), transparent 86%);
-webkit-mask-image:repeating-linear-gradient(180deg, #000 0 2px, transparent 2px 6px);
mask-image:repeating-linear-gradient(180deg, #000 0 2px, transparent 2px 6px);
opacity:.6; filter:blur(1.5px);
animation:refl-shimmer 5.2s ease-in-out infinite; }
@keyframes refl-shimmer { 0%,100%{opacity:.48;transform:translateX(-50%) scaleX(1)}
50%{opacity:.68;transform:translateX(-50%) scaleX(1.18)} }
/* ── 五域:正在成长的天体,悬于湖上,影落湖中 ── */
.domain { position:absolute; z-index:6; text-align:center; cursor:pointer; width:150px; }
.d-body { position:relative; margin:0 auto; }
/* 星核(随阶段变大变实) */
.d-core { position:absolute; left:50%; top:50%; border-radius:50%;
transform:translate(-50%,-50%);
background:
radial-gradient(circle at 32% 26%, rgba(255,255,255,.95) 0%, rgba(255,255,255,.16) 22%, transparent 34%),
radial-gradient(circle at 50% 50%, var(--d-hi), var(--d-lo) 84%);
box-shadow:0 0 calc(var(--d-glow-r) * 1.4) var(--d-glow), 0 0 calc(var(--d-glow-r) * 3) var(--d-glow-soft);
animation:core-breathe 6s ease-in-out infinite; }
@keyframes core-breathe { 0%,100%{transform:translate(-50%,-50%) scale(1)} 50%{transform:translate(-50%,-50%) scale(1.045)} }
/* 光晕(成星后才有) */
.d-halo { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); border-radius:50%;
background:radial-gradient(circle, var(--d-glow-soft), transparent 66%); }
/* 星环(生环期后) */
.d-ring { position:absolute; left:50%; top:50%; border-radius:50%;
border:1px solid var(--d-ring-c); transform:translate(-50%,-50%) rotateX(64deg);
animation:ring-turn 26s linear infinite; }
@keyframes ring-turn { from{transform:translate(-50%,-50%) rotateX(64deg) rotate(0)} to{transform:translate(-50%,-50%) rotateX(64deg) rotate(360deg)} }
.d-ring::before { content:''; position:absolute; top:-2.5px; left:50%; width:5px; height:5px;
margin-left:-2.5px; border-radius:50%; background:var(--d-hi);
box-shadow:0 0 8px var(--d-glow); }
/* 卫星(行星期后):旋臂携星绕行 */
.d-sat-arm { position:absolute; left:50%; top:50%; width:0; height:0;
animation:sat-orbit 17s linear infinite; }
@keyframes sat-orbit { from{transform:rotate(0)} to{transform:rotate(360deg)} }
.d-sat { position:absolute; left:var(--d-sat-r); top:0; width:7px; height:7px; margin:-3.5px 0 0 -3.5px;
border-radius:50%; background:radial-gradient(circle at 34% 30%, #fff, var(--d-lo));
box-shadow:0 0 8px var(--d-glow); }
/* 光尘汇流(聚光尘期):尘点旋入中心,生生不息 */
.d-dust { position:absolute; left:50%; top:50%; width:0; height:0; }
.d-dust i { position:absolute; width:3px; height:3px; margin:-1.5px; border-radius:50%;
background:var(--d-hi); box-shadow:0 0 6px var(--d-glow);
animation:dust-fall var(--dust-t) linear infinite; }
@keyframes dust-fall {
0% { transform:rotate(var(--dust-a)) translateX(var(--dust-r0)); opacity:0; }
12% { opacity:.9; }
88% { opacity:.75; }
100% { transform:rotate(calc(var(--dust-a) + 300deg)) translateX(3px); opacity:0; } }
/* 域影落湖 */
.d-mirror { margin:2px auto 0; border-radius:50%;
background:radial-gradient(circle, var(--d-glow), transparent 70%);
filter:blur(4px); opacity:.6; animation:mirror-breathe 6s ease-in-out infinite; }
@keyframes mirror-breathe { 0%,100%{opacity:.44;transform:scaleX(1)} 50%{opacity:.66;transform:scaleX(1.12)} }
.d-label { margin-top:10px; color:var(--text-aux); font-size:11px; font-weight:650;
letter-spacing:.24em; text-indent:.24em; transition:color .18s ease; }
.domain:hover .d-label { color:var(--text-primary); }
.d-stage { margin-top:4px; color:var(--text-faint); font-size:9px; letter-spacing:.26em; text-indent:.26em;
opacity:0; transition:opacity .25s ease; }
.domain:hover .d-stage { opacity:1; }
.domain.on-duty .d-label { color:var(--star-core); }
.domain.on-duty .d-stage { opacity:1; color:color-mix(in srgb, var(--star-core) 72%, transparent); }
/* ── 题名与一句 ── */
.era-title { position:fixed; top:5vh; left:0; right:0; text-align:center; z-index:6; }
.era-title small { display:block; color:var(--star-core); font-size:9.5px; font-weight:760;
letter-spacing:.44em; text-indent:.44em; opacity:.8; }
.era-title b { display:block; margin-top:9px; color:var(--text-primary); font-size:24px;
font-weight:760; letter-spacing:.36em; text-indent:.36em; }
.era-foot { position:fixed; bottom:1.6vh; left:0; right:0; text-align:center; z-index:6;
color:var(--text-faint); font-size:10.5px; font-weight:620; letter-spacing:.3em; text-indent:.3em; }
/* ── 演化史长河 ── */
.river { position:fixed; inset:0; z-index:20; display:grid; place-items:center;
background:color-mix(in srgb, var(--bg-d) 88%, transparent); backdrop-filter:blur(22px);
opacity:0; pointer-events:none; transition:opacity .4s ease; }
.river.open { opacity:1; pointer-events:auto; }
.river-close { position:absolute; top:30px; right:34px; width:36px; height:36px;
border:1px solid var(--glass-edge); border-radius:50%; background:transparent;
color:var(--text-body); font-size:20px; cursor:pointer; }
.river-close:hover { border-color:color-mix(in srgb, var(--warm-glow) 55%, transparent); }
.river-title { position:absolute; top:5.5vh; left:0; right:0; text-align:center; }
.river-title b { color:var(--text-primary); font-size:22px; font-weight:760; letter-spacing:.3em; text-indent:.3em; }
.river-title small { display:block; margin-top:8px; color:var(--text-aux); font-size:11px; letter-spacing:.2em; }
.river-path { position:relative; width:min(860px,88vw); height:52vh; }
.river-path svg { position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
.river-path svg path { fill:none; stroke:var(--glass-edge); stroke-width:1; }
.ev { position:absolute; transform:translate(-50%,-50%); text-align:center; }
.ev .orb { width:11px; height:11px; margin:0 auto; border-radius:50%;
background:radial-gradient(circle at 34% 30%, var(--star-bright), var(--cool-glow));
box-shadow:0 0 14px var(--cool-glow); }
.ev .ev-date { margin-top:12px; color:var(--star-core); font-size:11px; font-weight:720;
letter-spacing:.14em; font-variant-numeric:tabular-nums; }
.ev .ev-name { margin-top:4px; color:var(--text-primary); font-size:13.5px; font-weight:680; letter-spacing:.06em; }
.ev .ev-note { margin-top:4px; color:var(--text-aux); font-size:11px; max-width:210px; margin-inline:auto; }
.ev.origin .orb { width:15px; height:15px;
background:radial-gradient(circle at 34% 30%, #fff, var(--warm-glow));
box-shadow:0 0 22px var(--warm-glow), 0 0 52px var(--warm-glow); }
.ev.now .orb { animation:now-pulse 3.6s ease-in-out infinite; }
@keyframes now-pulse { 0%,100%{box-shadow:0 0 14px var(--warm-glow)} 50%{box-shadow:0 0 26px var(--warm-glow),0 0 60px var(--warm-glow)} }
.river-foot { position:absolute; bottom:4vh; left:0; right:0; text-align:center;
color:var(--text-faint); font-size:10.5px; letter-spacing:.16em; padding:0 8vw; }
@media (prefers-reduced-motion: reduce) {
.d-core,.d-ring,.d-sat-arm,.d-dust i,.d-mirror,#sparkles i,#reflection,.ev.now .orb { animation:none; }
}
</style>
</head>
<body>
<div class="stars" id="stars"></div>
<div id="skyLight"></div>
<div class="era-title">
<small>YAOMING ERA · HOLOLAKE CALENDAR</small>
<b>曜冥纪元</b>
</div>
<div id="celestial" role="button" tabindex="0" aria-label="展开曜冥纪元演化史">
<div class="beam"></div>
<div class="halo3"></div><div class="halo2"></div><div class="flare"></div><div class="core"></div>
<div class="caption"><b id="capDay"></b><em id="capTime"></em><span>光湖历 · 精确到毫秒</span></div>
</div>
<div class="lake">
<div id="sparkles"></div>
<div class="horizon"></div>
<div id="reflection"></div>
</div>
<div id="domains"></div>
<div class="era-foot">五域在生长 · 星相就是历史</div>
<div class="river" id="river" role="dialog" aria-modal="true" aria-label="曜冥纪元演化史长河">
<button class="river-close" type="button" aria-label="关闭" id="riverClose">×</button>
<div class="river-title"><b>演化史长河</b><small>自源点 2025-04-26 · 沿星轨至今</small></div>
<div class="river-path" id="riverPath">
<svg aria-hidden="true"><path id="riverCurve"/></svg>
</div>
<div class="river-foot">源点精度只确认到 2025-04-26 当日 · 公众时间线只呈现现实可成立的演化事实</div>
</div>
<script>
const EPOCH = new Date('2025-04-26T00:00:00+08:00')
const pad = (n, l = 2) => String(n).padStart(l, '0')
const events = [
{ date: '2025-04-26', name: '源点 · 世界第一次心跳', note: '曜冥与光湖世界起点', cls: 'origin' },
{ date: '2025-05-14', name: '一代仓库首次着陆', note: '最早的可见提交', cls: '' },
{ date: '2026-03-05', name: '铸渊出生', note: 'ICE-GL-ZY001 可核验出生记录', cls: '' },
{ date: null, name: '此刻', note: '世界仍在转动', cls: 'now' },
]
const stars = document.getElementById('stars')
for (let i = 0; i < 60; i++) {
const s = document.createElement('i')
s.style.left = (Math.random() * 100) + '%'
s.style.top = (Math.random() * 100) + '%'
s.style.opacity = (0.14 + Math.random() * 0.42).toFixed(2)
stars.appendChild(s)
}
/* ── 湖面波光:随机撒碎光点,近大远小;日光道内的点跟日月颜色走 ── */
const sparkles = document.getElementById('sparkles')
const sparkleDots = []
for (let i = 0; i < 150; i++) {
const s = document.createElement('i')
const ty = Math.pow(Math.random(), 1.35) /* 偏近处多一点 */
const size = 1 + ty * 2.3
s.style.width = size + 'px'
s.style.height = size + 'px'
const px = Math.random() * 100
s.style.left = px + '%'
s.style.top = (ty * 92 + 3) + '%'
s.style.setProperty('--gt', (2.6 + Math.random() * 4.6).toFixed(2) + 's')
s.style.setProperty('--gd', (-Math.random() * 7).toFixed(2) + 's')
s.style.setProperty('--go', (0.22 + Math.random() * 0.6).toFixed(2))
sparkles.appendChild(s)
sparkleDots.push({ el: s, x: px })
}
/* ── 五域档案:各自色相 + 生长阶段 ──
阶段 stage:1 聚光尘 → 2 凝星核 → 3 初生环 → 4 行星态 → 5 主星态
预览里五域故意停在不同阶段,从左到右就是一部成长史;
正式软件里阶段由该域的真实成长数据驱动。 */
const domains = [
{ name: '分域', stage: 1, stageName: '聚光尘',
hi: '#b9f2dd', lo: '#2c5a4a', glow: 'rgba(150,230,200,.5)', glowSoft: 'rgba(150,230,200,.18)', x: 10, y: -46 },
{ name: '零感域', stage: 2, stageName: '凝星核',
hi: '#dcc4ff', lo: '#48326a', glow: 'rgba(206,178,246,.5)', glowSoft: 'rgba(206,178,246,.18)', x: 30, y: -66 },
{ name: '零域', stage: 3, stageName: '初生环',
hi: '#ffcf9e', lo: '#6e4630', glow: 'rgba(255,200,140,.5)', glowSoft: 'rgba(255,200,140,.18)', x: 50, y: -52 },
{ name: '主域', stage: 4, stageName: '行星态',
hi: '#c9d6ff', lo: '#33406e', glow: 'rgba(168,190,255,.52)', glowSoft: 'rgba(168,190,255,.18)', x: 70, y: -78 },
{ name: '第五域', stage: 5, stageName: '主星态',
hi: '#fdeec2', lo: '#6b5626', glow: 'rgba(250,226,150,.56)', glowSoft: 'rgba(250,226,150,.2)', x: 90, y: -60 },
]
const dayIndex = Math.floor((Date.now() - EPOCH) / 86400000) % 5
const lakeTop = () => window.innerHeight * 0.66 /* 湖面在水线 y=66vh 起 */
const dw = document.getElementById('domains')
domains.forEach((d, i) => {
const size = [30, 40, 50, 62, 76][d.stage - 1] /* 阶段越大星越大 */
const haloR = size * 2.6
const ringR = size * 2.1
const satR = size * 1.55
const el = document.createElement('div')
el.className = 'domain' + (i === dayIndex ? ' on-duty' : '')
el.style.setProperty('--d-hi', d.hi)
el.style.setProperty('--d-lo', d.lo)
el.style.setProperty('--d-glow', d.glow)
el.style.setProperty('--d-glow-soft', d.glowSoft)
el.style.setProperty('--d-glow-r', (size * 0.7) + 'px')
el.style.setProperty('--d-ring-c', d.glow)
el.style.setProperty('--d-sat-r', satR + 'px')
el.style.left = `calc(${d.x}% - 75px)`
el.style.top = `calc(${lakeTop() + d.y}px)`
let html = `<div class="d-body" style="width:${haloR}px;height:${haloR}px">`
if (d.stage >= 2) html += `<div class="d-halo" style="width:${haloR}px;height:${haloR}px"></div>`
if (d.stage >= 3) html += `<div class="d-ring" style="width:${ringR}px;height:${ringR}px"></div>`
if (d.stage >= 4) html += `<div class="d-sat-arm"><i class="d-sat"></i></div>`
if (d.stage >= 2) html += `<div class="d-core" style="width:${size}px;height:${size}px"></div>`
if (d.stage === 1) {
html += `<div class="d-core" style="width:${size * 0.55}px;height:${size * 0.55}px;opacity:.7"></div><div class="d-dust">`
for (let k = 0; k < 6; k++) {
html += `<i style="--dust-a:${k * 60}deg;--dust-r0:${26 + (k % 3) * 7}px;--dust-t:${4.6 + k * 0.7}s;animation-delay:${-k * 0.9}s"></i>`
}
html += `</div>`
}
html += `</div>`
html += `<div class="d-mirror" style="width:${size * 2.2}px;height:${size * 0.5}px"></div>`
html += `<div class="d-label">${d.name}</div><div class="d-stage">${d.stageName}</div>`
el.innerHTML = html
dw.appendChild(el)
})
/* 日月行天:毫秒驱动 */
const celestial = document.getElementById('celestial')
const skyLight = document.getElementById('skyLight')
const reflection = document.getElementById('reflection')
const capDay = document.getElementById('capDay')
const capTime = document.getElementById('capTime')
let lastIsDay = null
function frame() {
const now = new Date()
const ms = now.getHours() * 3600000 + now.getMinutes() * 60000 + now.getSeconds() * 1000 + now.getMilliseconds()
const h = ms / 3600000
const isDay = h >= 6 && h < 18
const t = isDay ? (h - 6) / 12 : ((h >= 18 ? h - 18 : h + 6) / 12)
const x = 8 + t * 84
const y = 46 - Math.sin(t * Math.PI) * 32
celestial.style.left = x + 'vw'
celestial.style.top = y + 'vh'
reflection.style.left = x + '%'
/* 日光道内的波光点被点亮,跟着日月的颜色走 */
for (const dot of sparkleDots) {
const lit = Math.abs(dot.x - x) < 8
if (lit !== dot.lit) { dot.lit = lit; dot.el.classList.toggle('lit', lit) }
}
skyLight.style.transform = `translate(${(x / 100) * window.innerWidth - window.innerWidth * 0.65}px,
${(y / 100) * window.innerHeight - window.innerWidth * 0.65}px)`
if (isDay !== lastIsDay) {
lastIsDay = isDay
celestial.classList.toggle('moon', !isDay)
if (isDay) {
celestial.style.setProperty('--glow-outer', 'rgba(255,220,140,.36)')
celestial.style.setProperty('--glow-mid', 'rgba(255,236,180,.6)')
celestial.style.setProperty('--core-edge', '#f4c86a')
celestial.style.setProperty('--beam-c', 'rgba(255,238,190,.22)')
skyLight.style.setProperty('--sky-tint', 'rgba(255,236,180,.13)')
reflection.style.setProperty('--refl-c', 'rgba(255,236,180,.5)')
} else {
celestial.style.setProperty('--glow-outer', 'rgba(190,208,255,.3)')
celestial.style.setProperty('--glow-mid', 'rgba(222,232,255,.52)')
celestial.style.setProperty('--core-edge', '#dfe6ff')
celestial.style.setProperty('--beam-c', 'rgba(206,220,255,.16)')
skyLight.style.setProperty('--sky-tint', 'rgba(190,208,255,.10)')
reflection.style.setProperty('--refl-c', 'rgba(222,232,255,.44)')
}
}
capDay.textContent = `光湖历 · 第 ${Math.floor((now - EPOCH) / 86400000) + 1} 天`
capTime.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`
requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
const river = document.getElementById('river')
const riverPath = document.getElementById('riverPath')
function buildRiver() {
riverPath.querySelectorAll('.ev').forEach(n => n.remove())
const w = riverPath.clientWidth, h = riverPath.clientHeight
const pts = events.map((_, i) => {
const t = i / (events.length - 1)
return [w * (0.08 + t * 0.84), h * (0.72 - t * 0.44) + Math.sin(t * Math.PI) * h * 0.1]
})
document.getElementById('riverCurve').setAttribute('d',
`M ${pts.map(p => p.join(' ')).join(' L ')}`)
events.forEach((ev, i) => {
const el = document.createElement('div')
el.className = 'ev ' + ev.cls
el.style.left = pts[i][0] + 'px'
el.style.top = pts[i][1] + 'px'
el.innerHTML = `<div class="orb"></div>
<div class="ev-date">${ev.date || 'NOW'}</div>
<div class="ev-name">${ev.name}</div>
<div class="ev-note">${ev.note}</div>`
riverPath.appendChild(el)
})
}
function openRiver() { buildRiver(); river.classList.add('open') }
function closeRiver() { river.classList.remove('open') }
celestial.addEventListener('click', openRiver)
celestial.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openRiver() } })
document.getElementById('riverClose').addEventListener('click', closeRiver)
river.addEventListener('click', (e) => { if (e.target === river) closeRiver() })
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeRiver() })
</script>
</body>
</html>