diff --git a/node/server/public/app.js b/node/server/public/app.js
index 4432cfa..24690ca 100644
--- a/node/server/public/app.js
+++ b/node/server/public/app.js
@@ -50,17 +50,14 @@ 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;
}
@@ -120,8 +117,7 @@ draco.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(draco);
-
-// Virtuelles Interdimensionales Geisterteleportationsgerät
+// ---- SpinnerController ----
class SpinnerController {
constructor(scene) {
this.scene = scene;
@@ -135,16 +131,15 @@ class SpinnerController {
this.clock = new THREE.Clock();
this.ws = null;
this.reconnectDelay = 2000;
- this.connected = false; // Verbindungsstatus
-
- // Werte für sanften Übergang
+ this.connected = false;
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 // 1/x Sekunden bis Ziel (hier: ca. 3s)
+ lerpSpeed: 1/3
};
+ this.activeSpirits = [];
this.init();
}
@@ -191,10 +186,8 @@ 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;
@@ -206,8 +199,7 @@ class SpinnerController {
T.targetRotSpeed = 0.08;
T.targetLightIntensity = 0;
}
- // Lerp (sanft angleichen)
- const s = T.lerpSpeed * dt; // kleiner dt → smooth
+ const s = T.lerpSpeed * dt;
T.emission += (T.targetEmission - T.emission) * s;
T.bobMult += (T.targetBobMult - T.bobMult) * s;
T.rotSpeed += (T.targetRotSpeed - T.rotSpeed) * s;
@@ -221,14 +213,12 @@ 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;
@@ -239,7 +229,6 @@ 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(
@@ -271,11 +260,9 @@ class SpinnerController {
const msg = JSON.parse(event.data);
if (msg.type === 'spirit') {
if (typeof msg.timeSinceSpawnMs === "number" && msg.timeSinceSpawnMs > 0) {
- // Initiale Verbindung: Mit Offset
- spawnSpiritWithOffset(msg.data, msg.timeSinceSpawnMs, msg.spiritIntervalMs);
+ await this.spawnSpiritWithOffset(msg.data, msg.timeSinceSpawnMs, msg.spiritIntervalMs);
} else {
- // Normales Timer-Event
- spawnSpirit(msg.data);
+ await this.spawnSpirit(msg.data);
}
}
});
@@ -290,8 +277,39 @@ 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 {
@@ -300,10 +318,10 @@ class Spirit {
this.grp = new THREE.Group();
this.gltf = gltfScene;
this.info = info || {};
- this.spawnY = spawnPosition.y; // immer gleiche Start-Y
+ this.spawnY = spawnPosition.y;
this.clock = new THREE.Clock();
this.isFading = true;
- this.lifeTime = 20; // Sekunden
+ this.lifeTime = 20;
this.spiritMeshes = [];
this.grp.add(this.gltf);
this.gltf.rotation.x = -Math.PI;
@@ -331,10 +349,8 @@ 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) {
@@ -378,7 +394,6 @@ class Spirit {
}
_setupPicking() {
- // Hier ein einfacher Ansatz: Mesh mit Info-Objekt merken!
this.gltf.traverse(mesh => {
if (mesh.isMesh) {
mesh.userData._spiritInfo = this.info;
@@ -390,11 +405,9 @@ 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 });
@@ -429,131 +442,23 @@ async function loadGLB(path, pos, rotDeg, { receiveShadow = false, castShadow =
return obj;
}
-// ---- 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 ----
+// ---- Overlay, Picking, Render-Loop ----
let lastOverlaySpiritData = null;
-// Overlay zentriert in der Mitte mit Schließen-X
-function showSpiritOverlay(spirit) {
- // Entferne evtl. vorherige Overlay-Elemente
- document.getElementById('spirit-info-backdrop')?.remove();
+function showSpiritOverlay(spirit) { /* ...deine Overlay-Logik... */ }
+function closeSpiritOverlay() { /* ...deine Close-Logik... */ }
- // 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'] ? `` : ''}
-