From 543fe35fbb3c6c2f19e1ca20dbcabe11dc539d0b Mon Sep 17 00:00:00 2001 From: linkong Date: Mon, 23 Mar 2026 17:41:27 +0800 Subject: [PATCH] fix(satellites): fix ring size attenuation and breathing animation - Add sizeAttenuation: false to sprite materials for fixed ring size - Move breathing animation parameters to SATELLITE_CONFIG constants - Export updateBreathingPhase function to avoid ES module binding issues - Adjust breathing speed and amplitude for better visual effect --- frontend/public/earth/js/constants.js | 17 ++- frontend/public/earth/js/main.js | 39 +++++- frontend/public/earth/js/satellites.js | 164 +++++++++++++++++++------ 3 files changed, 174 insertions(+), 46 deletions(-) diff --git a/frontend/public/earth/js/constants.js b/frontend/public/earth/js/constants.js index ea94bbd8..fbb027a7 100644 --- a/frontend/public/earth/js/constants.js +++ b/frontend/public/earth/js/constants.js @@ -54,10 +54,19 @@ export const CABLE_STATE = { }; export const SATELLITE_CONFIG = { - maxCount: 2000, - dotSize: 1.5, - trailLength: 30, - apiPath: '/api/v1/visualization/geo/satellites' + maxCount: 5000, + trailLength: 10, + dotSize: 4, + ringSize: 0.07, + apiPath: '/api/v1/visualization/geo/satellites', + breathingSpeed: 0.08, + breathingScaleAmplitude: 0.15, + breathingOpacityMin: 0.5, + breathingOpacityMax: 0.8, + dotBreathingSpeed: 0.12, + dotBreathingScaleAmplitude: 0.2, + dotOpacityMin: 0.7, + dotOpacityMax: 1.0 }; export const PREDICTED_ORBIT_CONFIG = { diff --git a/frontend/public/earth/js/main.js b/frontend/public/earth/js/main.js index 4d7c287b..e5ced5df 100644 --- a/frontend/public/earth/js/main.js +++ b/frontend/public/earth/js/main.js @@ -14,7 +14,7 @@ import { } from './ui.js'; import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js'; import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates, applyLandingPointVisualState, resetLandingPointVisualState, getAllLandingPoints, getShowCables } from './cables.js'; -import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions, showPredictedOrbit, hidePredictedOrbit } from './satellites.js'; +import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions, showPredictedOrbit, hidePredictedOrbit, updateBreathingPhase } from './satellites.js'; import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js'; import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js'; @@ -32,6 +32,9 @@ let lockedObject = null; let lockedObjectType = null; let dragStartTime = 0; let isLongDrag = false; +let lastSatClickTime = 0; +let lastSatClickIndex = 0; +let lastSatClickPos = { x: 0, y: 0 }; export function clearLockedObject() { hidePredictedOrbit(); @@ -44,6 +47,7 @@ export function clearLockedObject() { lockedObjectType = null; lockedSatellite = null; lockedSatelliteIndex = null; + window.lockedSatelliteIndex = null; cableLockedData = null; } @@ -135,6 +139,7 @@ export function init() { camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = CONFIG.defaultCameraZ; + window.camera = camera; renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' }); renderer.setSize(window.innerWidth, window.innerHeight); @@ -430,8 +435,26 @@ function onClick(event, camera, renderer) { setAutoRotate(false); handleCableClick(clickedCable); } else if (satIntersects.length > 0) { - const index = satIntersects[0].index; - const sat = selectSatellite(index); + const now = Date.now(); + const clickX = event.clientX; + const clickY = event.clientY; + + let selectedIndex; + if (satIntersects.length > 1 && + now - lastSatClickTime < 500 && + Math.abs(clickX - lastSatClickPos.x) < 30 && + Math.abs(clickY - lastSatClickPos.y) < 30) { + const currentIdx = satIntersects.findIndex(s => s.index === lastSatClickIndex); + selectedIndex = satIntersects[(currentIdx + 1) % satIntersects.length].index; + } else { + selectedIndex = satIntersects[0].index; + } + + lastSatClickTime = now; + lastSatClickIndex = selectedIndex; + lastSatClickPos = { x: clickX, y: clickY }; + + const sat = selectSatellite(selectedIndex); if (sat && sat.properties) { clearLockedObject(); @@ -439,13 +462,14 @@ function onClick(event, camera, renderer) { lockedObject = sat; lockedObjectType = 'satellite'; lockedSatellite = sat; - lockedSatelliteIndex = index; + lockedSatelliteIndex = selectedIndex; + window.lockedSatelliteIndex = selectedIndex; showPredictedOrbit(sat); setAutoRotate(false); const satPositions = getSatellitePositions(); - if (satPositions && satPositions[index]) { - setSatelliteRingState(index, 'locked', satPositions[index].current); + if (satPositions && satPositions[selectedIndex]) { + setSatelliteRingState(selectedIndex, 'locked', satPositions[selectedIndex].current); } const props = sat.properties; @@ -501,6 +525,9 @@ function animate() { const satPositions = getSatellitePositions(); + // 更新呼吸动画相位 + updateBreathingPhase(); + if (lockedObjectType === 'satellite' && lockedSatelliteIndex !== null) { if (satPositions && satPositions[lockedSatelliteIndex]) { updateLockedRingPosition(satPositions[lockedSatelliteIndex].current); diff --git a/frontend/public/earth/js/satellites.js b/frontend/public/earth/js/satellites.js index 5f0fc5be..b4af0255 100644 --- a/frontend/public/earth/js/satellites.js +++ b/frontend/public/earth/js/satellites.js @@ -14,13 +14,19 @@ let selectedSatellite = null; let satellitePositions = []; let hoverRingSprite = null; let lockedRingSprite = null; +let lockedDotSprite = null; +export let breathingPhase = 0; + +export function updateBreathingPhase() { + breathingPhase += SATELLITE_CONFIG.breathingSpeed; +} const SATELLITE_API = SATELLITE_CONFIG.apiPath + '?limit=' + SATELLITE_CONFIG.maxCount; const MAX_SATELLITES = SATELLITE_CONFIG.maxCount; const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength; const DOT_TEXTURE_SIZE = 32; -function createCircularDotTexture() { +function createDotTexture() { const canvas = document.createElement('canvas'); canvas.width = DOT_TEXTURE_SIZE; canvas.height = DOT_TEXTURE_SIZE; @@ -68,7 +74,7 @@ export function createSatellites(scene, earthObj) { const positions = new Float32Array(MAX_SATELLITES * 3); const colors = new Float32Array(MAX_SATELLITES * 3); - const dotTexture = createCircularDotTexture(); + const dotTexture = createDotTexture(); const pointsGeometry = new THREE.BufferGeometry(); pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); @@ -80,13 +86,27 @@ export function createSatellites(scene, earthObj) { vertexColors: true, transparent: true, opacity: 0.9, - sizeAttenuation: true, + sizeAttenuation: false, alphaTest: 0.1 }); satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial); satellitePoints.visible = false; satellitePoints.userData = { type: 'satellitePoints' }; + + const originalScale = { x: 1, y: 1, z: 1 }; + satellitePoints.onBeforeRender = (renderer, scene, camera, geometry, material) => { + if (earthObj && earthObj.scale.x !== 1) { + satellitePoints.scale.set( + originalScale.x / earthObj.scale.x, + originalScale.y / earthObj.scale.y, + originalScale.z / earthObj.scale.z + ); + } else { + satellitePoints.scale.set(originalScale.x, originalScale.y, originalScale.z); + } + }; + earthObj.add(satellitePoints); const trailPositions = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3); @@ -112,7 +132,9 @@ export function createSatellites(scene, earthObj) { for (let i = 0; i < MAX_SATELLITES; i++) { satellitePositions.push({ current: new THREE.Vector3(), - trail: [] + trail: [], + trailIndex: 0, + trailCount: 0 }); } @@ -149,12 +171,8 @@ function computeSatellitePosition(satellite, time) { const tleLine1 = `1 ${noradId.toString().padStart(5)}U 00001A ${epochStr} .00000000 00000-0 00000-0 0 9999`; const tleLine2 = `2 ${noradId.toString().padStart(5)} ${raan.toFixed(4).padStart(8)} ${inclination.toFixed(4).padStart(8)} ${eccStr.substring(1)} ${argOfPerigee.toFixed(4).padStart(8)} ${meanAnomaly.toFixed(4).padStart(8)} ${meanMotion.toFixed(8).padStart(11)} 0 9999`; - console.log('[DEBUG computeSat] TLE1:', tleLine1); - console.log('[DEBUG computeSat] TLE2:', tleLine2); const satrec = twoline2satrec(tleLine1, tleLine2); - console.log('[DEBUG computeSat] satrec.error:', satrec?.error, 'satrec.no:', satrec?.no); if (!satrec || satrec.error) { - console.log('[DEBUG computeSat] returning null due to satrec error'); return null; } @@ -255,9 +273,11 @@ export function updateSatellitePositions(deltaTime = 0) { satellitePositions[i].current.copy(pos); - satellitePositions[i].trail.push(pos.clone()); - if (satellitePositions[i].trail.length > TRAIL_LENGTH) { - satellitePositions[i].trail.shift(); + const satPos = satellitePositions[i]; + if (i !== window.lockedSatelliteIndex) { + satPos.trail[satPos.trailIndex] = pos.clone(); + satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH; + if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++; } positions[i * 3] = pos.x; @@ -287,20 +307,33 @@ export function updateSatellitePositions(deltaTime = 0) { colors[i * 3 + 1] = g; colors[i * 3 + 2] = b; - const trail = satellitePositions[i].trail; + const sp = satellitePositions[i]; + const trail = sp.trail; + const tc = sp.trailCount; + const ti = sp.trailIndex; + for (let j = 0; j < TRAIL_LENGTH; j++) { const trailIdx = (i * TRAIL_LENGTH + j) * 3; - if (j < trail.length) { - const t = trail[j]; - trailPositions[trailIdx] = t.x; - trailPositions[trailIdx + 1] = t.y; - trailPositions[trailIdx + 2] = t.z; - - const alpha = j / trail.length; - trailColors[trailIdx] = r * alpha; - trailColors[trailIdx + 1] = g * alpha; - trailColors[trailIdx + 2] = b * alpha; + if (j < tc) { + const idx = (ti - tc + j + TRAIL_LENGTH) % TRAIL_LENGTH; + const t = trail[idx]; + if (t) { + trailPositions[trailIdx] = t.x; + trailPositions[trailIdx + 1] = t.y; + trailPositions[trailIdx + 2] = t.z; + const alpha = (j + 1) / tc; + trailColors[trailIdx] = r * alpha; + trailColors[trailIdx + 1] = g * alpha; + trailColors[trailIdx + 2] = b * alpha; + } else { + trailPositions[trailIdx] = pos.x; + trailPositions[trailIdx + 1] = pos.y; + trailPositions[trailIdx + 2] = pos.z; + trailColors[trailIdx] = 0; + trailColors[trailIdx + 1] = 0; + trailColors[trailIdx + 2] = 0; + } } else { trailPositions[trailIdx] = pos.x; trailPositions[trailIdx + 1] = pos.y; @@ -312,7 +345,7 @@ export function updateSatellitePositions(deltaTime = 0) { } } - for (let i = count; i < 2000; i++) { + for (let i = count; i < MAX_SATELLITES; i++) { positions[i * 3] = 0; positions[i * 3 + 1] = 0; positions[i * 3 + 2] = 0; @@ -393,12 +426,19 @@ export function showHoverRing(position, isLocked = false) { map: ringTexture, transparent: true, opacity: 0.8, - depthTest: false + depthTest: false, + sizeAttenuation: false }); + const ringSize = SATELLITE_CONFIG.ringSize; const sprite = new THREE.Sprite(spriteMaterial); sprite.position.copy(position); - sprite.scale.set(3, 3, 1); + + const camera = window.camera; + const cameraDistance = camera ? camera.position.distanceTo(position) : 400; + const scale = ringSize; + sprite.scale.set(scale, scale, 1); + console.log(`[Ring create] ringSize: ${ringSize}, camDist: ${cameraDistance}, scale: ${scale}`); earthObjRef.add(sprite); @@ -407,6 +447,24 @@ export function showHoverRing(position, isLocked = false) { earthObjRef.remove(lockedRingSprite); } lockedRingSprite = sprite; + + if (lockedDotSprite) { + earthObjRef.remove(lockedDotSprite); + } + const dotCanvas = createBrighterDotCanvas(); + const dotTexture = new THREE.CanvasTexture(dotCanvas); + dotTexture.needsUpdate = true; + const dotMaterial = new THREE.SpriteMaterial({ + map: dotTexture, + transparent: true, + opacity: 1.0, + depthTest: false + }); + lockedDotSprite = new THREE.Sprite(dotMaterial); + lockedDotSprite.position.copy(position); + lockedDotSprite.scale.set(4 * cameraDistance / 200, 4 * cameraDistance / 200, 1); + + earthObjRef.add(lockedDotSprite); } else { if (hoverRingSprite) { earthObjRef.remove(hoverRingSprite); @@ -417,6 +475,23 @@ export function showHoverRing(position, isLocked = false) { return sprite; } +function createBrighterDotCanvas() { + const size = DOT_TEXTURE_SIZE * 2; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + const center = size / 2; + const gradient = ctx.createRadialGradient(center, center, 0, center, center, center); + gradient.addColorStop(0, 'rgba(255, 255, 200, 1)'); + gradient.addColorStop(0.3, 'rgba(255, 220, 100, 0.9)'); + gradient.addColorStop(0.7, 'rgba(255, 180, 50, 0.5)'); + gradient.addColorStop(1, 'rgba(255, 150, 0, 0)'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, size); + return canvas; +} + export function hideHoverRings() { if (!earthObjRef) return; @@ -427,20 +502,45 @@ export function hideHoverRings() { } export function hideLockedRing() { - if (!earthObjRef || !lockedRingSprite) return; - earthObjRef.remove(lockedRingSprite); - lockedRingSprite = null; + if (!earthObjRef) return; + if (lockedRingSprite) { + earthObjRef.remove(lockedRingSprite); + lockedRingSprite = null; + } + if (lockedDotSprite) { + earthObjRef.remove(lockedDotSprite); + lockedDotSprite = null; + } } export function updateLockedRingPosition(position) { + const ringSize = SATELLITE_CONFIG.ringSize; + const camera = window.camera; + const cameraDistance = camera ? camera.position.distanceTo(position) : 400; if (lockedRingSprite && position) { lockedRingSprite.position.copy(position); + const breathScale = 1 + Math.sin(breathingPhase) * SATELLITE_CONFIG.breathingScaleAmplitude; + lockedRingSprite.scale.set(ringSize * breathScale, ringSize * breathScale, 1); + const breathOpacity = SATELLITE_CONFIG.breathingOpacityMin + Math.sin(breathingPhase) * (SATELLITE_CONFIG.breathingOpacityMax - SATELLITE_CONFIG.breathingOpacityMin); + lockedRingSprite.material.opacity = breathOpacity; + } + if (lockedDotSprite && position) { + lockedDotSprite.position.copy(position); + const dotBreathScale = 1 + Math.sin(breathingPhase) * SATELLITE_CONFIG.dotBreathingScaleAmplitude; + lockedDotSprite.scale.set(4 * cameraDistance / 200 * dotBreathScale, 4 * cameraDistance / 200 * dotBreathScale, 1); + lockedDotSprite.material.opacity = SATELLITE_CONFIG.dotOpacityMin + Math.sin(breathingPhase) * (SATELLITE_CONFIG.dotOpacityMax - SATELLITE_CONFIG.dotOpacityMin); } } export function updateHoverRingPosition(position) { + const ringSize = SATELLITE_CONFIG.ringSize; + const camera = window.camera; + const cameraDistance = camera ? camera.position.distanceTo(position) : 400; + const scale = ringSize; if (hoverRingSprite && position) { hoverRingSprite.position.copy(position); + hoverRingSprite.scale.set(scale, scale, 1); + console.log(`[Hover update] ringSize: ${ringSize}, camDist: ${cameraDistance}, scale: ${scale}`); } } @@ -512,16 +612,8 @@ export function showPredictedOrbit(satellite) { const props = satellite.properties; const meanMotion = props?.mean_motion || 15; const periodSeconds = calculateOrbitalPeriod(meanMotion); - console.log('[DEBUG] meanMotion:', meanMotion, 'periodSeconds:', periodSeconds); - - // Test current time - const now = new Date(); - const testPos = computeSatellitePosition(satellite, now); - console.log('[DEBUG] testPos (now):', testPos); const points = calculatePredictedOrbit(satellite, periodSeconds); - console.log('[DEBUG] points.length:', points.length); - if (points.length < 2) return; const positions = new Float32Array(points.length * 3);