diff --git a/public/css/style.css b/public/css/style.css index 4968383..9922c69 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -2257,6 +2257,12 @@ body.moonphase-page.has-modal { color: #c8ffc0; } +.moon-hd-btn.is-active { + background: rgba(201, 168, 76, 0.18); + border-color: var(--gold); + color: var(--gold-pale); +} + .moon-phase-name { font-family: 'Orbitron', sans-serif; font-size: 1.15rem; diff --git a/public/images/moons/moon_16384.jpg b/public/images/moons/moon_16384.jpg new file mode 100644 index 0000000..3239af0 Binary files /dev/null and b/public/images/moons/moon_16384.jpg differ diff --git a/public/images/moons/moon_displacement_16b_low.jpg b/public/images/moons/moon_displacement_16b_low.jpg new file mode 100644 index 0000000..de5f581 Binary files /dev/null and b/public/images/moons/moon_displacement_16b_low.jpg differ diff --git a/public/images/moons/moon_displacement_16bit.jpg b/public/images/moons/moon_displacement_16bit.jpg index de5f581..74573c1 100644 Binary files a/public/images/moons/moon_displacement_16bit.jpg and b/public/images/moons/moon_displacement_16bit.jpg differ diff --git a/public/moonphase.php b/public/moonphase.php index 6c09307..d4cff2d 100644 --- a/public/moonphase.php +++ b/public/moonphase.php @@ -166,6 +166,7 @@ try { >PNG export + @@ -328,6 +329,7 @@ const nextDayBtn = document.getElementById('nextDayBtn'); const nowBtn = document.getElementById('nowBtn'); const resetViewBtn = document.getElementById('resetViewBtn'); const freeCameraBtn = document.getElementById('freeCameraBtn'); +const hdTextureBtn = document.getElementById('hdTextureBtn'); const playPauseBtn = document.getElementById('playPauseBtn'); const playbackSpeedGroup = document.getElementById('playbackSpeedGroup'); const exportMoonBtn = document.getElementById('exportMoonBtn'); @@ -358,6 +360,8 @@ let moonPlaybackSpeed = 1; let isMoonExportRunning = false; let moonExportRestoreFocus = null; let freeCameraMode = false; +let hdTexturesLoaded = false; +let hdTexturesLoading = false; let cameraOrbitTheta = 0; let cameraOrbitPhi = Math.PI / 2; let moonDragStartOrbitTheta = 0; @@ -369,8 +373,10 @@ const craterOverlayScale = 0.940; const SYNODIC_MONTH = 29.530588853; const KNOWN_NEW_MOON_JD = 2451550.1; const MOON_AXIS_INCLINATION = 1.54242; -const MOON_TEXTURE_PATH = 'images/moons/moon_8192.jpg'; -const MOON_DISPLACEMENT_MAP_PATH = 'images/moons/moon_displacement_16bit.jpg'; +const MOON_TEXTURE_PATH = 'images/moons/moon_4096.jpg'; +const MOON_DISPLACEMENT_MAP_PATH = 'images/moons/moon_displacement_16b_low.jpg'; +const MOON_TEXTURE_PATH_HD = 'images/moons/moon_16384.jpg'; +const MOON_DISPLACEMENT_MAP_PATH_HD = 'images/moons/moon_displacement_16bit.jpg'; const MOON_TEXTURE_BASE_ROTATION_Y = -Math.PI / 2; const MOON_SPHERE_RADIUS = 1.6; const MOON_PROJECTED_RADIUS_SCALE = 0.962; @@ -378,8 +384,8 @@ const MOON_DISPLACEMENT_SCALE = 0.0095; const MOON_DISPLACEMENT_BIAS = -0.1; const MOON_BUMP_SCALE = 2.0; const MOON_CAMERA_BASE_Z = 6.8; -const MOON_ZOOM_MIN = 0.8; -const MOON_ZOOM_MAX = 3.5; +const MOON_ZOOM_MIN = 0.2; +const MOON_ZOOM_MAX = 4.1; const MOON_PAN_LIMIT = 1.8; const MOON_PLAYBACK_STEP_MINUTES = 5; const MOON_PLAYBACK_INTERVAL_MS = 180; @@ -521,6 +527,100 @@ function resetMoonView() { updateMoon(); } +async function fetchWithProgress(url, onProgress) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const contentLength = Number(response.headers.get('Content-Length') || 0); + const reader = response.body.getReader(); + const chunks = []; + let received = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + chunks.push(value); + received += value.length; + if (contentLength > 0) { + onProgress(received / contentLength); + } + } + + return URL.createObjectURL(new Blob(chunks)); +} + +async function loadHdTextures() { + if (hdTexturesLoaded || hdTexturesLoading || !moonSceneState) { + return; + } + + hdTexturesLoading = true; + hdTextureBtn.disabled = true; + moonLoading.classList.remove('is-hidden', 'is-error'); + setMoonLoadingProgress(0, 'HD-Texturen werden geladen... 0 %'); + + const textureLoader = new THREE.TextureLoader(); + const objectUrls = []; + + const loadTexture = (objectUrl, colorSpace) => new Promise((resolve, reject) => { + textureLoader.load(objectUrl, tex => { + tex.colorSpace = colorSpace; + tex.anisotropy = moonSceneState.renderer.capabilities.getMaxAnisotropy(); + resolve(tex); + }, undefined, reject); + }); + + try { + let prog1 = 0; + let prog2 = 0; + + const updateProgress = () => { + const combined = (prog1 + prog2) / 2; + setMoonLoadingProgress(combined, `HD-Texturen werden geladen... ${Math.round(combined * 100)} %`); + }; + + const [url1, url2] = await Promise.all([ + fetchWithProgress(MOON_TEXTURE_PATH_HD, p => { prog1 = p; updateProgress(); }), + fetchWithProgress(MOON_DISPLACEMENT_MAP_PATH_HD, p => { prog2 = p; updateProgress(); }) + ]); + + objectUrls.push(url1, url2); + setMoonLoadingProgress(1, 'HD-Texturen werden verarbeitet...'); + + const [hdTex, hdDisp] = await Promise.all([ + loadTexture(url1, THREE.SRGBColorSpace), + loadTexture(url2, THREE.NoColorSpace) + ]); + + moonSceneState.moonMesh.material.map = hdTex; + moonSceneState.moonMesh.material.displacementMap = hdDisp; + moonSceneState.moonMesh.material.bumpMap = hdDisp; + moonSceneState.moonMesh.material.needsUpdate = true; + + hdTexturesLoaded = true; + hdTexturesLoading = false; + hdTextureBtn.textContent = 'HD!'; + hdTextureBtn.classList.add('is-active'); + hdTextureBtn.disabled = true; + + moonLoadingText.textContent = 'HD-Texturen aktiv'; + window.setTimeout(hideMoonLoading, 600); + updateMoon(); + } catch (error) { + console.error('HD-Textur-Ladefehler:', error); + hdTexturesLoading = false; + hdTextureBtn.disabled = false; + hdTextureBtn.textContent = 'HD fehlgeschl.'; + hideMoonLoading(); + } finally { + objectUrls.forEach(u => URL.revokeObjectURL(u)); + } +} + function syncDatePickerWithDate(date) { const timezoneOffset = date.getTimezoneOffset() * 60000; datePicker.value = new Date(date.getTime() - timezoneOffset).toISOString().slice(0, 10); @@ -712,127 +812,134 @@ async function fetchMoonPhaseData(date) { return payload; } -function ensureMoonScene() { - if (moonSceneState || typeof THREE === 'undefined') { +let moonSceneInitPromise = null; + +async function ensureMoonScene() { + if (moonSceneState) { return moonSceneState; } - let loadedMoonAssets = 0; - let moonTextureFailed = false; + if (moonSceneInitPromise) { + return moonSceneInitPromise; + } + + moonSceneInitPromise = (async () => { + moonLoading.classList.remove('is-hidden', 'is-error'); + setMoonLoadingProgress(0, 'Lade Mondtexturen... 0 %'); + + const objectUrls = []; + + try { + let prog1 = 0; + let prog2 = 0; + + const updateProgress = () => { + const combined = (prog1 + prog2) / 2; + setMoonLoadingProgress(combined, `Lade Mondtexturen... ${Math.round(combined * 100)} %`); + }; + + const [url1, url2] = await Promise.all([ + fetchWithProgress(MOON_TEXTURE_PATH, p => { prog1 = p; updateProgress(); }), + fetchWithProgress(MOON_DISPLACEMENT_MAP_PATH, p => { prog2 = p; updateProgress(); }) + ]); + + objectUrls.push(url1, url2); + setMoonLoadingProgress(1, 'Mondtexturen werden verarbeitet...'); + + const renderer = new THREE.WebGLRenderer({ + canvas: moonCanvas, + antialias: true, + alpha: false, + powerPreference: 'high-performance' + }); + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.setClearColor(0x010102, 1); + renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 3)); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100); + camera.position.set(0, 0, MOON_CAMERA_BASE_Z); + + const ambientLight = new THREE.AmbientLight(0x1b2230, 0.004); + scene.add(ambientLight); + + const sunLight = new THREE.DirectionalLight(0xffffff, 2.9); + sunLight.castShadow = true; + sunLight.shadow.mapSize.width = 4096; + sunLight.shadow.mapSize.height = 4096; + sunLight.shadow.bias = -0.00015; + sunLight.shadow.normalBias = 0.02; + sunLight.shadow.camera.near = 1; + sunLight.shadow.camera.far = 20; + sunLight.shadow.camera.left = -3; + sunLight.shadow.camera.right = 3; + sunLight.shadow.camera.top = 3; + sunLight.shadow.camera.bottom = -3; + scene.add(sunLight); + + const rimLight = new THREE.DirectionalLight(0x89a8ff, 0.015); + rimLight.position.set(-2.5, 1.2, 3.2); + scene.add(rimLight); + + const textureLoader = new THREE.TextureLoader(); + const maxAnisotropy = renderer.capabilities.getMaxAnisotropy(); + + const loadTex = (url, colorSpace) => new Promise((resolve, reject) => { + textureLoader.load(url, tex => { + tex.colorSpace = colorSpace; + tex.anisotropy = maxAnisotropy; + resolve(tex); + }, undefined, reject); + }); + + const [texture, displacementMap] = await Promise.all([ + loadTex(url1, THREE.SRGBColorSpace), + loadTex(url2, THREE.NoColorSpace) + ]); + + const geometry = new THREE.SphereGeometry(1.6, 256, 256); + const material = new THREE.MeshStandardMaterial({ + map: texture, + displacementMap, + displacementScale: MOON_DISPLACEMENT_SCALE, + displacementBias: MOON_DISPLACEMENT_BIAS, + bumpMap: displacementMap, + bumpScale: MOON_BUMP_SCALE, + roughness: 1, + metalness: 0 + }); + const moonMesh = new THREE.Mesh(geometry, material); + moonMesh.castShadow = true; + moonMesh.receiveShadow = true; + moonMesh.rotation.y = MOON_TEXTURE_BASE_ROTATION_Y; + scene.add(moonMesh); + + moonSceneState = { renderer, scene, camera, moonMesh, sunLight, ambientLight, rimLight }; + + resizeMoonScene(); + applyMoonZoom(); + renderer.render(scene, camera); - const handleMoonAssetLoaded = () => { - loadedMoonAssets += 1; - setMoonLoadingProgress(loadedMoonAssets / MOON_TEXTURE_ASSET_COUNT); - if (loadedMoonAssets >= MOON_TEXTURE_ASSET_COUNT && !moonTextureFailed) { moonLoadingText.textContent = 'Mondtexturen geladen'; window.setTimeout(hideMoonLoading, 350); + + return moonSceneState; + } catch (error) { + console.error('Textur-Ladefehler:', error); + moonLoading.classList.remove('is-hidden'); + moonLoading.classList.add('is-error'); + moonLoadingFill.style.width = '100%'; + moonLoadingText.textContent = 'Texturen konnten nicht vollstaendig geladen werden'; + moonSceneInitPromise = null; + return null; + } finally { + objectUrls.forEach(u => URL.revokeObjectURL(u)); } - }; + })(); - const handleMoonAssetError = () => { - moonTextureFailed = true; - moonLoading.classList.remove('is-hidden'); - moonLoading.classList.add('is-error'); - moonLoadingFill.style.width = '100%'; - moonLoadingText.textContent = 'Texturen konnten nicht vollstaendig geladen werden'; - }; - - moonLoading.classList.remove('is-hidden', 'is-error'); - setMoonLoadingProgress(0); - - const renderer = new THREE.WebGLRenderer({ - canvas: moonCanvas, - antialias: true, - alpha: false, - powerPreference: 'high-performance' - }); - renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.setClearColor(0x010102, 1); - renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 3)); - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; - - const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100); - camera.position.set(0, 0, MOON_CAMERA_BASE_Z); - - const ambientLight = new THREE.AmbientLight(0x1b2230, 0.004); - scene.add(ambientLight); - - const sunLight = new THREE.DirectionalLight(0xffffff, 2.9); - sunLight.castShadow = true; - sunLight.shadow.mapSize.width = 4096; - sunLight.shadow.mapSize.height = 4096; - sunLight.shadow.bias = -0.00015; - sunLight.shadow.normalBias = 0.02; - sunLight.shadow.camera.near = 1; - sunLight.shadow.camera.far = 20; - sunLight.shadow.camera.left = -3; - sunLight.shadow.camera.right = 3; - sunLight.shadow.camera.top = 3; - sunLight.shadow.camera.bottom = -3; - scene.add(sunLight); - - const rimLight = new THREE.DirectionalLight(0x89a8ff, 0.015); - rimLight.position.set(-2.5, 1.2, 3.2); - scene.add(rimLight); - - const textureLoader = new THREE.TextureLoader(); - const texture = textureLoader.load( - MOON_TEXTURE_PATH, - () => { - handleMoonAssetLoaded(); - resizeMoonScene(); - renderer.render(scene, camera); - }, - undefined, - handleMoonAssetError - ); - texture.colorSpace = THREE.SRGBColorSpace; - texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); - const displacementMap = textureLoader.load( - MOON_DISPLACEMENT_MAP_PATH, - () => { - handleMoonAssetLoaded(); - resizeMoonScene(); - renderer.render(scene, camera); - }, - undefined, - handleMoonAssetError - ); - displacementMap.colorSpace = THREE.NoColorSpace; - displacementMap.anisotropy = renderer.capabilities.getMaxAnisotropy(); - - const geometry = new THREE.SphereGeometry(1.6, 256, 256); - const material = new THREE.MeshStandardMaterial({ - map: texture, - displacementMap, - displacementScale: MOON_DISPLACEMENT_SCALE, - displacementBias: MOON_DISPLACEMENT_BIAS, - bumpMap: displacementMap, - bumpScale: MOON_BUMP_SCALE, - roughness: 1, - metalness: 0 - }); - const moonMesh = new THREE.Mesh(geometry, material); - moonMesh.castShadow = true; - moonMesh.receiveShadow = true; - moonMesh.rotation.y = MOON_TEXTURE_BASE_ROTATION_Y; - scene.add(moonMesh); - - moonSceneState = { - renderer, - scene, - camera, - moonMesh, - sunLight, - ambientLight, - rimLight - }; - - resizeMoonScene(); - applyMoonZoom(); - return moonSceneState; + return moonSceneInitPromise; } function resizeMoonScene() { @@ -847,8 +954,8 @@ function resizeMoonScene() { moonSceneState.camera.updateProjectionMatrix(); } -function renderMoon3D(phaseData, libration) { - const state = ensureMoonScene(); +async function renderMoon3D(phaseData, libration) { + const state = await ensureMoonScene(); if (!state) { return false; } @@ -1034,7 +1141,7 @@ async function renderMoon2DToBlob(phaseData, size, transparentBackground = false } async function renderMoon3DToBlob(phaseData, libration, size, transparentBackground = false) { - const state = ensureMoonScene(); + const state = await ensureMoonScene(); if (!state) { return null; } @@ -1129,8 +1236,8 @@ window.openMoonExportDialog = openMoonExportDialog; window.closeMoonExportDialog = closeMoonExportDialog; window.exportMoonPng = exportMoonPng; -function renderMoon(phaseData, libration) { - if (renderMoon3D(phaseData, libration)) { +async function renderMoon(phaseData, libration) { + if (await renderMoon3D(phaseData, libration)) { renderMoonCraterOverlay(phaseData, libration); return; } @@ -1449,7 +1556,7 @@ async function updateMoon() { const phaseData = moonData.phase; const libration = moonData.libration; - renderMoon(phaseData, libration); + await renderMoon(phaseData, libration); phaseName.textContent = phaseData.label; illuminationValue.textContent = (phaseData.illumination * 100).toFixed(1).replace('.', ',') + ' %'; @@ -1565,6 +1672,7 @@ nowBtn.addEventListener('click', () => { updateMoon(); }); resetViewBtn.addEventListener('click', resetMoonView); +hdTextureBtn.addEventListener('click', loadHdTextures); freeCameraBtn.addEventListener('click', () => { freeCameraMode = !freeCameraMode; freeCameraBtn.classList.toggle('is-active', freeCameraMode);