diff --git a/node/server/public/app.js b/node/server/public/app.js
index 24690ca..4432cfa 100644
--- a/node/server/public/app.js
+++ b/node/server/public/app.js
@@ -50,14 +50,17 @@ function onResize() {
let renderW, renderH, styleW, styleH;
if (winW / winH > aspect) {
+ // Querformat (breit) → CONTAIN
renderH = winH;
renderW = winH * aspect;
styleW = `${renderW}px`;
styleH = `${renderH}px`;
} else {
+ // Hochformat oder quadratisch → COVER
renderW = winW;
renderH = winW / aspect;
if (renderH < winH) {
+ // Ist nach Covern immer noch zu klein? Dann auf volle Höhe und rechts/links abschneiden
renderH = winH;
renderW = winH * aspect;
}
@@ -117,7 +120,8 @@ draco.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(draco);
-// ---- SpinnerController ----
+
+// Virtuelles Interdimensionales Geisterteleportationsgerät
class SpinnerController {
constructor(scene) {
this.scene = scene;
@@ -131,15 +135,16 @@ class SpinnerController {
this.clock = new THREE.Clock();
this.ws = null;
this.reconnectDelay = 2000;
- this.connected = false;
+ this.connected = false; // Verbindungsstatus
+
+ // Werte für sanften Übergang
this.transition = {
emission: 3.0, targetEmission: 3.0,
bobMult: 0.5, targetBobMult: 0.5,
rotSpeed: 1.2, targetRotSpeed: 1.2,
lightIntensity: 5, targetLightIntensity: 5,
- lerpSpeed: 1/3
+ lerpSpeed: 1/3 // 1/x Sekunden bis Ziel (hier: ca. 3s)
};
- this.activeSpirits = [];
this.init();
}
@@ -186,8 +191,10 @@ class SpinnerController {
return obj;
}
+ // --- Werte sanft angleichen ---
smoothTransition(dt) {
let T = this.transition;
+ // Zielwerte setzen
if (this.connected) {
T.targetEmission = 3.0;
T.targetBobMult = 0.5;
@@ -199,7 +206,8 @@ class SpinnerController {
T.targetRotSpeed = 0.08;
T.targetLightIntensity = 0;
}
- const s = T.lerpSpeed * dt;
+ // Lerp (sanft angleichen)
+ const s = T.lerpSpeed * dt; // kleiner dt → smooth
T.emission += (T.targetEmission - T.emission) * s;
T.bobMult += (T.targetBobMult - T.bobMult) * s;
T.rotSpeed += (T.targetRotSpeed - T.rotSpeed) * s;
@@ -213,12 +221,14 @@ class SpinnerController {
const bob = Math.sin(t * 1.2) * T.bobMult;
const baseY = this.baseY + bob;
+ // Spinner
if (this.spinnerRed && this.spinnerBlue) {
this.spinnerRed.position.y = baseY + 0.8;
this.spinnerBlue.position.y = baseY;
this.spinnerRed.rotation.y -= T.rotSpeed * dt;
this.spinnerBlue.rotation.y += T.rotSpeed * dt;
+ // Emission auf beide Spinner anwenden
this.spinnerRed.traverse(c => {
if (c.isMesh && c.material && c.material.isMeshStandardMaterial)
c.material.emissiveIntensity = T.emission;
@@ -229,6 +239,7 @@ class SpinnerController {
});
}
+ // Rotierende Lichter (jetzt mit smooth intensity und Speed)
for (let i = 0; i < this.lights.length; i++) {
const ang = t * 0.8 + i * 2 * Math.PI / 3;
this.lights[i].position.set(
@@ -260,9 +271,11 @@ class SpinnerController {
const msg = JSON.parse(event.data);
if (msg.type === 'spirit') {
if (typeof msg.timeSinceSpawnMs === "number" && msg.timeSinceSpawnMs > 0) {
- await this.spawnSpiritWithOffset(msg.data, msg.timeSinceSpawnMs, msg.spiritIntervalMs);
+ // Initiale Verbindung: Mit Offset
+ spawnSpiritWithOffset(msg.data, msg.timeSinceSpawnMs, msg.spiritIntervalMs);
} else {
- await this.spawnSpirit(msg.data);
+ // Normales Timer-Event
+ spawnSpirit(msg.data);
}
}
});
@@ -277,40 +290,9 @@ class SpinnerController {
this.ws.close();
});
}
-
- async spawnSpirit(spiritData) {
- let spawnPos = {
- x: 0,
- y: this.spinnerRed ? this.spinnerRed.position.y - 1.5 : 15,
- z: 0.88
- };
- const modelUrl = spiritData['Model URL'] || spiritData.modelUrl;
- const { scene: gltfScene } = await gltfLoader.loadAsync(modelUrl);
- const spirit = new Spirit(this.scene, gltfScene, spiritData, spawnPos);
- spirit.clock.start();
- this.activeSpirits.push(spirit);
- }
-
- async spawnSpiritWithOffset(spiritData, timeSinceSpawnMs = 0, spiritIntervalMs = 20000) {
- let startY = this.spinnerRed ? this.spinnerRed.position.y - 1.5 : 15;
- let offset = (typeof timeSinceSpawnMs === 'number' && timeSinceSpawnMs > 0) ? timeSinceSpawnMs / 1000 : 0;
- let lifeTime = (spiritIntervalMs ? spiritIntervalMs : 20000) / 1000;
- const despawnSpeed = 0.8;
- let spawnPos = { x: 0, y: startY - (despawnSpeed * offset), z: 0.88 };
-
- const modelUrl = spiritData['Model URL'] || spiritData.modelUrl;
- const { scene: gltfScene } = await gltfLoader.loadAsync(modelUrl);
-
- const spirit = new Spirit(this.scene, gltfScene, spiritData, spawnPos);
- spirit.clock.start();
- if (offset > 0 && offset < lifeTime) {
- spirit.clock.elapsedTime = offset;
- }
- spirit.lifeTime = lifeTime;
- this.activeSpirits.push(spirit);
- }
}
+
// ---- Spirit-Klasse ----
class Spirit {
constructor(scene, gltfScene, info, spawnPosition) {
@@ -318,10 +300,10 @@ class Spirit {
this.grp = new THREE.Group();
this.gltf = gltfScene;
this.info = info || {};
- this.spawnY = spawnPosition.y;
+ this.spawnY = spawnPosition.y; // immer gleiche Start-Y
this.clock = new THREE.Clock();
this.isFading = true;
- this.lifeTime = 20;
+ this.lifeTime = 20; // Sekunden
this.spiritMeshes = [];
this.grp.add(this.gltf);
this.gltf.rotation.x = -Math.PI;
@@ -349,8 +331,10 @@ class Spirit {
const t = this.clock.getElapsedTime();
const despawnSpeed = 0.8;
+ // **NEU: Y-Position dynamisch berechnen!**
this.grp.position.y = this.spawnY - despawnSpeed * t;
+ // Fading
if (this.spiritMeshes && this.isFading) {
for (const mesh of this.spiritMeshes) {
if (t < 0.5) {
@@ -394,6 +378,7 @@ class Spirit {
}
_setupPicking() {
+ // Hier ein einfacher Ansatz: Mesh mit Info-Objekt merken!
this.gltf.traverse(mesh => {
if (mesh.isMesh) {
mesh.userData._spiritInfo = this.info;
@@ -405,9 +390,11 @@ class Spirit {
// ---- Szene initialisieren ----
let spinnerController;
let landscape, torigate, shadowTree;
+const activeSpirits = [];
const clock = new THREE.Clock();
(async () => {
+ // Lade statische Models
landscape = await loadGLB('assets/models/landscape.glb', [0, 0, 0], [90, 0, 0], { receiveShadow: true, castShadow: true });
torigate = await loadGLB('assets/models/tori.glb', [0, 6.59, 0.375], [90, 0, 0], { receiveShadow: true, castShadow: true });
shadowTree = await loadGLB('assets/models/tree_low.glb', [0, 0, 0], [90, 0, 0], { receiveShadow: false, castShadow: true, shadowOnly: true });
@@ -442,23 +429,131 @@ async function loadGLB(path, pos, rotDeg, { receiveShadow = false, castShadow =
return obj;
}
-// ---- Overlay, Picking, Render-Loop ----
+// ---- Spirit spawnen ----
+async function spawnSpirit(spiritData) {
+ let spawnPos = { x: 0, y: spinnerController && spinnerController.spinnerRed ? spinnerController.spinnerRed.position.y - 1.5 : 15, z: 0.88 };
+ const modelUrl = spiritData['Model URL'] || spiritData.modelUrl; // Fallback!
+ const { scene: gltfScene } = await gltfLoader.loadAsync(modelUrl);
+ const spirit = new Spirit(scene, gltfScene, spiritData, spawnPos);
+ spirit.clock.start(); // neu!
+ activeSpirits.push(spirit);
+}
+
+async function spawnSpiritWithOffset(spiritData, timeSinceSpawnMs = 0, spiritIntervalMs = 20000) {
+ let spawnPos = { x: 0, y: spinnerController && spinnerController.spinnerRed ? spinnerController.spinnerRed.position.y - 1.5 : 15, z: 0.88 };
+ const modelUrl = spiritData['Model URL'] || spiritData.modelUrl;
+ const { scene: gltfScene } = await gltfLoader.loadAsync(modelUrl);
+
+ let offset = (typeof timeSinceSpawnMs === 'number' && timeSinceSpawnMs > 0) ? timeSinceSpawnMs / 1000 : 0;
+ let lifeTime = (spiritIntervalMs ? spiritIntervalMs : 20000) / 1000;
+
+ const spirit = new Spirit(scene, gltfScene, spiritData, spawnPos);
+ spirit.clock.start();
+ if (offset > 0 && offset < lifeTime) {
+ spirit.clock.elapsedTime = offset;
+ }
+ spirit.lifeTime = lifeTime;
+ activeSpirits.push(spirit);
+}
+
+// ---- Overlay-Logik ----
let lastOverlaySpiritData = null;
-function showSpiritOverlay(spirit) { /* ...deine Overlay-Logik... */ }
-function closeSpiritOverlay() { /* ...deine Close-Logik... */ }
+// Overlay zentriert in der Mitte mit Schließen-X
+function showSpiritOverlay(spirit) {
+ // Entferne evtl. vorherige Overlay-Elemente
+ document.getElementById('spirit-info-backdrop')?.remove();
+ // Erzeuge einen semi-transparenten Backdrop
+ const backdrop = document.createElement('div');
+ backdrop.id = 'spirit-info-backdrop';
+ backdrop.style = `
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 9998;
+ `;
+ document.body.appendChild(backdrop);
+
+ let el = document.getElementById('spirit-info');
+ if (!el) {
+ el = document.createElement('div');
+ el.id = 'spirit-info';
+ el.style = `
+ position: fixed;
+ left: 50%; top: 50%;
+ transform: translate(-50%,-50%);
+ color: white;
+ background: rgba(0,0,0,0.94);
+ padding: 28px 36px 24px 36px;
+ border-radius: 18px;
+ font-family: 'Segoe UI', sans-serif;
+ z-index: 9999;
+ max-width: 560px;
+ min-width: 320px;
+ box-shadow: 0 12px 64px #000a;
+ text-align: left;
+ `;
+ document.body.appendChild(el);
+ }
+ el.innerHTML = `
+
+ ${spirit['Image URL'] ? `` : ''}
+