release: bump version to 0.29.2
This commit is contained in:
@@ -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() {
|
||||
|
||||
325
frontend/public/earth/js/controls.js
vendored
325
frontend/public/earth/js/controls.js
vendored
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user