release: bump version to 0.29.2

This commit is contained in:
linkong
2026-04-21 10:43:48 +08:00
parent e6d0332fba
commit 2b0d4cfc49
13 changed files with 816 additions and 487 deletions

View File

@@ -1 +1 @@
0.29.1
0.29.2

View File

@@ -10,6 +10,25 @@ This project follows the repository versioning rule:
## [0.29.1] — 2026-04-20
## [0.29.2] — 2026-04-21
### ✨ Highlights
- Earth 继续收口 HUD 交互与设置面板表现,设置弹窗改成更接近从按钮展开的窗口感,同时加入系统级 admin 入口
- 修正天球太阳方向与地球受光解耦后的日照逻辑,地表昼夜判断改为按太阳直射点经纬度落到地球贴图坐标
### 🔧 Improvements
- toolbar 进一步收成更贴近 hub 的浅弓形排列,并统一成与 HUD panel 一致的液态玻璃配色与透明度
- 设置弹窗与各 HUD panel 继续统一样式、等比缩放和头部基线,设置列表补充系统分组与 admin 跳转
- 所有 HUD panel 增加更统一的液态玻璃高光与 hover / press 反馈
### 🐛 Fixes
- 修复设置弹窗仍像旧圆角矩形、标题文案重复和从底边直直飞出的动画问题
- 修复天球与太阳方向混用显示校准导致中国白天仍落在夜面的日照错误
---
## [0.29.1] — 2026-04-20
### ✨ Highlights
- Earth 加载状态条改成单一队列式通知面板,加载阶段不再因为步骤文案变化而回缩,也不会被其他通知打断
- 调整 brand panel 的呈现方式与昼夜/选中态可读性,让品牌区更自然、交互高亮在白天和黑夜里都更稳定

View File

@@ -16,12 +16,13 @@
## Current Version
- `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.29.1`
- `dev` 当前开发分支历史推导到:`0.29.2`
## Timeline
| Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- |
| `0.29.2` | bugfix | `dev` | `pending` | 修正 Earth 设置弹窗展开表现与系统入口,继续统一液态玻璃 HUD并校正太阳受光方向 |
| `0.29.1` | bugfix | `dev` | `pending` | Earth 加载通知条改为队列式单面板显示brand panel 去框并收敛昼夜与选中态可读性 |
| `0.29.0` | feature | `dev` | `pending` | Earth 新增天球背景与太阳/月亮位置层,强化昼夜分隔并收口卫星图例与图层面板交互 |
| `0.28.2` | bugfix | `dev` | `pending` | 修正媒体情报 tab 尺寸记忆与切换锚点逻辑,并清理 docs 根目录遗留旧路径文档 |

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.29.1",
"version": "0.29.2",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -1,11 +1,16 @@
/* hud.css - HUD surfaces and shared overlays */
.hud-panel {
--panel-glow-x: 18%;
--panel-glow-y: 0%;
--panel-glow-opacity: 0.1;
--panel-tilt-x: 0deg;
--panel-tilt-y: 0deg;
position: absolute;
overflow: hidden;
isolation: isolate;
background:
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.08), transparent 30%),
radial-gradient(circle at var(--panel-glow-x) var(--panel-glow-y), rgba(255, 255, 255, calc(0.08 + var(--panel-glow-opacity))), transparent 30%),
radial-gradient(circle at 86% 115%, rgba(145, 186, 255, 0.08), transparent 36%),
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 26%),
linear-gradient(180deg, var(--hud-surface-top), var(--hud-surface-bottom));
@@ -14,9 +19,14 @@
inset 0 1px 0 var(--hud-highlight),
inset 0 -1px 0 rgba(255, 255, 255, 0.03),
var(--hud-shadow),
0 0 0 1px rgba(255, 255, 255, 0.02);
0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 20px rgba(123, 176, 236, calc(0.04 + var(--panel-glow-opacity) * 0.32));
backdrop-filter: blur(18px) saturate(125%);
-webkit-backdrop-filter: blur(18px) saturate(125%);
transition:
background 0.22s ease,
border-color 0.22s ease,
box-shadow 0.22s ease;
}
.hud-panel::before {
@@ -28,6 +38,11 @@
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.02) 58%, transparent 100%);
opacity: 0.52;
pointer-events: none;
transform:
perspective(240px)
rotateX(calc(var(--panel-tilt-x) * 0.36))
rotateY(calc(var(--panel-tilt-y) * 0.36));
transition: opacity 0.18s ease, transform 0.18s ease;
}
.hud-panel::after {
@@ -40,7 +55,11 @@
linear-gradient(135deg, rgba(244, 249, 255, 0.22), rgba(164, 194, 226, 0.08) 36%, rgba(90, 123, 161, 0.04) 70%, rgba(255, 255, 255, 0.16));
opacity: 0.72;
pointer-events: none;
filter: blur(0.2px);
filter: url(#liquid-glass-distortion) blur(0.22px);
transform:
perspective(240px)
rotateX(calc(var(--panel-tilt-x) * 0.24))
rotateY(calc(var(--panel-tilt-y) * 0.24));
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
@@ -49,6 +68,36 @@
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.hud-panel:hover:not(.is-dragging) {
--panel-glow-opacity: 0.16;
border-color: var(--hud-border-hover);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 -1px 0 rgba(255, 255, 255, 0.04),
0 20px 48px rgba(1, 7, 16, 0.34),
0 0 0 1px rgba(255, 255, 255, 0.03),
0 0 28px rgba(123, 176, 236, 0.1);
}
.hud-panel:hover:not(.is-dragging)::before {
opacity: 0.6;
}
.hud-panel:hover:not(.is-dragging)::after {
opacity: 0.84;
}
.hud-panel.is-pressed:not(.is-dragging) {
--panel-glow-opacity: 0.13;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
inset 0 -1px 0 rgba(255, 255, 255, 0.04),
0 14px 34px rgba(1, 7, 16, 0.28),
0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 22px rgba(123, 176, 236, 0.08);
}
.hud-panel > * {
@@ -406,11 +455,21 @@
position: fixed;
inset: 0;
z-index: 260;
display: none;
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 0.24s ease, visibility 0.24s ease;
}
.earth-settings-modal.is-opening,
.earth-settings-modal.is-open,
.earth-settings-modal.is-closing {
visibility: visible;
}
.earth-settings-modal.is-open {
display: block;
opacity: 1;
pointer-events: auto;
}
.earth-settings-backdrop {
@@ -419,46 +478,47 @@
background: rgba(2, 8, 20, 0.46);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
opacity: 0;
transition: opacity 0.26s ease;
}
.earth-settings-modal.is-open .earth-settings-backdrop {
opacity: 1;
}
.earth-settings-sheet {
--settings-scale: clamp(0.72, calc(var(--hud-scale) * 0.96), 1);
position: fixed;
top: max(32px, 9vh);
right: 16px;
left: 16px;
width: min(560px, calc(100vw - 32px));
max-width: 560px;
max-height: calc(100vh - max(64px, 18vh));
top: max(calc(32px * var(--settings-scale)), 9vh);
right: calc(16px * var(--settings-scale));
left: calc(16px * var(--settings-scale));
width: min(calc(560px * var(--settings-scale)), calc(100vw - (32px * var(--settings-scale))));
max-width: calc(560px * var(--settings-scale));
max-height: calc(100vh - max(calc(64px * var(--settings-scale)), 18vh));
margin-inline: auto;
transform: none;
border-radius: calc(24px * var(--hud-scale));
padding: calc(20px * var(--hud-scale));
border-radius: 0;
padding: calc(var(--hud-panel-padding) * var(--settings-scale));
display: flex;
flex-direction: column;
gap: var(--hud-gap-md);
gap: calc(var(--hud-gap-md) * var(--settings-scale));
overflow: hidden;
transform: translateZ(0);
opacity: 1;
filter: none;
border-radius: 0;
will-change: transform, opacity, filter, border-radius;
}
.earth-settings-sheet.liquid-glass-surface {
animation: none;
background:
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.09), transparent 30%),
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 28%),
linear-gradient(180deg, rgba(19, 34, 56, 0.92), rgba(8, 18, 31, 0.9));
border-color: rgba(207, 224, 243, 0.12);
.earth-settings-sheet.hud-panel {
--panel-glow-x: 18%;
--panel-glow-y: 0%;
--panel-glow-opacity: 0.1;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 24px 56px rgba(2, 7, 15, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.025);
}
.earth-settings-sheet.liquid-glass-surface:hover,
.earth-settings-sheet.liquid-glass-surface:active,
.earth-settings-sheet.liquid-glass-surface.is-pressed {
--btn-scale: 1;
--press-offset: 0px;
--glow-opacity: 0.24;
transform: none;
inset 0 1px 0 var(--hud-highlight),
inset 0 -1px 0 rgba(255, 255, 255, 0.03),
var(--hud-shadow),
0 0 0 1px rgba(255, 255, 255, 0.02);
}
.earth-settings-header,
@@ -468,12 +528,10 @@
}
.earth-settings-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
--hud-header-padding: 0 0 var(--hud-gap-sm);
--hud-header-gap: var(--hud-gap-md);
align-items: center;
gap: var(--hud-gap-md);
padding-bottom: var(--hud-gap-sm);
border-bottom: 1px solid var(--hud-line);
}
.earth-settings-kicker {
@@ -483,25 +541,9 @@
text-transform: uppercase;
}
.earth-settings-title {
margin: 4px 0 0;
color: var(--hud-title);
font-size: var(--hud-panel-header-title-size);
font-weight: 600;
line-height: 1.2;
}
.earth-settings-close {
margin-top: 4px;
align-self: auto;
width: auto;
height: auto;
min-width: 0;
padding: calc(7px * var(--hud-scale));
}
.earth-settings-close .material-symbols-rounded {
font-size: calc(16px * var(--hud-scale));
margin-top: 0;
flex-shrink: 0;
}
.earth-settings-content {
@@ -566,6 +608,10 @@
transform: translateY(-1px);
}
.earth-settings-link {
text-decoration: none;
}
.earth-settings-copy {
display: flex;
flex-direction: column;
@@ -584,6 +630,22 @@
line-height: 1.4;
}
.earth-settings-link-meta {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--hud-text-soft);
flex-shrink: 0;
}
.earth-settings-link-meta .material-symbols-rounded:first-child {
font-size: 1.1rem;
}
.earth-settings-link-meta .material-symbols-rounded:last-child {
font-size: 1rem;
}
.earth-settings-switch {
position: relative;
display: inline-flex;

View File

@@ -1,4 +1,4 @@
/* toolbar.css - bottom dock and floating toolbar primitives */
/* toolbar.css - orbital hub toolbar */
.earth-toolbar-group {
position: absolute;
@@ -6,7 +6,6 @@
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
z-index: 200;
@@ -24,119 +23,153 @@
}
.earth-toolbar {
--toolbar-scale: 1;
--toolbar-orb-size: calc(46px * var(--toolbar-scale));
--toolbar-hub-size: calc(58px * var(--toolbar-scale));
--toolbar-arc-width: calc(420px * var(--toolbar-scale));
--toolbar-arc-height: calc(160px * var(--toolbar-scale));
--toolbar-inner-arc-width: calc(260px * var(--toolbar-scale));
--toolbar-inner-arc-height: calc(56px * var(--toolbar-scale));
position: relative;
width: min(620px, calc(100vw - 40px));
height: calc(200px * var(--toolbar-scale));
display: flex;
align-items: center;
justify-content: center;
gap: 0;
background: transparent;
border: none;
box-shadow: none;
padding: 0;
}
.earth-toolbar-items {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: nowrap;
}
.earth-toolbar-popover {
.earth-toolbar-cluster {
position: relative;
}
.earth-toolbar-popover::before {
content: '';
position: absolute;
left: 50%;
bottom: 100%;
transform: translateX(-50%);
width: 56px;
height: 16px;
background: transparent;
}
.earth-toolbar-popover > .earth-stack-toolbar {
position: absolute;
left: 50%;
top: auto;
right: auto;
bottom: calc(100% + 12px);
transform: translate(-50%, 10px);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
opacity: 0;
visibility: hidden;
width: 100%;
height: 100%;
pointer-events: none;
transition:
opacity 0.22s ease,
transform 0.22s ease,
visibility 0.22s ease;
z-index: 220;
}
.earth-toolbar-btn {
.earth-toolbar-cluster::before {
content: "";
position: absolute;
left: 50%;
bottom: calc(14px * var(--toolbar-scale));
width: var(--toolbar-arc-width);
height: var(--toolbar-arc-height);
transform: translateX(-50%);
border-radius: 50%;
border: 1px solid rgba(145, 186, 255, 0.08);
border-bottom-color: transparent;
background:
radial-gradient(circle at 50% 100%, rgba(145, 186, 255, 0.05), transparent 58%);
opacity: 0.9;
mask: linear-gradient(180deg, rgba(0, 0, 0, 0.82), transparent 86%);
pointer-events: none;
}
.earth-toolbar-orb {
position: absolute;
left: 50%;
bottom: calc(30px * var(--toolbar-scale));
transform: translate(-50%, -50%);
}
.earth-toolbar-hub {
position: absolute;
left: 50%;
bottom: calc(8px * var(--toolbar-scale));
transform: translateX(-50%);
}
.earth-toolbar-orb {
pointer-events: none;
opacity: 1;
transition:
transform 0.36s cubic-bezier(0.34, 1.15, 0.64, 1),
opacity 0.24s ease;
}
.earth-toolbar-cluster.is-expanded .earth-toolbar-orb {
transform: translate(calc(-50% + var(--orb-x)), calc(-50% + var(--orb-y)));
}
.earth-toolbar-cluster.is-collapsed .earth-toolbar-orb {
transform: translate(-50%, -50%) scale(0.42);
opacity: 0;
}
.earth-toolbar-orb > *,
.earth-toolbar-hub > * {
pointer-events: auto;
}
.earth-toolbar-orb > .liquid-glass-surface {
animation: floatDock 4.6s ease-in-out infinite;
animation-delay: var(--orb-delay, 0s);
}
.earth-toolbar-cluster.is-dock-engaged .earth-toolbar-orb > .liquid-glass-surface {
animation-play-state: paused;
}
.earth-toolbar-btn,
.earth-toolbar-hub-btn {
position: relative;
width: 28px;
height: 28px;
border: none;
border-radius: 0;
background: transparent;
color: #4db8ff;
color: var(--hud-text-soft);
font-size: 14px;
cursor: pointer;
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 0;
margin: 0;
overflow: visible;
appearance: none;
-webkit-appearance: none;
}
.earth-toolbar-btn.floating-btn {
width: 42px;
height: 42px;
min-width: 42px;
min-height: 42px;
width: var(--toolbar-orb-size);
height: var(--toolbar-orb-size);
min-width: var(--toolbar-orb-size);
min-height: var(--toolbar-orb-size);
border-radius: 50%;
overflow: hidden;
}
.earth-toolbar-btn:not(.liquid-glass-surface)::after {
content: none;
.earth-toolbar-hub-btn {
width: var(--toolbar-hub-size);
height: var(--toolbar-hub-size);
min-width: var(--toolbar-hub-size);
min-height: var(--toolbar-hub-size);
border-radius: 50%;
overflow: hidden;
color: var(--hud-title);
}
.earth-toolbar-btn .icon,
.earth-toolbar-hub-btn .material-symbols-rounded {
position: relative;
z-index: 1;
line-height: 1;
}
.earth-toolbar-btn .icon {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
transform: translateZ(0);
transition: transform 0.16s ease, opacity 0.16s ease;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
line-height: 1;
}
.earth-toolbar-btn svg {
width: 20px;
height: 20px;
stroke: currentColor;
stroke-width: 2.1;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.earth-toolbar-btn .material-symbols-rounded {
font-size: 21px;
.earth-toolbar-btn .material-symbols-rounded,
.earth-toolbar-hub-btn .material-symbols-rounded {
font-size: calc(21px * var(--toolbar-scale));
line-height: 1;
font-variation-settings:
'FILL' 0,
@@ -154,28 +187,6 @@
-moz-osx-font-smoothing: grayscale;
}
.earth-toolbar-btn img {
width: 20px;
height: 20px;
display: block;
user-select: none;
pointer-events: none;
shape-rendering: geometricPrecision;
image-rendering: -webkit-optimize-contrast;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.earth-toolbar-items > :nth-child(2n).floating-btn,
.earth-toolbar-items > :nth-child(2n) .floating-btn {
animation-delay: 0.18s;
}
.earth-toolbar-items > :nth-child(3n).floating-btn,
.earth-toolbar-items > :nth-child(3n) .floating-btn {
animation-delay: 0.34s;
}
.liquid-glass-surface {
--elastic-x: 0px;
--elastic-y: 0px;
@@ -190,12 +201,14 @@
position: relative;
isolation: isolate;
transform-style: preserve-3d;
transform-origin: center center;
will-change: transform, box-shadow;
overflow: hidden;
background:
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.16), transparent 34%),
radial-gradient(circle at 50% 118%, rgba(255, 255, 255, 0.08), transparent 30%),
linear-gradient(180deg, var(--glass-fill-top), var(--glass-fill-bottom)),
rgba(8, 20, 38, 0.22);
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.12), transparent 34%),
radial-gradient(circle at 50% 118%, rgba(255, 255, 255, 0.05), transparent 30%),
linear-gradient(180deg, var(--hud-surface-top), var(--hud-surface-bottom)),
rgba(8, 20, 38, 0.12);
border: 1px solid var(--hud-border);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
@@ -205,7 +218,11 @@
backdrop-filter: blur(18px) saturate(145%);
-webkit-backdrop-filter: blur(18px) saturate(145%);
transform:
translate3d(var(--elastic-x), calc(var(--float-offset) + var(--press-offset) + var(--elastic-y)), 0)
translate3d(
var(--elastic-x),
calc(var(--float-offset) + var(--press-offset) + var(--elastic-y)),
0
)
scale(var(--btn-scale));
transition:
transform 0.22s ease,
@@ -213,17 +230,16 @@
background 0.22s ease,
opacity 0.18s ease,
border-color 0.22s ease;
animation: floatDock 3.8s ease-in-out infinite;
}
.liquid-glass-surface::before {
content: '';
content: "";
position: absolute;
inset: 1px 1px 18px 1px;
border-radius: inherit;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.05) 28%, transparent 68%);
opacity: 0.5;
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04) 28%, transparent 68%);
opacity: 0.42;
pointer-events: none;
transform:
perspective(120px)
@@ -234,14 +250,14 @@
}
.liquid-glass-surface::after {
content: '';
content: "";
position: absolute;
inset: -1px;
padding: 1.35px;
border-radius: inherit;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.36), rgba(168, 222, 255, 0.22) 34%, rgba(96, 175, 255, 0.16) 66%, rgba(255, 255, 255, 0.28));
opacity: 0.82;
opacity: 0.72;
pointer-events: none;
filter: url(#liquid-glass-distortion) blur(0.35px);
transform:
@@ -260,15 +276,28 @@
transition: opacity 0.18s ease, transform 0.18s ease;
}
.earth-toolbar-hub-btn.liquid-glass-surface {
background:
radial-gradient(circle at 50% 24%, rgba(255, 255, 255, 0.16), transparent 34%),
linear-gradient(180deg, rgba(28, 54, 90, 0.26), rgba(11, 24, 43, 0.22)),
rgba(10, 28, 52, 0.16);
border-color: var(--hud-border);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
inset 0 -1px 0 rgba(255, 255, 255, 0.05),
0 16px 30px rgba(0, 0, 0, 0.24),
0 0 30px rgba(104, 181, 247, 0.18);
}
.liquid-glass-surface:hover {
--btn-scale: 1.035;
--btn-scale: 1.04;
--press-offset: -1px;
--glow-opacity: 0.32;
background:
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.18), transparent 34%),
radial-gradient(circle at 50% 118%, rgba(255, 255, 255, 0.1), transparent 30%),
linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(128, 198, 255, 0.1)),
rgba(8, 20, 38, 0.2);
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.14), transparent 34%),
radial-gradient(circle at 50% 118%, rgba(255, 255, 255, 0.08), transparent 30%),
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(128, 198, 255, 0.06)),
rgba(8, 20, 38, 0.14);
border-color: var(--hud-border-hover);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.2),
@@ -289,52 +318,16 @@
.liquid-glass-surface:active,
.liquid-glass-surface.is-pressed {
--btn-scale: 0.942;
--btn-scale: 0.95;
--press-offset: 2px;
--glow-opacity: 0.2;
background:
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.24), transparent 34%),
radial-gradient(circle at 50% 118%, rgba(255, 255, 255, 0.14), transparent 30%),
linear-gradient(180deg, rgba(255, 255, 255, 0.24), rgba(146, 210, 255, 0.16)),
rgba(10, 24, 44, 0.24);
border-color: rgba(240, 249, 255, 0.58);
box-shadow:
inset 0 2px 10px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.16),
0 4px 10px rgba(0, 0, 0, 0.18),
0 0 14px rgba(176, 226, 255, 0.18);
}
.liquid-glass-surface:active::before,
.liquid-glass-surface.is-pressed::before {
opacity: 0.46;
transform: translateY(2px) scale(0.985);
}
.liquid-glass-surface:active::after,
.liquid-glass-surface.is-pressed::after {
opacity: 0.78;
transform: scale(0.985);
}
.liquid-glass-surface:active .icon,
.liquid-glass-surface.is-pressed .icon {
transform: translateY(1.5px);
}
.liquid-glass-surface:active img,
.liquid-glass-surface.is-pressed img,
.liquid-glass-surface:active .material-symbols-rounded,
.liquid-glass-surface.is-pressed .material-symbols-rounded {
transform: translateY(1.5px);
transition: transform 0.16s ease, opacity 0.16s ease;
}
.liquid-glass-surface.active {
background:
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.18), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(118, 200, 255, 0.14)),
rgba(11, 34, 58, 0.26);
radial-gradient(circle at var(--glow-x) var(--glow-y), rgba(255, 255, 255, 0.14), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(118, 200, 255, 0.08)),
rgba(11, 34, 58, 0.18);
border-color: var(--hud-border-active);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
@@ -357,137 +350,50 @@
.earth-zoom-group:hover > .earth-zoom-toolbar,
.earth-zoom-group:focus-within > .earth-zoom-toolbar,
.earth-zoom-group.open > .earth-zoom-toolbar,
.earth-info-group:hover > .earth-info-toolbar,
.earth-info-group:focus-within > .earth-info-toolbar,
.earth-info-group.open > .earth-info-toolbar {
.earth-zoom-group.open > .earth-zoom-toolbar {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translate(-50%, 0);
}
.earth-zoom-group.force-closed > .earth-zoom-toolbar,
.earth-info-group.force-closed > .earth-info-toolbar {
.earth-zoom-group.force-closed > .earth-zoom-toolbar {
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translate(-50%, 8px);
}
.earth-zoom-group > .earth-zoom-toolbar,
.earth-info-group > .earth-info-toolbar {
.earth-toolbar-popover::before {
content: "";
position: absolute;
left: 50%;
bottom: 100%;
transform: translateX(-50%);
width: calc(56px * var(--toolbar-scale));
height: calc(16px * var(--toolbar-scale));
background: transparent;
}
.earth-toolbar-popover > .earth-stack-toolbar {
position: absolute;
left: 50%;
top: auto;
right: auto;
left: 50%;
bottom: calc(100% + 12px);
bottom: calc(100% + (12px * var(--toolbar-scale)));
transform: translate(-50%, 10px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 8px;
}
.earth-info-toolbar {
width: min(280px, calc(100vw - 36px));
padding: 12px;
border-radius: 22px;
background:
radial-gradient(circle at top, rgba(255, 255, 255, 0.12), transparent 34%),
linear-gradient(180deg, rgba(16, 29, 48, 0.96), rgba(8, 18, 33, 0.94));
border: 1px solid rgba(211, 228, 246, 0.14);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.28),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
.earth-layer-toolbar-header {
width: 100%;
padding: 2px 4px 8px;
border-bottom: 1px solid rgba(201, 225, 247, 0.08);
margin-bottom: 2px;
}
.earth-layer-toolbar-title {
display: block;
color: var(--hud-accent-strong);
font-size: 0.86rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.earth-layer-toolbar-subtitle {
display: block;
margin-top: 4px;
color: var(--hud-text-soft);
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.earth-layer-btn {
width: 100%;
min-width: 0;
min-height: 52px;
height: auto;
border-radius: 16px;
padding: 12px 14px;
justify-content: space-between;
align-items: center;
gap: 12px;
overflow: hidden;
animation: none;
}
.earth-layer-btn__copy {
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
}
.earth-layer-btn__label {
color: var(--hud-text);
font-size: 0.92rem;
font-weight: 600;
line-height: 1.15;
}
.earth-layer-btn__meta {
color: var(--hud-text-soft);
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1.2;
}
.earth-layer-btn__state {
flex: 0 0 auto;
min-width: 42px;
padding: 5px 10px;
border-radius: 999px;
border: 1px solid rgba(201, 225, 247, 0.12);
color: var(--hud-text-soft);
font-size: 0.67rem;
font-weight: 700;
letter-spacing: 0.12em;
text-align: center;
text-transform: uppercase;
background: rgba(255, 255, 255, 0.04);
}
.earth-layer-btn.active .earth-layer-btn__state {
color: #dff4ff;
border-color: rgba(220, 240, 255, 0.24);
background: rgba(131, 197, 255, 0.14);
}
.earth-layer-btn .earth-toolbar-tooltip {
display: none;
gap: calc(8px * var(--toolbar-scale));
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 0.22s ease,
transform 0.22s ease,
visibility 0.22s ease;
z-index: 220;
}
.earth-zoom-toolbar .earth-zoom-btn,
@@ -495,8 +401,8 @@
width: 42px;
min-width: 42px;
border-radius: 50%;
color: #4db8ff;
animation: floatDock 3.8s ease-in-out infinite;
color: var(--hud-text-soft);
animation: none;
}
.earth-zoom-toolbar .earth-zoom-btn {
@@ -514,37 +420,8 @@
padding: 0;
font-size: 0.68rem;
letter-spacing: normal;
animation-delay: 0.18s;
}
.earth-zoom-toolbar .earth-zoom-btn:active,
.earth-zoom-toolbar .earth-zoom-btn.is-pressed,
.earth-zoom-toolbar .earth-zoom-value:active,
.earth-zoom-toolbar .earth-zoom-value.is-pressed {
letter-spacing: -0.01em;
}
.earth-zoom-toolbar .earth-zoom-btn:nth-child(1) {
animation-delay: 0s;
}
.earth-zoom-toolbar .earth-zoom-btn:nth-child(3) {
animation-delay: 0.34s;
}
.earth-zoom-toolbar .earth-toolbar-tooltip {
bottom: calc(100% + 10px);
}
.earth-zoom-toolbar .earth-toolbar-tooltip::after {
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: rgba(77, 184, 255, 0.4);
}
.earth-app.layout-expanded .earth-toolbar-group {
bottom: 18px;
transform: translateX(-50%);
@@ -555,8 +432,9 @@
bottom: 56px;
left: 50%;
transform: translateX(-50%);
background: rgba(10, 10, 30, 0.95);
color: #fff;
background:
linear-gradient(180deg, rgba(18, 31, 52, 0.96), rgba(8, 18, 32, 0.95));
color: var(--hud-text);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
@@ -564,9 +442,10 @@
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
border: 1px solid rgba(77, 184, 255, 0.4);
border: 1px solid var(--hud-border);
pointer-events: none;
z-index: 100;
box-shadow: var(--hud-shadow-soft);
}
.earth-toolbar-btn:hover .earth-toolbar-tooltip,
@@ -578,11 +457,11 @@
}
.earth-toolbar-btn .earth-toolbar-tooltip::after {
content: '';
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: rgba(77, 184, 255, 0.4);
border-top-color: rgba(18, 31, 52, 0.96);
}

View File

@@ -154,41 +154,43 @@
<div id="right-toolbar-group" class="earth-toolbar-group">
<div id="control-toolbar" class="earth-toolbar">
<div class="earth-toolbar-items">
<button id="search-action" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="搜索功能(待开发)">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">search</span>
</span>
<span class="tooltip earth-toolbar-tooltip">搜索功能(待开发)</span>
</button>
<button id="rotate-toggle" class="floating-btn liquid-glass-surface earth-toolbar-btn earth-rotate-toggle" title="自动旋转">
<span class="icon rotate-icon icon-pause" aria-hidden="true">
<span class="material-symbols-rounded">pause</span>
</span>
<span class="icon rotate-icon icon-play" aria-hidden="true">
<span class="material-symbols-rounded">play_arrow</span>
</span>
<span class="tooltip earth-toolbar-tooltip">自动旋转</span>
</button>
<button id="toggle-tv" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="新闻直播">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">live_tv</span>
</span>
<span class="tooltip earth-toolbar-tooltip">打开新闻直播</span>
</button>
<button id="toggle-news" class="floating-btn liquid-glass-surface earth-toolbar-btn active" title="全球态势新闻">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">newspaper</span>
</span>
<span class="tooltip earth-toolbar-tooltip">打开态势新闻</span>
</button>
<button id="reload-data" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="重新加载数据">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">refresh</span>
</span>
<span class="tooltip earth-toolbar-tooltip">重新加载数据</span>
</button>
<div id="zoom-control-group" class="earth-toolbar-popover earth-zoom-group">
<div id="toolbar-cluster" class="earth-toolbar-cluster is-expanded">
<div class="earth-toolbar-orb" data-orb-index="0" style="--orb-delay: 0s;">
<button id="search-action" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="搜索功能(待开发)">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">search</span>
</span>
<span class="tooltip earth-toolbar-tooltip">搜索功能(待开发)</span>
</button>
</div>
<div class="earth-toolbar-orb" data-orb-index="1" style="--orb-delay: 0.18s;">
<button id="rotate-toggle" class="floating-btn liquid-glass-surface earth-toolbar-btn earth-rotate-toggle" title="自动旋转">
<span class="icon rotate-icon icon-pause" aria-hidden="true">
<span class="material-symbols-rounded">pause</span>
</span>
<span class="icon rotate-icon icon-play" aria-hidden="true">
<span class="material-symbols-rounded">play_arrow</span>
</span>
<span class="tooltip earth-toolbar-tooltip">自动旋转</span>
</button>
</div>
<div class="earth-toolbar-orb" data-orb-index="2" style="--orb-delay: 0.36s;">
<button id="toggle-tv" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="新闻直播">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">live_tv</span>
</span>
<span class="tooltip earth-toolbar-tooltip">打开媒体面板</span>
</button>
</div>
<div class="earth-toolbar-orb" data-orb-index="3" style="--orb-delay: 0.54s;">
<button id="reload-data" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="重新加载数据">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">refresh</span>
</span>
<span class="tooltip earth-toolbar-tooltip">重新加载数据</span>
</button>
</div>
<div class="earth-toolbar-orb earth-toolbar-popover earth-zoom-group" id="zoom-control-group" data-orb-index="4" style="--orb-delay: 0.72s;">
<button id="zoom-trigger" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="缩放控制">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">zoom_in</span>
@@ -201,27 +203,38 @@
<button id="zoom-out" class="liquid-glass-surface earth-zoom-btn" title="缩小" aria-label="缩小"><span aria-hidden="true"></span></button>
</div>
</div>
<button id="settings-trigger" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="设置">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">settings</span>
</span>
<span class="tooltip earth-toolbar-tooltip">设置</span>
</button>
<button id="reset-view" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="重置视角">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">my_location</span>
</span>
<span class="tooltip earth-toolbar-tooltip">重置视角</span>
</button>
<button id="layout-toggle" class="floating-btn liquid-glass-surface earth-toolbar-btn earth-layout-toggle" title="最大化布局">
<span class="icon layout-icon layout-expand" aria-hidden="true">
<span class="material-symbols-rounded">open_in_full</span>
</span>
<span class="icon layout-icon layout-collapse" aria-hidden="true">
<span class="material-symbols-rounded">close_fullscreen</span>
</span>
<span class="tooltip earth-toolbar-tooltip">最大化布局</span>
</button>
<div class="earth-toolbar-orb" data-orb-index="5" style="--orb-delay: 0.9s;">
<button id="settings-trigger" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="设置">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">settings</span>
</span>
<span class="tooltip earth-toolbar-tooltip">设置</span>
</button>
</div>
<div class="earth-toolbar-orb" data-orb-index="6" style="--orb-delay: 1.08s;">
<button id="reset-view" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="重置视角">
<span class="icon" aria-hidden="true">
<span class="material-symbols-rounded">my_location</span>
</span>
<span class="tooltip earth-toolbar-tooltip">重置视角</span>
</button>
</div>
<div class="earth-toolbar-orb" data-orb-index="7" style="--orb-delay: 1.26s;">
<button id="layout-toggle" class="floating-btn liquid-glass-surface earth-toolbar-btn earth-layout-toggle" title="最大化布局">
<span class="icon layout-icon layout-expand" aria-hidden="true">
<span class="material-symbols-rounded">open_in_full</span>
</span>
<span class="icon layout-icon layout-collapse" aria-hidden="true">
<span class="material-symbols-rounded">close_fullscreen</span>
</span>
<span class="tooltip earth-toolbar-tooltip">最大化布局</span>
</button>
</div>
<div class="earth-toolbar-hub">
<button id="toolbar-hub" class="earth-toolbar-hub-btn liquid-glass-surface" title="工具菜单" aria-label="展开工具菜单">
<span class="material-symbols-rounded" aria-hidden="true">tune</span>
</button>
</div>
</div>
</div>
</div>
@@ -401,17 +414,16 @@
<div id="tooltip" class="earth-tooltip"></div>
<div id="settings-modal" class="earth-settings-modal" aria-hidden="true">
<div id="settings-backdrop" class="earth-settings-backdrop"></div>
<div class="earth-settings-sheet liquid-glass-surface" role="dialog" aria-modal="true" aria-labelledby="settings-title">
<div class="earth-settings-header">
<div>
<div class="earth-settings-sheet hud-panel" role="dialog" aria-modal="true" aria-label="设置">
<div class="earth-settings-header hud-panel__header">
<div class="hud-panel__title-group">
<div class="earth-settings-kicker">设置</div>
<h3 id="settings-title" class="earth-settings-title hud-panel-title">显示与视图</h3>
</div>
<button id="settings-close" class="earth-settings-close hud-panel-close" type="button" aria-label="关闭设置">
<button id="settings-close" class="earth-settings-close hud-panel__action hud-panel__action--close" type="button" aria-label="关闭设置">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="earth-settings-content">
<div class="earth-settings-content hud-panel__body">
<section class="earth-settings-section">
<div class="earth-settings-section-title">视图</div>
<div class="earth-settings-list">
@@ -447,8 +459,8 @@
</label>
<label class="earth-settings-item" for="toggle-view-tv">
<div class="earth-settings-copy">
<span class="earth-settings-item-title">电视直播</span>
<span class="earth-settings-item-subtitle">控制新闻直播窗口显示</span>
<span class="earth-settings-item-title">新闻直播</span>
<span class="earth-settings-item-subtitle">控制电视直播 / 态势聚合显示</span>
</div>
<span class="earth-settings-switch">
<input id="toggle-view-tv" type="checkbox" data-settings-panel="media-panel">
@@ -457,6 +469,26 @@
</label>
</div>
</section>
<section class="earth-settings-section">
<div class="earth-settings-section-title">系统</div>
<div class="earth-settings-list">
<a
class="earth-settings-item earth-settings-link"
href="/admin"
target="_blank"
rel="noreferrer noopener"
>
<div class="earth-settings-copy">
<span class="earth-settings-item-title">Admin</span>
<span class="earth-settings-item-subtitle">打开管理后台仪表盘</span>
</div>
<span class="earth-settings-link-meta">
<span class="material-symbols-rounded">admin_panel_settings</span>
<span class="material-symbols-rounded">arrow_forward</span>
</span>
</a>
</div>
</section>
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import * as THREE from "three";
import * as Astronomy from "astronomy-engine";
import { CELESTIAL_CONFIG, EARTH_CONFIG } from "./constants.js";
import { latLonToVector3 } from "./utils.js";
const textureLoader = new THREE.TextureLoader();
const defaultSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize();
@@ -32,6 +33,59 @@ let runtimeFollowConfig = {
};
const scratchEuler = new THREE.Euler(0, 0, 0, "YXZ");
function normalizeDegrees180(value) {
let normalized = value;
while (normalized <= -180) normalized += 360;
while (normalized > 180) normalized -= 360;
return normalized;
}
function computeGreenwichMeanSiderealDegrees(date) {
const jd = date.getTime() / 86400000 + 2440587.5;
const t = (jd - 2451545.0) / 36525.0;
const gmst =
280.46061837 +
360.98564736629 * (jd - 2451545.0) +
0.000387933 * t * t -
(t * t * t) / 38710000;
return THREE.MathUtils.euclideanModulo(gmst, 360);
}
function computeSubsolarLocalDirection(date) {
const vector = Astronomy.GeoVector(Astronomy.Body.Sun, date, false);
const radius = Math.sqrt(
vector.x * vector.x +
vector.y * vector.y +
vector.z * vector.z,
);
if (!Number.isFinite(radius) || radius === 0) {
return defaultSunDirection.clone();
}
const rightAscensionDeg = THREE.MathUtils.radToDeg(
Math.atan2(vector.y, vector.x),
);
const declinationDeg = THREE.MathUtils.radToDeg(
Math.asin(THREE.MathUtils.clamp(vector.z / radius, -1, 1)),
);
const gmstDeg = computeGreenwichMeanSiderealDegrees(date);
const subsolarLonDeg = normalizeDegrees180(rightAscensionDeg - gmstDeg);
return latLonToVector3(declinationDeg, subsolarLonDeg, 1).normalize();
}
function getPhysicalSunDirection(date = new Date()) {
const localSunDirection = computeSubsolarLocalDirection(date);
if (!linkedEarth) {
return localSunDirection;
}
return localSunDirection
.clone()
.applyQuaternion(linkedEarth.quaternion)
.normalize();
}
function getCelestialEuler() {
const { x, y, z } = runtimeOrientationEuler;
return new THREE.Euler(x, y, z, "YXZ");
@@ -279,13 +333,15 @@ function updateSpritePositions() {
}
function updateLighting() {
const calibratedSunDirection = applyCelestialOrientation(sunDirection);
const physicalSunDirection = getPhysicalSunDirection(
new Date(lastUpdatedAt || Date.now()),
);
if (linkedSunLight) {
linkedSunLight.color.setHex(CELESTIAL_CONFIG.sunLightColor);
linkedSunLight.intensity = CELESTIAL_CONFIG.sunLightIntensity;
linkedSunLight.position
.copy(calibratedSunDirection)
.copy(physicalSunDirection)
.multiplyScalar(CELESTIAL_CONFIG.sunLightDistance);
}
@@ -293,7 +349,7 @@ function updateLighting() {
linkedBackLight.color.setHex(CELESTIAL_CONFIG.backLightColor);
linkedBackLight.intensity = CELESTIAL_CONFIG.backLightIntensity;
linkedBackLight.position
.copy(calibratedSunDirection)
.copy(physicalSunDirection)
.multiplyScalar(-CELESTIAL_CONFIG.sunLightDistance * 0.7);
}
}
@@ -413,7 +469,7 @@ export function updateCelestialLayer(date = new Date(), camera = null) {
}
export function getSunDirection() {
return applyCelestialOrientation(sunDirection);
return getPhysicalSunDirection(new Date(lastUpdatedAt || Date.now()));
}
export function getMoonDirection() {

View File

@@ -1,5 +1,6 @@
// controls.js - Zoom, rotate and toggle controls
import * as THREE from "three";
import { CONFIG, EARTH_CONFIG } from "./constants.js";
import { updateZoomDisplay, showStatusMessage } from "./ui.js";
import { toggleTerrain } from "./earth.js";
@@ -40,6 +41,132 @@ const HUD_PANEL_IDS = [
];
const DRAGGABLE_PANEL_SELECTOR = ".hud-panel-draggable";
const PANEL_LAYOUT_ANIMATION_MS = 420;
const TOOLBAR_BASE_WIDTH_PX = 620;
const TOOLBAR_MIN_SCALE = 0.68;
const TOOLBAR_ORB_SIZE_PX = 46;
const TOOLBAR_HUB_SIZE_PX = 58;
const TOOLBAR_ORB_GAP_PX = 12;
const TOOLBAR_ARCH_SPAN_PX = 232;
const TOOLBAR_ARCH_RISE_PX = 40;
const TOOLBAR_SIDE_PADDING_PX = 12;
const TOOLBAR_BOTTOM_CLEARANCE_PX = 34;
const TOOLBAR_EXTRA_HEIGHT_PX = 34;
const SETTINGS_MODAL_OPEN_ANIMATION_MS = 420;
const SETTINGS_MODAL_CLOSE_ANIMATION_MS = 320;
const SETTINGS_SHEET_MIN_SCALE = 0.06;
const SETTINGS_SHEET_MAX_SCALE_X = 0.22;
const SETTINGS_SHEET_MAX_SCALE_Y = 0.18;
let settingsModalTimer = null;
let settingsSheetAnimation = null;
function cancelSettingsSheetAnimation() {
if (settingsSheetAnimation) {
settingsSheetAnimation.cancel();
settingsSheetAnimation = null;
}
}
function getSettingsSheetAnimationState(trigger, sheet) {
if (!(trigger instanceof HTMLElement) || !(sheet instanceof HTMLElement)) {
return null;
}
const triggerRect = trigger.getBoundingClientRect();
const sheetRect = sheet.getBoundingClientRect();
const triggerCenterX = triggerRect.left + triggerRect.width / 2;
const triggerCenterY = triggerRect.top + triggerRect.height / 2;
const sheetCenterX = sheetRect.left + sheetRect.width / 2;
const sheetCenterY = sheetRect.top + sheetRect.height / 2;
return {
translateX: triggerCenterX - sheetCenterX,
translateY: triggerCenterY - sheetCenterY,
scaleX: Math.max(
SETTINGS_SHEET_MIN_SCALE,
Math.min(
SETTINGS_SHEET_MAX_SCALE_X,
triggerRect.width / Math.max(sheetRect.width, 1),
),
),
scaleY: Math.max(
SETTINGS_SHEET_MIN_SCALE,
Math.min(
SETTINGS_SHEET_MAX_SCALE_Y,
triggerRect.height / Math.max(sheetRect.height, 1),
),
),
radius: `${Math.max(triggerRect.width, triggerRect.height).toFixed(2)}px`,
};
}
function animateSettingsSheet(sheet, trigger, opening) {
const animationState = getSettingsSheetAnimationState(trigger, sheet);
if (!animationState || typeof sheet.animate !== "function") {
return;
}
cancelSettingsSheetAnimation();
const fromTransform = `translate(${animationState.translateX.toFixed(2)}px, ${animationState.translateY.toFixed(2)}px) scale(${animationState.scaleX.toFixed(4)}, ${animationState.scaleY.toFixed(4)})`;
const toTransform = "translate(0px, 0px) scale(1, 1)";
const keyframes = opening
? [
{
transform: fromTransform,
opacity: 0.22,
filter: "blur(10px)",
borderRadius: animationState.radius,
},
{
transform: "translate(0px, 0px) scale(1.015, 1.015)",
opacity: 1,
filter: "blur(0px)",
borderRadius: "0px",
offset: 0.76,
},
{
transform: toTransform,
opacity: 1,
filter: "blur(0px)",
borderRadius: "0px",
},
]
: [
{
transform: toTransform,
opacity: 1,
filter: "blur(0px)",
borderRadius: "0px",
},
{
transform: fromTransform,
opacity: 0.08,
filter: "blur(10px)",
borderRadius: animationState.radius,
},
];
settingsSheetAnimation = sheet.animate(keyframes, {
duration: opening
? SETTINGS_MODAL_OPEN_ANIMATION_MS
: SETTINGS_MODAL_CLOSE_ANIMATION_MS,
easing: opening
? "cubic-bezier(0.16, 1, 0.3, 1)"
: "cubic-bezier(0.4, 0, 0.2, 1)",
fill: "both",
});
settingsSheetAnimation.onfinish = () => {
sheet.style.transform = "";
sheet.style.opacity = "";
sheet.style.filter = "";
sheet.style.borderRadius = "";
settingsSheetAnimation = null;
};
settingsSheetAnimation.oncancel = () => {
settingsSheetAnimation = null;
};
}
function getFloatingGroups() {
return [
@@ -76,17 +203,49 @@ function closeFloatingMenus() {
function openSettingsModal() {
const modal = document.getElementById("settings-modal");
const trigger = document.getElementById("settings-trigger");
const sheet = modal?.querySelector(".earth-settings-sheet");
if (!modal) return;
if (settingsModalTimer) {
clearTimeout(settingsModalTimer);
settingsModalTimer = null;
}
closeFloatingMenus();
cancelSettingsSheetAnimation();
modal.classList.remove("is-closing");
modal.classList.add("is-opening");
modal.classList.add("is-open");
modal.setAttribute("aria-hidden", "false");
requestAnimationFrame(() => {
if (sheet instanceof HTMLElement) {
animateSettingsSheet(sheet, trigger, true);
}
window.setTimeout(() => {
modal.classList.remove("is-opening");
}, SETTINGS_MODAL_OPEN_ANIMATION_MS);
});
}
function closeSettingsModal() {
const modal = document.getElementById("settings-modal");
const trigger = document.getElementById("settings-trigger");
const sheet = modal?.querySelector(".earth-settings-sheet");
if (!modal) return;
cancelSettingsSheetAnimation();
modal.classList.remove("is-open");
modal.setAttribute("aria-hidden", "true");
modal.classList.add("is-closing");
if (sheet instanceof HTMLElement) {
animateSettingsSheet(sheet, trigger, false);
}
if (settingsModalTimer) {
clearTimeout(settingsModalTimer);
}
settingsModalTimer = window.setTimeout(() => {
modal.classList.remove("is-closing", "is-opening");
modal.setAttribute("aria-hidden", "true");
settingsModalTimer = null;
}, SETTINGS_MODAL_CLOSE_ANIMATION_MS);
}
function setHudPanelVisibility(panelId, visible) {
@@ -361,6 +520,7 @@ export function setupControls(camera, renderer, scene, earth) {
setupRotateControls(camera, earth);
setupTerrainControls();
setupLiquidGlassInteractions();
setupToolbarHubCluster();
setupKeyboardControls();
}
@@ -841,42 +1001,68 @@ function setupKeyboardControls() {
}
function setupLiquidGlassInteractions() {
const surfaces = document.querySelectorAll(".liquid-glass-surface");
const surfaces = document.querySelectorAll(".liquid-glass-surface, .hud-panel");
const resetSurface = (surface) => {
surface.style.setProperty("--elastic-x", "0px");
surface.style.setProperty("--elastic-y", "0px");
surface.style.setProperty("--tilt-x", "0deg");
surface.style.setProperty("--tilt-y", "0deg");
surface.style.setProperty("--panel-tilt-x", "0deg");
surface.style.setProperty("--panel-tilt-y", "0deg");
surface.style.setProperty("--dock-scale", "1");
surface.style.setProperty("--dock-lift", "0px");
surface.style.setProperty("--dock-shift-x", "0px");
surface.style.setProperty("--glow-x", "50%");
surface.style.setProperty("--glow-y", "22%");
surface.style.setProperty("--glow-opacity", "0.24");
surface.style.setProperty("--panel-glow-x", "18%");
surface.style.setProperty("--panel-glow-y", "0%");
surface.style.setProperty("--panel-glow-opacity", "0.1");
surface.classList.remove("dock-focused");
surface.classList.remove("dock-active");
surface.classList.remove("is-pressed");
};
surfaces.forEach((surface) => {
resetSurface(surface);
const isToolbarSurface = Boolean(surface.closest(".earth-toolbar-items"));
const isPanelSurface = surface.classList.contains("hud-panel");
bindListener(surface, "pointermove", (event) => {
const rect = surface.getBoundingClientRect();
const px = (event.clientX - rect.left) / rect.width;
const py = (event.clientY - rect.top) / rect.height;
const offsetX = (px - 0.5) * 6;
const offsetY = (py - 0.5) * 6;
const tiltX = (0.5 - py) * 8;
const tiltY = (px - 0.5) * 10;
if (isPanelSurface) {
const panelTiltX = (0.5 - py) * 5;
const panelTiltY = (px - 0.5) * 6;
surface.style.setProperty("--panel-glow-x", `${(px * 100).toFixed(1)}%`);
surface.style.setProperty("--panel-glow-y", `${(py * 100).toFixed(1)}%`);
surface.style.setProperty("--panel-glow-opacity", "0.16");
surface.style.setProperty("--panel-tilt-x", `${panelTiltX.toFixed(2)}deg`);
surface.style.setProperty("--panel-tilt-y", `${panelTiltY.toFixed(2)}deg`);
} else if (!isToolbarSurface) {
const offsetX = (px - 0.5) * 6;
const offsetY = (py - 0.5) * 6;
const tiltX = (0.5 - py) * 8;
const tiltY = (px - 0.5) * 10;
surface.style.setProperty("--elastic-x", `${offsetX.toFixed(2)}px`);
surface.style.setProperty("--elastic-y", `${offsetY.toFixed(2)}px`);
surface.style.setProperty("--tilt-x", `${tiltX.toFixed(2)}deg`);
surface.style.setProperty("--tilt-y", `${tiltY.toFixed(2)}deg`);
surface.style.setProperty("--elastic-x", `${offsetX.toFixed(2)}px`);
surface.style.setProperty("--elastic-y", `${offsetY.toFixed(2)}px`);
surface.style.setProperty("--tilt-x", `${tiltX.toFixed(2)}deg`);
surface.style.setProperty("--tilt-y", `${tiltY.toFixed(2)}deg`);
}
surface.style.setProperty("--glow-x", `${(px * 100).toFixed(1)}%`);
surface.style.setProperty("--glow-y", `${(py * 100).toFixed(1)}%`);
surface.style.setProperty("--glow-opacity", "0.34");
});
bindListener(surface, "pointerenter", () => {
surface.style.setProperty("--glow-opacity", "0.28");
if (isPanelSurface) {
surface.style.setProperty("--panel-glow-opacity", "0.14");
} else {
surface.style.setProperty("--glow-opacity", "0.28");
}
});
bindListener(surface, "pointerleave", () => {
@@ -897,6 +1083,123 @@ function setupLiquidGlassInteractions() {
});
}
function setupToolbarHubCluster() {
const cluster = document.getElementById("toolbar-cluster");
const hub = document.getElementById("toolbar-hub");
const toolbar = document.getElementById("control-toolbar");
if (!(cluster instanceof HTMLElement) || !(hub instanceof HTMLButtonElement)) {
return;
}
let collapseTimer = null;
const layoutToolbarOrbs = () => {
const orbs = Array.from(cluster.querySelectorAll(".earth-toolbar-orb"));
if (orbs.length === 0) return;
const toolbarWidth = toolbar.clientWidth || TOOLBAR_BASE_WIDTH_PX;
const orbCount = orbs.length;
let toolbarScale = THREE.MathUtils.clamp(
toolbarWidth / TOOLBAR_BASE_WIDTH_PX,
TOOLBAR_MIN_SCALE,
1,
);
let orbSize = TOOLBAR_ORB_SIZE_PX * toolbarScale;
let desiredGap = TOOLBAR_ORB_GAP_PX * toolbarScale;
let span = TOOLBAR_ARCH_SPAN_PX * toolbarScale;
let rise = TOOLBAR_ARCH_RISE_PX * toolbarScale;
const minSpanForSpacing =
orbCount > 1 ? (orbCount - 1) * (orbSize + desiredGap) : orbSize;
const maxSpanByWidth =
toolbarWidth - orbSize - TOOLBAR_SIDE_PADDING_PX * 2 * toolbarScale;
if (minSpanForSpacing > maxSpanByWidth) {
toolbarScale = THREE.MathUtils.clamp(
maxSpanByWidth / minSpanForSpacing,
TOOLBAR_MIN_SCALE,
toolbarScale,
);
orbSize = TOOLBAR_ORB_SIZE_PX * toolbarScale;
desiredGap = TOOLBAR_ORB_GAP_PX * toolbarScale;
span = TOOLBAR_ARCH_SPAN_PX * toolbarScale;
rise = TOOLBAR_ARCH_RISE_PX * toolbarScale;
}
span = THREE.MathUtils.clamp(
span,
minSpanForSpacing,
maxSpanByWidth,
);
rise = Math.min(rise, span * 0.32);
const hubSize = TOOLBAR_HUB_SIZE_PX * toolbarScale;
const maxVerticalReach = rise + orbSize * 0.5;
const toolbarHeight = maxVerticalReach + hubSize + TOOLBAR_BOTTOM_CLEARANCE_PX * toolbarScale + TOOLBAR_EXTRA_HEIGHT_PX * toolbarScale;
toolbar.style.setProperty("--toolbar-scale", toolbarScale.toFixed(3));
toolbar.style.height = `${Math.ceil(toolbarHeight)}px`;
toolbar.style.setProperty("--toolbar-arc-width", `${Math.ceil(span + orbSize + (TOOLBAR_SIDE_PADDING_PX * 2 * toolbarScale))}px`);
toolbar.style.setProperty("--toolbar-arc-height", `${Math.ceil(rise + orbSize * 0.95)}px`);
toolbar.style.setProperty("--toolbar-inner-arc-width", `${Math.ceil(span * 0.72)}px`);
toolbar.style.setProperty("--toolbar-inner-arc-height", `${Math.ceil((hubSize * 0.8) + (desiredGap * 0.5))}px`);
orbs.forEach((orb, index) => {
const t = orbCount === 1 ? 0.5 : index / (orbCount - 1);
const x = (t - 0.5) * span;
const normalized = (x / (span / 2 || 1));
const y = -(1 - normalized * normalized) * rise;
orb.style.setProperty("--orb-x", `${x.toFixed(2)}px`);
orb.style.setProperty("--orb-y", `${y.toFixed(2)}px`);
});
};
const setExpanded = (expanded) => {
cluster.classList.toggle("is-expanded", expanded);
cluster.classList.toggle("is-collapsed", !expanded);
};
const scheduleCollapse = () => {
if (collapseTimer) clearTimeout(collapseTimer);
collapseTimer = window.setTimeout(() => {
setExpanded(false);
collapseTimer = null;
}, 200);
};
const cancelCollapse = () => {
if (collapseTimer) {
clearTimeout(collapseTimer);
collapseTimer = null;
}
};
cleanupFns.push(() => {
if (collapseTimer) clearTimeout(collapseTimer);
});
// Start collapsed — hub acts as the hover target to reveal the arc
layoutToolbarOrbs();
setExpanded(false);
bindListener(window, "resize", layoutToolbarOrbs);
bindListener(hub, "mouseenter", () => {
cancelCollapse();
setExpanded(true);
});
// Keep expanded while cursor stays anywhere within the toolbar area
bindListener(toolbar, "mouseenter", () => {
cancelCollapse();
});
bindListener(toolbar, "mouseleave", () => {
scheduleCollapse();
});
}
export function teardownControls() {
resetCleanup();
}

View File

@@ -1,5 +1,5 @@
import { showStatusMessage } from "./ui.js";
import { getActiveTVTab, openTVPanelTab, isTVPanelVisible, setTVPanelVisible } from "./tv.js";
import { isTVPanelVisible } from "./tv.js";
// News aggregation now lives inside the shared media panel:
// - outer shell: #media-panel
@@ -19,7 +19,6 @@ let lastFetchAt = 0;
let lastRegionSwitchAt = 0;
function getElements() {
return {
toggleBtn: document.getElementById("toggle-news"),
refreshBtn: document.getElementById("news-refresh"),
openBtn: document.getElementById("news-open-external"),
status: document.getElementById("news-board-status"),
@@ -53,14 +52,7 @@ function formatRelativeTime(raw) {
}
export function updateNewsToggleUI(visible) {
const { toggleBtn } = getElements();
if (!toggleBtn) return;
const active = visible && getActiveTVTab() === "news";
toggleBtn.classList.toggle("active", active);
const tooltip = toggleBtn.querySelector(".earth-toolbar-tooltip");
if (tooltip) {
tooltip.textContent = active ? "关闭态势新闻" : "打开态势新闻";
}
void visible;
}
function renderEmptyState(message) {
@@ -283,39 +275,11 @@ export function initNewsPanel() {
if (initialized) return;
initialized = true;
const { toggleBtn, refreshBtn, openBtn } = getElements();
const { refreshBtn, openBtn } = getElements();
updateNewsToggleUI(isTVPanelVisible());
renderEmptyState("正在准备全球态势新闻聚合源...");
const openNewsTab = async () => {
openTVPanelTab("news");
updateNewsToggleUI(true);
try {
await ensureNewsPanelReady();
} catch {
// surface already handled
}
};
toggleBtn?.addEventListener("click", async () => {
const visible = isTVPanelVisible();
const active = visible && getActiveTVTab() === "news";
if (!visible) {
await openNewsTab();
return;
}
if (!active) {
await openNewsTab();
return;
}
setTVPanelVisible(false);
updateNewsToggleUI(false);
});
window.addEventListener("earth:tv-tab-change", () => {
updateNewsToggleUI(isTVPanelVisible());
});

View File

@@ -323,11 +323,20 @@ function setupResizeHandle() {
function updateToggleButton(visible) {
const { toggleBtn } = getElements();
if (!toggleBtn) return;
const active = visible && activeTab === "live";
toggleBtn.classList.toggle("active", active);
const icon = toggleBtn.querySelector(".material-symbols-rounded");
const isLiveTab = activeTab === "live";
toggleBtn.classList.toggle("active", visible);
if (icon) {
icon.textContent = isLiveTab ? "live_tv" : "newspaper";
}
const title = visible
? (isLiveTab ? "切换到态势新闻" : "切换到新闻直播")
: (isLiveTab ? "打开新闻直播" : "打开态势新闻");
toggleBtn.title = title;
toggleBtn.setAttribute("aria-label", title);
const tooltip = toggleBtn.querySelector(".earth-toolbar-tooltip");
if (tooltip) {
tooltip.textContent = active ? "关闭新闻直播" : "打开新闻直播";
tooltip.textContent = title;
}
}
@@ -498,6 +507,7 @@ function setActiveTab(tab) {
updateTabState(newsHeaderControls, nextTab === "news");
updateTabState(livePane, nextTab === "live", "tv-tab-pane--active");
updateTabState(newsPane, nextTab === "news", "tv-tab-pane--active");
updateToggleButton(mediaPanel?.isVisible() ?? !panel?.classList.contains("hud-panel-hidden"));
if (
restoreLayoutState &&
@@ -1037,20 +1047,23 @@ export function initTVPanel() {
const currentlyVisible = mediaPanel?.isVisible() ?? false;
if (!currentlyVisible) {
setPanelVisible(true);
setActiveTab("live");
await ensureTVPanelReady();
showStatusMessage("新闻直播窗口已打开", "info");
if (activeTab === "live") {
await ensureTVPanelReady();
showStatusMessage("新闻直播窗口已打开", "info");
} else {
showStatusMessage("态势新闻窗口已打开", "info");
}
return;
}
if (activeTab !== "live") {
setActiveTab("live");
const nextTab = activeTab === "live" ? "news" : "live";
setActiveTab(nextTab);
if (nextTab === "live") {
await ensureTVPanelReady();
showStatusMessage("已切换到新闻直播", "info");
return;
}
setPanelVisible(false);
showStatusMessage("新闻直播窗口已关闭", "info");
showStatusMessage("已切换到态势新闻", "info");
});
select?.addEventListener("change", (event) => {

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.29.1"
version = "0.29.2"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]]
name = "planet"
version = "0.29.1"
version = "0.29.2"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },