release: bump version to 0.29.2
This commit is contained in:
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user