From 30a29a6e34efabaf94e17012f0c12ec7c6e035d0 Mon Sep 17 00:00:00 2001 From: linkong Date: Thu, 26 Mar 2026 17:58:03 +0800 Subject: [PATCH] fix: redesign earth hud interactions and legend behavior --- VERSION | 2 +- docs/CHANGELOG.md | 29 + docs/version-history.md | 1 + frontend/public/earth/css/base.css | 751 +++++++++++++----- .../public/earth/css/coordinates-display.css | 7 +- frontend/public/earth/css/earth-stats.css | 14 +- frontend/public/earth/css/info-panel.css | 50 +- frontend/public/earth/css/legend.css | 20 +- frontend/public/earth/index.html | 203 +++-- frontend/public/earth/js/cables.js | 2 + frontend/public/earth/js/controls.js | 135 +++- frontend/public/earth/js/info-card.js | 28 +- frontend/public/earth/js/legend.js | 59 ++ frontend/public/earth/js/main.js | 4 + frontend/public/earth/js/ui.js | 47 +- 15 files changed, 988 insertions(+), 364 deletions(-) create mode 100644 frontend/public/earth/js/legend.js diff --git a/VERSION b/VERSION index a67cebaf..59dad104 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.21.1 +0.21.2 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a6a406cb..9f2c21e9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,35 @@ This project follows the repository versioning rule: - `feature` -> `+0.1.0` - `bugfix` -> `+0.0.1` +## 0.21.2 + +Released: 2026-03-26 + +### Highlights + +- Reworked the Earth page HUD into a bottom-centered floating toolbar with grouped popovers and richer interaction feedback. +- Unified toolbar and corner cards under a liquid-glass visual language and refined status toasts, object info cards, and legend behavior. +- Made the legend state reflect the currently selected Earth object instead of a fixed static list. + +### Added + +- Added a reusable Earth legend module in [legend.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/legend.js). +- Added Material Symbols-based Earth toolbar icons and dedicated fullscreen-collapse icon support. +- Added click-to-copy support for info-card field labels. + +### Improved + +- Improved Earth toolbar layout with centered floating controls, popover-based display toggles, and zoom controls. +- Improved Earth HUD visuals with liquid-glass styling for buttons, info cards, panels, and animated status messages. +- Improved info-card spacing, scrollbar styling, and object detail readability. +- Improved legend rendering so cable and satellite object selection can drive the displayed legend content. + +### Fixed + +- Fixed tooltip coverage and splash copy mismatches in the Earth page controls. +- Fixed several toolbar icon clarity, centering, and state-toggle issues. +- Fixed status-message behavior so repeated notifications replay the slide-in animation. + ## 0.20.0 Released: 2026-03-26 diff --git a/docs/version-history.md b/docs/version-history.md index 531447c2..920afb37 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -64,6 +64,7 @@ | `0.20.0` | feature | `dev` | `ce5feba3` | stabilize Earth module and fix satellite TLE handling | | `0.21.0` | feature | `dev` | `pending` | add Earth inertial drag, sync hover/trail state, and support unlimited satellite loading | | `0.21.1` | bugfix | `dev` | `pending` | polish Earth toolbar controls, icons, and loading copy | +| `0.21.2` | bugfix | `dev` | `pending` | redesign Earth HUD with liquid-glass controls, dynamic legend switching, and info-card interaction polish | ## Maintenance Commits Not Counted as Version Bumps diff --git a/frontend/public/earth/css/base.css b/frontend/public/earth/css/base.css index 0742a0af..aca47c74 100644 --- a/frontend/public/earth/css/base.css +++ b/frontend/public/earth/css/base.css @@ -13,6 +13,23 @@ body { overflow: hidden; } +:root { + --hud-border: rgba(210, 237, 255, 0.32); + --hud-border-hover: rgba(232, 246, 255, 0.48); + --hud-border-active: rgba(245, 251, 255, 0.62); + --glass-fill-top: rgba(255, 255, 255, 0.18); + --glass-fill-bottom: rgba(115, 180, 255, 0.08); + --glass-sheen: rgba(255, 255, 255, 0.34); + --glass-shadow: 0 14px 30px rgba(0, 0, 0, 0.22); + --glass-glow: 0 0 26px rgba(120, 200, 255, 0.16); +} + +@property --float-offset { + syntax: ''; + inherits: false; + initial-value: 0px; +} + #container { position: relative; width: 100vw; @@ -23,136 +40,103 @@ body { cursor: grabbing; } -/* Right Toolbar Group */ +/* Bottom Dock */ #right-toolbar-group { position: absolute; - bottom: 20px; - right: 290px; + bottom: 18px; + left: 50%; + transform: translateX(-50%); display: flex; - flex-direction: column; - align-items: flex-end; - gap: 10px; + flex-direction: row; + align-items: center; + justify-content: center; z-index: 200; } -/* Zoom Toolbar - Right side, vertical */ -#zoom-toolbar { - position: relative; - bottom: auto; - right: auto; - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - background: rgba(10, 10, 30, 0.9); - padding: 8px 4px; - border-radius: 24px; - border: 1px solid rgba(77, 184, 255, 0.3); - box-shadow: 0 0 20px rgba(77, 184, 255, 0.2); +#right-toolbar-group, +#info-panel, +#coordinates-display, +#legend, +#earth-stats { + transition: + top 0.45s ease, + right 0.45s ease, + bottom 0.45s ease, + left 0.45s ease, + transform 0.45s ease, + box-shadow 0.45s ease; } -#zoom-toolbar #zoom-slider { - width: 4px; - height: 50px; - margin: 4px 0; - writing-mode: vertical-lr; - direction: rtl; - -webkit-appearance: slider-vertical; -} - -#zoom-toolbar .zoom-percent { - font-size: 0.75rem; - font-weight: 600; - color: #4db8ff; - min-width: 30px; - display: inline-block; - text-align: center; - cursor: pointer; - padding: 2px 4px; - border-radius: 3px; - transition: all 0.2s ease; -} - -#zoom-toolbar .zoom-percent:hover { - background: rgba(77, 184, 255, 0.2); - box-shadow: 0 0 10px rgba(77, 184, 255, 0.3); -} - -#zoom-toolbar .zoom-btn { - width: 28px; - height: 28px; - min-width: 28px; - border: none; - border-radius: 50%; - background: rgba(77, 184, 255, 0.2); - color: #4db8ff; - font-size: 14px; - font-weight: bold; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - padding: 0; - margin: 0; - flex: 0 0 auto; - box-sizing: border-box; - position: relative; -} - -#zoom-toolbar .zoom-btn:hover { - background: rgba(77, 184, 255, 0.4); - transform: scale(1.1); - box-shadow: 0 0 10px rgba(77, 184, 255, 0.5); -} - -#zoom-toolbar #reset-view svg { - width: 18px; - height: 18px; - stroke: currentColor; - stroke-width: 1.8; - fill: none; - stroke-linecap: round; - stroke-linejoin: round; -} - -#zoom-toolbar .zoom-percent { - position: relative; -} - -#zoom-toolbar .tooltip { +#info-panel, +#coordinates-display, +#legend, +#earth-stats, +#satellite-info { position: absolute; - right: calc(100% + 10px); - top: 50%; - transform: translateY(-50%); - background: rgba(10, 10, 30, 0.95); - color: #fff; - padding: 6px 12px; - border-radius: 6px; - font-size: 12px; - white-space: nowrap; - opacity: 0; - visibility: hidden; - transition: all 0.2s ease; - border: 1px solid rgba(77, 184, 255, 0.4); - pointer-events: none; - z-index: 100; + overflow: hidden; + isolation: isolate; + background: + radial-gradient(circle at 24% 12%, rgba(255, 255, 255, 0.12), transparent 28%), + radial-gradient(circle at 78% 115%, rgba(255, 255, 255, 0.06), transparent 32%), + linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(110, 176, 255, 0.06)), + rgba(7, 18, 36, 0.28); + border: 1px solid rgba(225, 242, 255, 0.2); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.14), + inset 0 -1px 0 rgba(255, 255, 255, 0.04), + 0 18px 40px rgba(0, 0, 0, 0.24), + 0 0 32px rgba(120, 200, 255, 0.12); + backdrop-filter: blur(20px) saturate(145%); + -webkit-backdrop-filter: blur(20px) saturate(145%); } -#zoom-toolbar .zoom-btn:hover .tooltip, -#zoom-toolbar .zoom-percent:hover .tooltip { - opacity: 1; - visibility: visible; -} - -#zoom-toolbar .tooltip::after { +#info-panel::before, +#coordinates-display::before, +#legend::before, +#earth-stats::before, +#satellite-info::before { content: ''; position: absolute; - left: 100%; - top: 50%; - transform: translateY(-50%); - border: 6px solid transparent; - border-left-color: rgba(77, 184, 255, 0.4); + inset: 1px 1px 24% 1px; + border-radius: inherit; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.05) 26%, transparent 70%); + opacity: 0.46; + pointer-events: none; +} + +#info-panel::after, +#coordinates-display::after, +#legend::after, +#earth-stats::after, +#satellite-info::after { + content: ''; + position: absolute; + inset: -1px; + padding: 1.4px; + border-radius: inherit; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(170, 223, 255, 0.2) 34%, rgba(88, 169, 255, 0.14) 68%, rgba(255, 255, 255, 0.24)); + opacity: 0.78; + pointer-events: none; + filter: url(#liquid-glass-distortion) blur(0.35px); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; +} + +#info-panel > *, +#coordinates-display > *, +#legend > *, +#earth-stats > *, +#satellite-info > * { + position: relative; + z-index: 1; } #loading { @@ -236,16 +220,28 @@ input[type="range"]::-webkit-slider-thumb { .status-message { position: absolute; top: 20px; - right: 260px; + left: 50%; + transform: translate(-50%, -18px); background-color: rgba(10, 10, 30, 0.85); border-radius: 10px; padding: 10px 15px; - z-index: 10; + z-index: 210; box-shadow: 0 0 20px rgba(0, 150, 255, 0.3); border: 1px solid rgba(0, 150, 255, 0.2); font-size: 0.9rem; display: none; backdrop-filter: blur(5px); + text-align: center; + min-width: 180px; + opacity: 0; + transition: + transform 0.28s ease, + opacity 0.28s ease; +} + +.status-message.visible { + transform: translate(-50%, 0); + opacity: 1; } .status-message.success { @@ -278,83 +274,172 @@ input[type="range"]::-webkit-slider-thumb { user-select: none; } -/* Control Toolbar - Stellarium/Star Walk style */ +/* Floating toolbar dock */ #control-toolbar { position: relative; - bottom: auto; - right: auto; display: flex; align-items: center; - background: rgba(10, 10, 30, 0.9); - border-radius: 24px; - padding: 8px; - border: 1px solid rgba(77, 184, 255, 0.3); - box-shadow: 0 0 20px rgba(77, 184, 255, 0.2); - transition: all 0.3s ease; -} - -#control-toolbar.collapsed { - padding: 8px; -} - -#control-toolbar.collapsed .toolbar-items { - width: 0; - padding: 0; - margin: 0; - overflow: hidden; - opacity: 0; -} - -#toolbar-toggle { - min-width: 28px; - line-height: 1; - transition: all 0.3s ease; - flex-shrink: 0; + justify-content: center; + gap: 0; background: transparent; border: none; -} - -.toggle-arrow { - display: inline-flex; - align-items: center; - justify-content: center; - color: #4db8ff; - transition: transform 0.3s ease; -} - -.toggle-arrow svg { - width: 16px; - height: 16px; - stroke: currentColor; - stroke-width: 2.2; - fill: none; - stroke-linecap: round; - stroke-linejoin: round; -} - -#control-toolbar.collapsed .toggle-arrow { - transform: rotate(0deg); -} - -#control-toolbar:not(.collapsed) .toggle-arrow { - transform: rotate(180deg); -} - -#control-toolbar.collapsed #toolbar-toggle { - background: transparent; + box-shadow: none; + padding: 0; } .toolbar-items { display: flex; - gap: 6px; - width: auto; - padding: 0 4px 0 2px; - overflow: visible; - opacity: 1; - transition: all 0.3s ease; - border-right: 1px solid rgba(77, 184, 255, 0.3); - margin-right: 4px; - flex-shrink: 0; + gap: 10px; + align-items: center; + flex-wrap: nowrap; +} + +.floating-popover-group { + position: relative; +} + +.floating-popover-group::before { + content: ''; + position: absolute; + left: 50%; + bottom: 100%; + transform: translateX(-50%); + width: 56px; + height: 16px; + background: transparent; +} + +.floating-popover-group > .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; + pointer-events: none; + transition: + opacity 0.22s ease, + transform 0.22s ease, + visibility 0.22s ease; + z-index: 220; +} + +.toolbar-btn.floating-btn { + width: 42px; + height: 42px; + min-width: 42px; + min-height: 42px; + border-radius: 50%; + overflow: hidden; +} + +.liquid-glass-surface { + --elastic-x: 0px; + --elastic-y: 0px; + --tilt-x: 0deg; + --tilt-y: 0deg; + --btn-scale: 1; + --press-offset: 0px; + --float-offset: 0px; + --glow-opacity: 0.24; + --glow-x: 50%; + --glow-y: 22%; + position: relative; + isolation: isolate; + transform-style: preserve-3d; + 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); + border: 1px solid 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), + var(--glass-shadow), + var(--glass-glow); + 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) + scale(var(--btn-scale)); + transition: + transform 0.22s ease, + box-shadow 0.22s ease, + 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: ''; + 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; + pointer-events: none; + transform: + perspective(120px) + rotateX(calc(var(--tilt-x) * 0.7)) + rotateY(calc(var(--tilt-y) * 0.7)) + translate3d(calc(var(--elastic-x) * 0.22), calc(var(--elastic-y) * 0.22), 0); + transition: opacity 0.18s ease, transform 0.18s ease; +} + +.liquid-glass-surface::after { + 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; + pointer-events: none; + filter: url(#liquid-glass-distortion) blur(0.35px); + transform: + perspective(120px) + rotateX(calc(var(--tilt-x) * 0.5)) + rotateY(calc(var(--tilt-y) * 0.5)) + translate3d(calc(var(--elastic-x) * 0.16), calc(var(--elastic-y) * 0.16), 0); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask-composite: exclude; + transition: opacity 0.18s ease, transform 0.18s ease; +} + +.toolbar-items > :nth-child(2n).floating-btn, +.toolbar-items > :nth-child(2n) .floating-btn { + animation-delay: 0.18s; +} + +.toolbar-items > :nth-child(3n).floating-btn, +.toolbar-items > :nth-child(3n) .floating-btn { + animation-delay: 0.34s; +} + +@keyframes floatDock { + 0%, 100% { + --float-offset: 0px; + } + 50% { + --float-offset: -4px; + } } .toolbar-btn { @@ -362,44 +447,132 @@ input[type="range"]::-webkit-slider-thumb { width: 28px; height: 28px; border: none; - border-radius: 50%; - background: rgba(77, 184, 255, 0.15); + border-radius: 0; + background: transparent; color: #4db8ff; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; - transition: all 0.2s ease; box-sizing: border-box; padding: 0; margin: 0; + overflow: visible; + appearance: none; + -webkit-appearance: none; } -.toolbar-btn:hover { - background: rgba(77, 184, 255, 0.35); - transform: scale(1.1); - box-shadow: 0 0 15px rgba(77, 184, 255, 0.5); -} - -.toolbar-btn:active { - transform: scale(0.95); -} - -.toolbar-btn.active { - background: rgba(77, 184, 255, 0.4); - box-shadow: 0 0 10px rgba(77, 184, 255, 0.4) inset; +.toolbar-btn:not(.liquid-glass-surface)::after { + content: none; } .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; +} + +.liquid-glass-surface:hover { + --btn-scale: 1.035; + --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); + border-color: var(--hud-border-hover); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.2), + inset 0 -1px 0 rgba(255, 255, 255, 0.08), + 0 18px 36px rgba(0, 0, 0, 0.24), + 0 0 28px rgba(145, 214, 255, 0.22); +} + +.liquid-glass-surface:hover::before { + opacity: 0.62; + transform: scale(1.01); +} + +.liquid-glass-surface:hover::after { + opacity: 0.96; + transform: scale(1.01); +} + +.liquid-glass-surface:active, +.liquid-glass-surface.is-pressed { + --btn-scale: 0.942; + --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; +} + +#zoom-control-group #zoom-toolbar .zoom-btn:active, +#zoom-control-group #zoom-toolbar .zoom-btn.is-pressed, +#zoom-control-group #zoom-toolbar .zoom-percent:active, +#zoom-control-group #zoom-toolbar .zoom-percent.is-pressed { + letter-spacing: -0.01em; +} + +.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); + border-color: var(--hud-border-active); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.22), + inset 0 0 18px rgba(160, 220, 255, 0.14), + 0 18px 34px rgba(0, 0, 0, 0.24), + 0 0 30px rgba(145, 214, 255, 0.24); } .toolbar-btn svg { - width: 18px; - height: 18px; + width: 20px; + height: 20px; stroke: currentColor; stroke-width: 2.1; fill: none; @@ -407,18 +580,176 @@ input[type="range"]::-webkit-slider-thumb { stroke-linejoin: round; } +.toolbar-btn .material-symbols-rounded { + font-size: 21px; + line-height: 1; + font-variation-settings: + 'FILL' 0, + 'wght' 500, + 'GRAD' 0, + 'opsz' 24; + color: currentColor; + display: inline-flex; + align-items: center; + justify-content: center; + user-select: none; + pointer-events: none; + text-rendering: geometricPrecision; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.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; +} + #rotate-toggle .icon-play, -#rotate-toggle.is-stopped .icon-pause { +#rotate-toggle.is-stopped .icon-pause, +#layout-toggle .layout-collapse, +#layout-toggle.active .layout-expand { display: none; } -#rotate-toggle.is-stopped .icon-play { +#rotate-toggle.is-stopped .icon-play, +#layout-toggle.active .layout-collapse { display: inline-flex; } +#zoom-control-group:hover #zoom-toolbar, +#zoom-control-group:focus-within #zoom-toolbar, +#zoom-control-group.open #zoom-toolbar, +#info-control-group:hover #info-toolbar, +#info-control-group:focus-within #info-toolbar, +#info-control-group.open #info-toolbar { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translate(-50%, 0); +} + +#zoom-control-group #zoom-toolbar .zoom-percent { + min-width: 0; + width: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + height: 42px; + padding: 0; + font-size: 0.68rem; + border-radius: 50%; + color: #4db8ff; + animation: floatDock 3.8s ease-in-out infinite; + animation-delay: 0.18s; +} + +#zoom-control-group #zoom-toolbar .zoom-percent:hover { +} + +#zoom-control-group #zoom-toolbar, +#info-control-group #info-toolbar { + top: auto; + right: auto; + left: 50%; + bottom: calc(100% + 12px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 8px; +} + +#info-toolbar .toolbar-btn:nth-child(1) { + animation-delay: 0.34s; +} + +#info-toolbar .toolbar-btn:nth-child(2) { + animation-delay: 0.18s; +} + +#info-toolbar .toolbar-btn:nth-child(3) { + animation-delay: 0.1s; +} + +#info-toolbar .toolbar-btn:nth-child(4) { + animation-delay: 0s; +} + +#zoom-control-group #zoom-toolbar .zoom-btn { + width: 42px; + height: 42px; + min-width: 42px; + border-radius: 50%; + color: #4db8ff; + animation: floatDock 3.8s ease-in-out infinite; +} + +#zoom-toolbar .zoom-btn:nth-child(1) { + animation-delay: 0s; +} + +#zoom-toolbar .zoom-btn:nth-child(3) { + animation-delay: 0.34s; +} + +#zoom-control-group #zoom-toolbar .zoom-btn:hover { +} + +#zoom-control-group #zoom-toolbar .zoom-btn:active, +#zoom-control-group #zoom-toolbar .zoom-percent:active { +} + +#zoom-control-group #zoom-toolbar .tooltip { + bottom: calc(100% + 10px); +} + +#zoom-control-group #zoom-toolbar .tooltip::after { + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: rgba(77, 184, 255, 0.4); +} + +#container.layout-expanded #info-panel { + top: 20px; + left: 20px; + transform: translate(calc(-100% + 20px), calc(-100% + 20px)); +} + +#container.layout-expanded #coordinates-display { + top: 20px; + right: 20px; + transform: translate(calc(100% - 20px), calc(-100% + 20px)); +} + +#container.layout-expanded #legend { + left: 20px; + bottom: 20px; + transform: translate(calc(-100% + 20px), calc(100% - 20px)); +} + +#container.layout-expanded #earth-stats { + right: 20px; + bottom: 20px; + transform: translate(calc(100% - 20px), calc(100% - 20px)); +} + +#container.layout-expanded #right-toolbar-group { + bottom: 18px; + transform: translateX(-50%); +} + .toolbar-btn .tooltip { position: absolute; - bottom: 50px; + bottom: 56px; left: 50%; transform: translateX(-50%); background: rgba(10, 10, 30, 0.95); @@ -435,10 +766,12 @@ input[type="range"]::-webkit-slider-thumb { z-index: 100; } -.toolbar-btn:hover .tooltip { +.toolbar-btn:hover .tooltip, +.floating-popover-group:hover > .toolbar-btn .tooltip, +.floating-popover-group:focus-within > .toolbar-btn .tooltip { opacity: 1; visibility: visible; - bottom: 52px; + bottom: 58px; } .toolbar-btn .tooltip::after { diff --git a/frontend/public/earth/css/coordinates-display.css b/frontend/public/earth/css/coordinates-display.css index 07fe73f4..011cc8c9 100644 --- a/frontend/public/earth/css/coordinates-display.css +++ b/frontend/public/earth/css/coordinates-display.css @@ -1,18 +1,13 @@ /* coordinates-display */ #coordinates-display { - position: absolute; top: 20px; right: 20px; - background-color: rgba(10, 10, 30, 0.85); - border-radius: 10px; + border-radius: 18px; padding: 10px 15px; z-index: 10; - box-shadow: 0 0 20px rgba(0, 150, 255, 0.3); - border: 1px solid rgba(0, 150, 255, 0.2); font-size: 0.9rem; min-width: 180px; - backdrop-filter: blur(5px); } #coordinates-display .coord-item { diff --git a/frontend/public/earth/css/earth-stats.css b/frontend/public/earth/css/earth-stats.css index d7a1458a..9118063f 100644 --- a/frontend/public/earth/css/earth-stats.css +++ b/frontend/public/earth/css/earth-stats.css @@ -1,18 +1,13 @@ /* earth-stats */ #earth-stats { - position: absolute; bottom: 20px; right: 20px; - background-color: rgba(10, 10, 30, 0.85); - border-radius: 10px; + border-radius: 18px; padding: 15px; width: 250px; z-index: 10; - box-shadow: 0 0 20px rgba(0, 150, 255, 0.3); - border: 1px solid rgba(0, 150, 255, 0.2); font-size: 0.9rem; - backdrop-filter: blur(5px); } #earth-stats .stats-item { @@ -31,18 +26,13 @@ } #satellite-info { - position: absolute; bottom: 20px; right: 290px; - background-color: rgba(10, 10, 30, 0.9); - border-radius: 10px; + border-radius: 18px; padding: 15px; width: 220px; z-index: 10; - box-shadow: 0 0 20px rgba(0, 229, 255, 0.3); - border: 1px solid rgba(0, 229, 255, 0.3); font-size: 0.85rem; - backdrop-filter: blur(5px); } #satellite-info .stats-item { diff --git a/frontend/public/earth/css/info-panel.css b/frontend/public/earth/css/info-panel.css index a358db03..d99e45f0 100644 --- a/frontend/public/earth/css/info-panel.css +++ b/frontend/public/earth/css/info-panel.css @@ -1,17 +1,12 @@ /* info-panel */ #info-panel { - position: absolute; top: 20px; left: 20px; - background-color: rgba(10, 10, 30, 0.85); - border-radius: 10px; + border-radius: 18px; padding: 20px; width: 320px; z-index: 10; - box-shadow: 0 0 20px rgba(0, 150, 255, 0.3); - border: 1px solid rgba(0, 150, 255, 0.2); - backdrop-filter: blur(5px); } #info-panel h1 { @@ -159,8 +154,14 @@ /* Info Card - Unified details panel (inside info-panel) */ .info-card { margin-top: 15px; - background: rgba(0, 0, 0, 0.3); - border-radius: 8px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(110, 176, 255, 0.04)), + rgba(7, 18, 36, 0.2); + border-radius: 14px; + border: 1px solid rgba(225, 242, 255, 0.12); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 10px 24px rgba(0, 0, 0, 0.16); padding: 0; overflow: hidden; } @@ -174,7 +175,7 @@ display: flex; align-items: center; padding: 10px 12px; - background: rgba(77, 184, 255, 0.1); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(77, 184, 255, 0.06)); gap: 8px; } @@ -189,16 +190,35 @@ color: #4db8ff; } -.info-card-content { +#info-card-content { padding: 10px 12px; - max-height: 200px; + max-height: 40vh; overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgba(160, 220, 255, 0.45) transparent; +} + +#info-card-content::-webkit-scrollbar { + width: 6px; +} + +#info-card-content::-webkit-scrollbar-track { + background: transparent; +} + +#info-card-content::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, rgba(210, 237, 255, 0.32), rgba(110, 176, 255, 0.34)); + border-radius: 999px; +} + +#info-card-content::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, rgba(232, 246, 255, 0.42), rgba(128, 198, 255, 0.46)); } .info-card-property { display: flex; justify-content: space-between; - padding: 6px 0; + padding: 6px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } @@ -209,6 +229,12 @@ .info-card-label { color: #aaa; font-size: 0.85rem; + cursor: pointer; + transition: color 0.18s ease; +} + +.info-card-label:hover { + color: #d9f1ff; } .info-card-value { diff --git a/frontend/public/earth/css/legend.css b/frontend/public/earth/css/legend.css index b268ce00..efa99439 100644 --- a/frontend/public/earth/css/legend.css +++ b/frontend/public/earth/css/legend.css @@ -1,23 +1,29 @@ /* legend */ #legend { - position: absolute; bottom: 20px; left: 20px; - background-color: rgba(10, 10, 30, 0.85); - border-radius: 10px; + border-radius: 18px; padding: 15px; width: 220px; z-index: 10; - box-shadow: 0 0 20px rgba(0, 150, 255, 0.3); - border: 1px solid rgba(0, 150, 255, 0.2); - backdrop-filter: blur(5px); +} + +#legend .legend-title { + color: #4db8ff; + margin-bottom: 10px; + font-size: 1.1rem; +} + +#legend .legend-list { + display: flex; + flex-direction: column; + gap: 8px; } #legend .legend-item { display: flex; align-items: center; - margin-bottom: 8px; } #legend .legend-color { diff --git a/frontend/public/earth/index.html b/frontend/public/earth/index.html index e5f0f219..45872962 100644 --- a/frontend/public/earth/index.html +++ b/frontend/public/earth/index.html @@ -18,8 +18,18 @@ + +

智能星球计划

@@ -37,105 +47,92 @@
-
- - - 100%重置缩放到100% - -
-
- + - +
+ + + + +
+
+ - - - - +
+ +
+ + 100%重置缩放到100% + +
+
+ +
-
@@ -154,22 +151,24 @@
-

图例

-
-
- Americas II -
-
-
- AU Aleutian A -
-
-
- AU Aleutian B -
-
-
- 其他电缆 +

线缆图例

+
+
+
+ Americas II +
+
+
+ AU Aleutian A +
+
+
+ AU Aleutian B +
+
+
+ 其他电缆 +
diff --git a/frontend/public/earth/js/cables.js b/frontend/public/earth/js/cables.js index a2c375e1..baf6c647 100644 --- a/frontend/public/earth/js/cables.js +++ b/frontend/public/earth/js/cables.js @@ -6,6 +6,7 @@ import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from "./constants.js"; import { latLonToVector3 } from "./utils.js"; import { updateEarthStats, showStatusMessage } from "./ui.js"; import { showInfoCard } from "./info-card.js"; +import { setLegendMode } from "./legend.js"; export let cableLines = []; export let landingPoints = []; @@ -383,6 +384,7 @@ export function handleCableClick(cable) { lockedCable = cable; const data = cable.userData; + setLegendMode("cables"); showInfoCard("cable", { name: data.name, owner: data.owner, diff --git a/frontend/public/earth/js/controls.js b/frontend/public/earth/js/controls.js index 62735577..07bdaafd 100644 --- a/frontend/public/earth/js/controls.js +++ b/frontend/public/earth/js/controls.js @@ -16,6 +16,7 @@ export let autoRotate = true; export let zoomLevel = 1.0; export let showTerrain = false; export let isDragging = false; +export let layoutExpanded = false; let earthObj = null; let listeners = []; @@ -43,6 +44,7 @@ export function setupControls(camera, renderer, scene, earth) { setupWheelZoom(camera, renderer); setupRotateControls(camera, earth); setupTerrainControls(); + setupLiquidGlassInteractions(); } function setupZoomControls(camera) { @@ -285,13 +287,18 @@ function setupRotateControls(camera) { } function setupTerrainControls() { + const container = document.getElementById("container"); + const searchBtn = document.getElementById("search-action"); + const infoGroup = document.getElementById("info-control-group"); + const infoTrigger = document.getElementById("info-trigger"); const terrainBtn = document.getElementById("toggle-terrain"); const satellitesBtn = document.getElementById("toggle-satellites"); const trailsBtn = document.getElementById("toggle-trails"); const cablesBtn = document.getElementById("toggle-cables"); + const layoutBtn = document.getElementById("layout-toggle"); const reloadBtn = document.getElementById("reload-data"); - const toolbarToggle = document.getElementById("toolbar-toggle"); - const toolbar = document.getElementById("control-toolbar"); + const zoomGroup = document.getElementById("zoom-control-group"); + const zoomTrigger = document.getElementById("zoom-trigger"); if (trailsBtn) { trailsBtn.classList.add("active"); @@ -299,6 +306,10 @@ function setupTerrainControls() { if (tooltip) tooltip.textContent = "隐藏轨迹"; } + bindListener(searchBtn, "click", () => { + showStatusMessage("搜索功能待开发", "info"); + }); + bindListener(terrainBtn, "click", function () { showTerrain = !showTerrain; toggleTerrain(showTerrain); @@ -352,11 +363,102 @@ function setupTerrainControls() { await reloadData(); }); - if (toolbarToggle && toolbar) { - bindListener(toolbarToggle, "click", () => { - toolbar.classList.toggle("collapsed"); + bindListener(zoomTrigger, "click", (event) => { + event.stopPropagation(); + infoGroup?.classList.remove("open"); + zoomGroup?.classList.toggle("open"); + }); + + bindListener(zoomGroup, "click", (event) => { + event.stopPropagation(); + }); + + bindListener(infoTrigger, "click", (event) => { + event.stopPropagation(); + zoomGroup?.classList.remove("open"); + infoGroup?.classList.toggle("open"); + }); + + bindListener(infoGroup, "click", (event) => { + event.stopPropagation(); + }); + + bindListener(document, "click", (event) => { + if (zoomGroup?.classList.contains("open")) { + if (!zoomGroup.contains(event.target)) { + zoomGroup.classList.remove("open"); + } + } + if (infoGroup?.classList.contains("open")) { + if (!infoGroup.contains(event.target)) { + infoGroup.classList.remove("open"); + } + } + }); + + bindListener(layoutBtn, "click", () => { + const expanded = toggleLayoutExpanded(container); + showStatusMessage(expanded ? "布局已最大化" : "布局已恢复", "info"); + }); + + updateLayoutUI(container); +} + +function setupLiquidGlassInteractions() { + const surfaces = document.querySelectorAll(".liquid-glass-surface"); + + 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("--glow-x", "50%"); + surface.style.setProperty("--glow-y", "22%"); + surface.style.setProperty("--glow-opacity", "0.24"); + surface.classList.remove("is-pressed"); + }; + + surfaces.forEach((surface) => { + resetSurface(surface); + + 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; + + 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"); + }); + + bindListener(surface, "pointerleave", () => { + resetSurface(surface); + }); + + bindListener(surface, "pointerdown", () => { + surface.classList.add("is-pressed"); + }); + + bindListener(surface, "pointerup", () => { + surface.classList.remove("is-pressed"); + }); + + bindListener(surface, "pointercancel", () => { + resetSurface(surface); + }); + }); } export function teardownControls() { @@ -396,3 +498,24 @@ export function getZoomLevel() { export function getShowTerrain() { return showTerrain; } + +function updateLayoutUI(container) { + if (container) { + container.classList.toggle("layout-expanded", layoutExpanded); + } + + const btn = document.getElementById("layout-toggle"); + if (btn) { + btn.classList.toggle("active", layoutExpanded); + const tooltip = btn.querySelector(".tooltip"); + const nextLabel = layoutExpanded ? "恢复布局" : "最大化布局"; + btn.title = nextLabel; + if (tooltip) tooltip.textContent = nextLabel; + } +} + +function toggleLayoutExpanded(container) { + layoutExpanded = !layoutExpanded; + updateLayoutUI(container); + return layoutExpanded; +} diff --git a/frontend/public/earth/js/info-card.js b/frontend/public/earth/js/info-card.js index e7281862..6be5cee8 100644 --- a/frontend/public/earth/js/info-card.js +++ b/frontend/public/earth/js/info-card.js @@ -1,4 +1,5 @@ // info-card.js - Unified info card module +import { showStatusMessage } from './ui.js'; let currentType = null; @@ -55,7 +56,32 @@ const CARD_CONFIG = { }; export function initInfoCard() { - // Close button removed - now uses external clear button + const content = document.getElementById('info-card-content'); + if (!content || content.dataset.copyBound === 'true') return; + + content.addEventListener('click', async (event) => { + const label = event.target.closest('.info-card-label'); + if (!label) return; + + const property = label.closest('.info-card-property'); + const valueEl = property?.querySelector('.info-card-value'); + const value = valueEl?.textContent?.trim(); + + if (!value || value === '-') { + showStatusMessage('无可复制内容', 'warning'); + return; + } + + try { + await navigator.clipboard.writeText(value); + showStatusMessage(`已复制${label.textContent}:${value}`, 'success'); + } catch (error) { + console.error('Copy failed:', error); + showStatusMessage('复制失败', 'error'); + } + }); + + content.dataset.copyBound = 'true'; } export function setInfoCardNoBorder(noBorder = true) { diff --git a/frontend/public/earth/js/legend.js b/frontend/public/earth/js/legend.js new file mode 100644 index 00000000..18eac298 --- /dev/null +++ b/frontend/public/earth/js/legend.js @@ -0,0 +1,59 @@ +const LEGEND_MODES = { + cables: { + title: "线缆图例", + items: [ + { color: "#ff4444", label: "Americas II" }, + { color: "#44ff44", label: "AU Aleutian A" }, + { color: "#4444ff", label: "AU Aleutian B" }, + { color: "#ffff44", label: "其他电缆" }, + ], + }, + satellites: { + title: "卫星图例", + items: [ + { color: "#4db8ff", label: "卫星本体" }, + { color: "#9be7ff", label: "卫星轨迹" }, + { color: "#7dffb3", label: "悬停高亮" }, + { color: "#ffd166", label: "选中目标" }, + ], + }, +}; + +let currentLegendMode = "cables"; + +export function initLegend() { + renderLegend(currentLegendMode); +} + +export function setLegendMode(mode) { + const nextMode = LEGEND_MODES[mode] ? mode : "cables"; + if (nextMode === currentLegendMode) return; + currentLegendMode = nextMode; + renderLegend(currentLegendMode); +} + +export function getLegendMode() { + return currentLegendMode; +} + +function renderLegend(mode) { + const legend = document.getElementById("legend"); + if (!legend) return; + + const config = LEGEND_MODES[mode] || LEGEND_MODES.cables; + const itemsHtml = config.items + .map( + (item) => ` +
+
+ ${item.label} +
+ `, + ) + .join(""); + + legend.innerHTML = ` +

${config.title}

+
${itemsHtml}
+ `; +} diff --git a/frontend/public/earth/js/main.js b/frontend/public/earth/js/main.js index 9e397916..7b9217f8 100644 --- a/frontend/public/earth/js/main.js +++ b/frontend/public/earth/js/main.js @@ -74,6 +74,7 @@ import { hideInfoCard, setInfoCardNoBorder, } from "./info-card.js"; +import { initLegend, setLegendMode } from "./legend.js"; export let scene; export let camera; @@ -194,6 +195,7 @@ function isSameCable(cable1, cable2) { } function showCableInfo(cable) { + setLegendMode("cables"); showInfoCard("cable", { name: cable.userData.name, owner: cable.userData.owner, @@ -211,6 +213,7 @@ function showSatelliteInfo(props) { const perigee = (6371 * (1 - ecc)).toFixed(0); const apogee = (6371 * (1 + ecc)).toFixed(0); + setLegendMode("satellites"); showInfoCard("satellite", { name: props?.name || "-", norad_id: props?.norad_cat_id, @@ -333,6 +336,7 @@ export function init() { addLights(); initInfoCard(); + initLegend(); const earthObj = createEarth(scene); targetRotation = { x: earthObj.rotation.x, diff --git a/frontend/public/earth/js/ui.js b/frontend/public/earth/js/ui.js index d2cf346a..672150e8 100644 --- a/frontend/public/earth/js/ui.js +++ b/frontend/public/earth/js/ui.js @@ -1,6 +1,8 @@ // ui.js - UI update functions let statusTimeoutId = null; +let statusHideTimeoutId = null; +let statusReplayTimeoutId = null; // Show status message export function showStatusMessage(message, type = "info") { @@ -12,15 +14,44 @@ export function showStatusMessage(message, type = "info") { statusTimeoutId = null; } - statusEl.textContent = message; - statusEl.className = `status-message ${type}`; - statusEl.style.display = "block"; + if (statusHideTimeoutId) { + clearTimeout(statusHideTimeoutId); + statusHideTimeoutId = null; + } - statusTimeoutId = setTimeout(() => { - statusEl.style.display = "none"; - statusEl.textContent = ""; - statusTimeoutId = null; - }, 3000); + if (statusReplayTimeoutId) { + clearTimeout(statusReplayTimeoutId); + statusReplayTimeoutId = null; + } + + const startShow = () => { + statusEl.textContent = message; + statusEl.className = `status-message ${type}`; + statusEl.style.display = "block"; + statusEl.offsetHeight; + statusEl.classList.add("visible"); + + statusTimeoutId = setTimeout(() => { + statusEl.classList.remove("visible"); + statusHideTimeoutId = setTimeout(() => { + statusEl.style.display = "none"; + statusEl.textContent = ""; + statusHideTimeoutId = null; + }, 280); + statusTimeoutId = null; + }, 3000); + }; + + if (statusEl.classList.contains("visible")) { + statusEl.classList.remove("visible"); + statusReplayTimeoutId = setTimeout(() => { + startShow(); + statusReplayTimeoutId = null; + }, 180); + return; + } + + startShow(); } // Update coordinates display