release: bump version to 0.31.0
This commit is contained in:
378
frontend/public/earth/js/controls.js
vendored
378
frontend/public/earth/js/controls.js
vendored
@@ -1,7 +1,7 @@
|
||||
// controls.js - Zoom, rotate and toggle controls
|
||||
|
||||
import * as THREE from "three";
|
||||
import { CONFIG, EARTH_CONFIG } from "./constants.js";
|
||||
import { CONFIG, EARTH_CONFIG, ROTATION_MODE } from "./constants.js";
|
||||
import { updateZoomDisplay, showStatusMessage } from "./ui.js";
|
||||
import { toggleTerrain } from "./earth.js";
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ export let autoRotate = true;
|
||||
export let zoomLevel = 1.0;
|
||||
export let showTerrain = false;
|
||||
export let layoutExpanded = false;
|
||||
export let rotationMode = ROTATION_MODE.ROTATE;
|
||||
|
||||
let earthObj = null;
|
||||
let listeners = [];
|
||||
@@ -57,6 +58,7 @@ 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 HUD_EDGE_GAP_PX = 20;
|
||||
const SETTINGS_MODAL_OPEN_ANIMATION_MS = 420;
|
||||
const SETTINGS_MODAL_CLOSE_ANIMATION_MS = 320;
|
||||
const SETTINGS_SHEET_MIN_SCALE = 0.06;
|
||||
@@ -65,6 +67,7 @@ const SETTINGS_SHEET_MAX_SCALE_Y = 0.18;
|
||||
let settingsModalTimer = null;
|
||||
let settingsSheetAnimation = null;
|
||||
let terrainToggleToken = 0;
|
||||
let focusViewAnimationToken = 0;
|
||||
|
||||
function getViewRotation(targetLat, targetRotLon) {
|
||||
const latRot = (targetLat * Math.PI) / 180;
|
||||
@@ -74,6 +77,17 @@ function getViewRotation(targetLat, targetRotLon) {
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchRotationModeChange() {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("earth:rotation-mode-change", {
|
||||
detail: {
|
||||
mode: rotationMode,
|
||||
active: autoRotate,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function applyTerrainUiState(button, enabled) {
|
||||
showTerrain = enabled;
|
||||
toggleTerrain(enabled);
|
||||
@@ -362,6 +376,7 @@ function setupSettingsControls() {
|
||||
|
||||
const terrainOpacitySlider = document.getElementById("terrain-opacity-slider");
|
||||
const terrainOpacityValue = document.getElementById("terrain-opacity-value");
|
||||
const rotationModeButtons = document.querySelectorAll("[data-rotation-mode]");
|
||||
const syncTerrainOpacityUi = (nextOpacity) => {
|
||||
const safeOpacity = Math.round(nextOpacity * 100);
|
||||
if (terrainOpacitySlider instanceof HTMLInputElement) {
|
||||
@@ -386,7 +401,18 @@ function setupSettingsControls() {
|
||||
});
|
||||
}
|
||||
|
||||
rotationModeButtons.forEach((button) => {
|
||||
bindListener(button, "click", (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLButtonElement)) return;
|
||||
const nextMode = target.dataset.rotationMode;
|
||||
if (!nextMode) return;
|
||||
setRotationMode(nextMode);
|
||||
});
|
||||
});
|
||||
|
||||
syncAllHudPanelToggles();
|
||||
syncRotationModeButtons();
|
||||
}
|
||||
|
||||
function setupHudPanelControls() {
|
||||
@@ -403,6 +429,116 @@ function setupHudPanelControls() {
|
||||
});
|
||||
}
|
||||
|
||||
function capturePanelAnchor(app, panel, desiredLeft, desiredTop) {
|
||||
// 拖拽期间:按用户给出的绝对位置重置 anchor,轴模式回到 left/top。
|
||||
// clamp 真的触发时由 syncPanelAnchorFromClamp 改写成 right/bottom 模式。
|
||||
panel.dataset.anchorXSide = "left";
|
||||
panel.dataset.anchorX = String(desiredLeft);
|
||||
panel.dataset.anchorYSide = "top";
|
||||
panel.dataset.anchorY = String(desiredTop);
|
||||
}
|
||||
|
||||
function getHudScaleValue() {
|
||||
const rawScale = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--hud-scale")
|
||||
.trim();
|
||||
const parsedScale = Number.parseFloat(rawScale);
|
||||
return Number.isFinite(parsedScale) && parsedScale > 0 ? parsedScale : 1;
|
||||
}
|
||||
|
||||
function getPreferredHudEdgeGap() {
|
||||
return HUD_EDGE_GAP_PX * getHudScaleValue();
|
||||
}
|
||||
|
||||
function syncDraggedPanelSize(panel) {
|
||||
const dragWidthBase = Number.parseFloat(panel.dataset.dragWidthBase ?? "");
|
||||
if (!Number.isFinite(dragWidthBase)) return;
|
||||
const nextWidth = dragWidthBase * getHudScaleValue();
|
||||
panel.style.width = `${nextWidth}px`;
|
||||
}
|
||||
|
||||
function syncPanelAnchorFromClamp(
|
||||
app,
|
||||
panel,
|
||||
desiredLeft,
|
||||
desiredTop,
|
||||
clampedLeft,
|
||||
clampedTop,
|
||||
) {
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
// 只在 clamp 真的改了坐标时切换贴边方向:
|
||||
// clamp 把 left 往小推 → 右边/下方的边碰到 panel 了 → 切到 right/bottom 模式。
|
||||
// 这里保存的是“恢复时应回到的默认边距”,不是 shrink 期间瞬时的 0 间距。
|
||||
// clamp 把 left 往大推 → 左边/上方的边(含 brand L 区)碰到 panel 了 → 记到 left/top 模式。
|
||||
const preferredGap = getPreferredHudEdgeGap();
|
||||
if (clampedLeft < desiredLeft) {
|
||||
panel.dataset.anchorXSide = "right";
|
||||
panel.dataset.anchorX = String(preferredGap);
|
||||
} else if (clampedLeft > desiredLeft) {
|
||||
panel.dataset.anchorXSide = "left";
|
||||
panel.dataset.anchorX = String(clampedLeft <= 0 ? preferredGap : clampedLeft);
|
||||
}
|
||||
if (clampedTop < desiredTop) {
|
||||
panel.dataset.anchorYSide = "bottom";
|
||||
panel.dataset.anchorY = String(preferredGap);
|
||||
} else if (clampedTop > desiredTop) {
|
||||
panel.dataset.anchorYSide = "top";
|
||||
panel.dataset.anchorY = String(clampedTop <= 0 ? preferredGap : clampedTop);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAnchorDesiredPosition(app, panel) {
|
||||
syncDraggedPanelSize(panel);
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const anchorX = parseFloat(panel.dataset.anchorX ?? "");
|
||||
const anchorY = parseFloat(panel.dataset.anchorY ?? "");
|
||||
const fallbackLeft = parseFloat(panel.style.left) || 0;
|
||||
const fallbackTop = parseFloat(panel.style.top) || 0;
|
||||
|
||||
const desiredLeft = Number.isFinite(anchorX)
|
||||
? panel.dataset.anchorXSide === "right"
|
||||
? appRect.width - panelRect.width - anchorX
|
||||
: anchorX
|
||||
: fallbackLeft;
|
||||
const desiredTop = Number.isFinite(anchorY)
|
||||
? panel.dataset.anchorYSide === "bottom"
|
||||
? appRect.height - panelRect.height - anchorY
|
||||
: anchorY
|
||||
: fallbackTop;
|
||||
|
||||
return { desiredLeft, desiredTop };
|
||||
}
|
||||
|
||||
function clampDraggedPanelPosition(app, panel, desiredLeft, desiredTop) {
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const brandPanel = document.getElementById("brand-panel");
|
||||
const brandRect = brandPanel ? brandPanel.getBoundingClientRect() : null;
|
||||
const brandBottom = brandRect ? brandRect.bottom - appRect.top : 0;
|
||||
const brandRight = brandRect ? brandRect.right - appRect.left : 0;
|
||||
|
||||
const maxLeft = Math.max(0, appRect.width - panelRect.width);
|
||||
const maxTop = Math.max(0, appRect.height - panelRect.height);
|
||||
let nextLeft = Math.min(Math.max(desiredLeft, 0), maxLeft);
|
||||
let nextTop = Math.min(Math.max(desiredTop, 0), maxTop);
|
||||
|
||||
// Brand 面板形成 L 形禁区:panel 不能进入 brand 左上角矩形区域。
|
||||
// 当两个轴同时越界时,比较两侧超出量——哪侧需要的调整量更小就卡哪侧。
|
||||
// 从右侧滑入 → leftAdjust 小 → 卡右边;从下方滑入 → topAdjust 小 → 卡底边。
|
||||
if (brandRect && nextLeft < brandRight && nextTop < brandBottom) {
|
||||
const leftAdjust = brandRight - nextLeft;
|
||||
const topAdjust = brandBottom - nextTop;
|
||||
if (leftAdjust <= topAdjust) {
|
||||
nextLeft = brandRight;
|
||||
} else {
|
||||
nextTop = brandBottom;
|
||||
}
|
||||
}
|
||||
return { left: nextLeft, top: nextTop };
|
||||
}
|
||||
|
||||
function setupDraggableHudPanels() {
|
||||
const app = document.getElementById("container");
|
||||
const draggablePanels = document.querySelectorAll(DRAGGABLE_PANEL_SELECTOR);
|
||||
@@ -428,40 +564,22 @@ function setupDraggableHudPanels() {
|
||||
|
||||
const onMove = (event) => {
|
||||
if (!isDragging) return;
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const brandPanel = document.getElementById("brand-panel");
|
||||
const brandRect = brandPanel ? brandPanel.getBoundingClientRect() : null;
|
||||
const brandBottom = brandRect ? brandRect.bottom - appRect.top : 0;
|
||||
const brandRight = brandRect ? brandRect.right - appRect.left : 0;
|
||||
|
||||
let nextLeft = Math.min(
|
||||
Math.max(startLeft + (event.clientX - startPointerX), 0),
|
||||
appRect.width - panelRect.width,
|
||||
const desiredLeft = startLeft + (event.clientX - startPointerX);
|
||||
const desiredTop = startTop + (event.clientY - startPointerY);
|
||||
capturePanelAnchor(app, panel, desiredLeft, desiredTop);
|
||||
const { left, top } = clampDraggedPanelPosition(
|
||||
app,
|
||||
panel,
|
||||
desiredLeft,
|
||||
desiredTop,
|
||||
);
|
||||
let nextTop = Math.min(
|
||||
Math.max(startTop + (event.clientY - startPointerY), 0),
|
||||
appRect.height - panelRect.height,
|
||||
);
|
||||
// Brand 面板形成 L 形禁区:panel 不能进入 brand 左上角矩形区域。
|
||||
// 当两个轴同时越界时,比较两侧超出量——哪侧需要的调整量更小就卡哪侧。
|
||||
// 从右侧滑入 → leftAdjust 小 → 卡右边;从下方滑入 → topAdjust 小 → 卡底边。
|
||||
if (brandRect && nextLeft < brandRight && nextTop < brandBottom) {
|
||||
const leftAdjust = brandRight - nextLeft;
|
||||
const topAdjust = brandBottom - nextTop;
|
||||
if (leftAdjust <= topAdjust) {
|
||||
nextLeft = brandRight;
|
||||
} else {
|
||||
nextTop = brandBottom;
|
||||
}
|
||||
}
|
||||
|
||||
panel.style.left = `${nextLeft}px`;
|
||||
panel.style.top = `${nextTop}px`;
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.top = `${top}px`;
|
||||
panel.style.right = "auto";
|
||||
panel.style.bottom = "auto";
|
||||
panel.style.transform = "none";
|
||||
panel.dataset.dragged = "true";
|
||||
syncPanelAnchorFromClamp(app, panel, desiredLeft, desiredTop, left, top);
|
||||
};
|
||||
|
||||
bindListener(handle, "pointerdown", (event) => {
|
||||
@@ -478,6 +596,9 @@ function setupDraggableHudPanels() {
|
||||
panel.dataset.originalParentId = panel.parentElement?.id || "";
|
||||
panel.dataset.originalNextSiblingId = panel.nextElementSibling?.id || "";
|
||||
const capturedWidth = panelRect.width;
|
||||
panel.dataset.dragWidthBase = String(
|
||||
capturedWidth / Math.max(getHudScaleValue(), 0.001),
|
||||
);
|
||||
panel.style.position = "absolute";
|
||||
panel.style.width = `${capturedWidth}px`;
|
||||
panel.style.margin = "0";
|
||||
@@ -492,6 +613,7 @@ function setupDraggableHudPanels() {
|
||||
panel.style.bottom = "auto";
|
||||
panel.style.transform = "none";
|
||||
panel.dataset.dragged = "true";
|
||||
capturePanelAnchor(app, panel, startLeft, startTop);
|
||||
panel.classList.add("is-dragging");
|
||||
document.body.style.userSelect = "none";
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
@@ -502,6 +624,30 @@ function setupDraggableHudPanels() {
|
||||
bindListener(handle, "pointercancel", stopDragging);
|
||||
bindListener(handle, "lostpointercapture", stopDragging);
|
||||
});
|
||||
|
||||
const reclampDraggedPanels = () => {
|
||||
draggablePanels.forEach((panel) => {
|
||||
if (panel.dataset.dragged !== "true") return;
|
||||
syncDraggedPanelSize(panel);
|
||||
const { desiredLeft, desiredTop } = resolveAnchorDesiredPosition(
|
||||
app,
|
||||
panel,
|
||||
);
|
||||
const { left, top } = clampDraggedPanelPosition(
|
||||
app,
|
||||
panel,
|
||||
desiredLeft,
|
||||
desiredTop,
|
||||
);
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.top = `${top}px`;
|
||||
syncPanelAnchorFromClamp(app, panel, desiredLeft, desiredTop, left, top);
|
||||
});
|
||||
};
|
||||
|
||||
bindListener(window, "resize", () => {
|
||||
window.requestAnimationFrame(reclampDraggedPanels);
|
||||
});
|
||||
}
|
||||
|
||||
function clearForcedFloatingClose() {
|
||||
@@ -736,9 +882,14 @@ function applyZoom(camera) {
|
||||
}
|
||||
|
||||
function animateValue(start, end, duration, onUpdate, onComplete) {
|
||||
const animationToken = ++focusViewAnimationToken;
|
||||
const startTime = performance.now();
|
||||
|
||||
function update(currentTime) {
|
||||
if (animationToken !== focusViewAnimationToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||
@@ -748,7 +899,7 @@ function animateValue(start, end, duration, onUpdate, onComplete) {
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(update);
|
||||
} else if (onComplete) {
|
||||
} else if (onComplete && animationToken === focusViewAnimationToken) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
@@ -759,58 +910,37 @@ function animateValue(start, end, duration, onUpdate, onComplete) {
|
||||
export function resetView(camera) {
|
||||
if (!earthObj) return;
|
||||
|
||||
function animateToView(targetLat, targetLon, targetRotLon) {
|
||||
const targetRotation = getViewRotation(targetLat, targetRotLon);
|
||||
|
||||
const startRotX = earthObj.rotation.x;
|
||||
const startRotY = earthObj.rotation.y;
|
||||
const startZoom = zoomLevel;
|
||||
const targetZoom = 1.0;
|
||||
|
||||
animateValue(
|
||||
0,
|
||||
1,
|
||||
800,
|
||||
(progress) => {
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
earthObj.rotation.x =
|
||||
startRotX + (targetRotation.x - startRotX) * ease;
|
||||
earthObj.rotation.y =
|
||||
startRotY + (targetRotation.y - startRotY) * ease;
|
||||
|
||||
zoomLevel = startZoom + (targetZoom - startZoom) * ease;
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||
},
|
||||
() => {
|
||||
zoomLevel = 1.0;
|
||||
showStatusMessage("视角已重置", "info");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) =>
|
||||
animateToView(
|
||||
pos.coords.latitude,
|
||||
pos.coords.longitude,
|
||||
-pos.coords.longitude,
|
||||
),
|
||||
focusEarthView(camera, {
|
||||
lat: pos.coords.latitude,
|
||||
lon: pos.coords.longitude,
|
||||
rotLon: pos.coords.longitude - 270,
|
||||
zoom: 1.0,
|
||||
duration: 800,
|
||||
suppressStatus: false,
|
||||
}),
|
||||
() =>
|
||||
animateToView(
|
||||
EARTH_CONFIG.chinaLat,
|
||||
EARTH_CONFIG.chinaLon,
|
||||
EARTH_CONFIG.chinaRotLon,
|
||||
),
|
||||
focusEarthView(camera, {
|
||||
lat: EARTH_CONFIG.chinaLat,
|
||||
lon: EARTH_CONFIG.chinaLon,
|
||||
rotLon: EARTH_CONFIG.chinaRotLon,
|
||||
zoom: 1.0,
|
||||
duration: 800,
|
||||
suppressStatus: false,
|
||||
}),
|
||||
{ timeout: 5000, enableHighAccuracy: false },
|
||||
);
|
||||
} else {
|
||||
animateToView(
|
||||
EARTH_CONFIG.chinaLat,
|
||||
EARTH_CONFIG.chinaLon,
|
||||
EARTH_CONFIG.chinaRotLon,
|
||||
);
|
||||
focusEarthView(camera, {
|
||||
lat: EARTH_CONFIG.chinaLat,
|
||||
lon: EARTH_CONFIG.chinaLon,
|
||||
rotLon: EARTH_CONFIG.chinaRotLon,
|
||||
zoom: 1.0,
|
||||
duration: 800,
|
||||
suppressStatus: false,
|
||||
});
|
||||
}
|
||||
|
||||
clearLockedObject();
|
||||
@@ -822,7 +952,8 @@ function setupRotateControls(camera) {
|
||||
|
||||
bindListener(rotateBtn, "click", () => {
|
||||
const isRotating = toggleAutoRotate();
|
||||
showStatusMessage(isRotating ? "自动旋转已开启" : "自动旋转已暂停", "info");
|
||||
const label = rotationMode === ROTATION_MODE.CRUISE ? "巡航" : "自动旋转";
|
||||
showStatusMessage(isRotating ? `${label}已开启` : `${label}已暂停`, "info");
|
||||
});
|
||||
|
||||
updateRotateUI();
|
||||
@@ -995,6 +1126,9 @@ function setupTerrainControls() {
|
||||
const showNextBGP = !getShowBGP();
|
||||
clearSelectionIfHiding(!showNextBGP);
|
||||
toggleBGP(showNextBGP);
|
||||
if (!showNextBGP && rotationMode === ROTATION_MODE.CRUISE && autoRotate) {
|
||||
setAutoRotate(false);
|
||||
}
|
||||
updateLayerButtonState(this, showNextBGP);
|
||||
setButtonTooltip(this, showNextBGP ? "隐藏BGP观测" : "显示BGP观测");
|
||||
const bgpCountEl = document.getElementById("bgp-anomaly-count");
|
||||
@@ -1292,28 +1426,111 @@ export function getAutoRotate() {
|
||||
return autoRotate;
|
||||
}
|
||||
|
||||
function getRotationModeLabel(mode = rotationMode) {
|
||||
return mode === ROTATION_MODE.CRUISE ? "巡航模式" : "旋转模式";
|
||||
}
|
||||
|
||||
function syncRotationModeButtons() {
|
||||
const buttons = document.querySelectorAll("[data-rotation-mode]");
|
||||
buttons.forEach((button) => {
|
||||
if (!(button instanceof HTMLButtonElement)) return;
|
||||
const isActive = button.dataset.rotationMode === rotationMode;
|
||||
button.classList.toggle("is-active", isActive);
|
||||
button.setAttribute("aria-pressed", isActive ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function updateRotateUI() {
|
||||
const btn = document.getElementById("rotate-toggle");
|
||||
if (btn) {
|
||||
btn.classList.toggle("active", autoRotate);
|
||||
btn.classList.toggle("is-stopped", !autoRotate);
|
||||
const tooltip = btn.querySelector(".earth-toolbar-tooltip");
|
||||
if (tooltip) tooltip.textContent = autoRotate ? "暂停旋转" : "开始旋转";
|
||||
const activeLabel =
|
||||
rotationMode === ROTATION_MODE.CRUISE ? "巡航" : "自动旋转";
|
||||
if (tooltip) {
|
||||
tooltip.textContent = autoRotate ? `暂停${activeLabel}` : `开始${activeLabel}`;
|
||||
}
|
||||
btn.title = `${getRotationModeLabel()} · ${activeLabel}`;
|
||||
}
|
||||
|
||||
syncRotationModeButtons();
|
||||
}
|
||||
|
||||
export function setAutoRotate(value) {
|
||||
autoRotate = value;
|
||||
updateRotateUI();
|
||||
dispatchRotationModeChange();
|
||||
}
|
||||
|
||||
export function toggleAutoRotate() {
|
||||
autoRotate = !autoRotate;
|
||||
updateRotateUI();
|
||||
clearLockedObject();
|
||||
dispatchRotationModeChange();
|
||||
return autoRotate;
|
||||
}
|
||||
|
||||
export function getRotationMode() {
|
||||
return rotationMode;
|
||||
}
|
||||
|
||||
export function setRotationMode(nextMode) {
|
||||
const normalizedMode =
|
||||
nextMode === ROTATION_MODE.CRUISE ? ROTATION_MODE.CRUISE : ROTATION_MODE.ROTATE;
|
||||
const changed = normalizedMode !== rotationMode;
|
||||
rotationMode = normalizedMode;
|
||||
updateRotateUI();
|
||||
dispatchRotationModeChange();
|
||||
if (changed) {
|
||||
showStatusMessage(
|
||||
normalizedMode === ROTATION_MODE.CRUISE ? "已切换到巡航模式" : "已切换到旋转模式",
|
||||
"info",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function focusEarthView(camera, options = {}) {
|
||||
if (!earthObj || !camera) return Promise.resolve();
|
||||
|
||||
const {
|
||||
lat = EARTH_CONFIG.chinaLat,
|
||||
lon = EARTH_CONFIG.chinaLon,
|
||||
rotLon = lon - 270,
|
||||
zoom = 1.0,
|
||||
duration = 800,
|
||||
suppressStatus = true,
|
||||
} = options;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const nextRotation = getViewRotation(lat, rotLon);
|
||||
const startRotX = earthObj.rotation.x;
|
||||
const startRotY = earthObj.rotation.y;
|
||||
const startZoom = zoomLevel;
|
||||
|
||||
animateValue(
|
||||
0,
|
||||
1,
|
||||
duration,
|
||||
(progress) => {
|
||||
const ease = 1 - Math.pow(1 - progress, 3);
|
||||
earthObj.rotation.x = startRotX + (nextRotation.x - startRotX) * ease;
|
||||
earthObj.rotation.y = startRotY + (nextRotation.y - startRotY) * ease;
|
||||
zoomLevel = startZoom + (zoom - startZoom) * ease;
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||
},
|
||||
() => {
|
||||
zoomLevel = zoom;
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage("视角已重置", "info");
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getZoomLevel() {
|
||||
return zoomLevel;
|
||||
}
|
||||
@@ -1362,6 +1579,11 @@ function resetPanelInlineLayout(panel) {
|
||||
panel.style.width = "";
|
||||
panel.style.margin = "";
|
||||
delete panel.dataset.dragged;
|
||||
delete panel.dataset.anchorX;
|
||||
delete panel.dataset.anchorY;
|
||||
delete panel.dataset.anchorXSide;
|
||||
delete panel.dataset.anchorYSide;
|
||||
delete panel.dataset.dragWidthBase;
|
||||
}
|
||||
|
||||
function isPanelVisible(panel) {
|
||||
|
||||
Reference in New Issue
Block a user