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);