// avatars.js — Verwaltung der fremden Avatare: Erzeugen, Namensschild, Interpolation. import * as THREE from 'three'; const BODY_GEO = new THREE.CapsuleGeometry(0.35, 0.9, 4, 8); const NOSE_GEO = new THREE.ConeGeometry(0.12, 0.25, 8); // zeigt Blickrichtung export class Avatars { constructor(scene) { this.scene = scene; this.map = new Map(); // id -> Eintrag } get count() { return this.map.size; } add(user) { if (this.map.has(user.id)) return; const group = new THREE.Group(); const mat = new THREE.MeshStandardMaterial({ color: colorFor(user.id), roughness: 0.6 }); const body = new THREE.Mesh(BODY_GEO, mat); body.position.y = 1.05; body.castShadow = true; group.add(body); const nose = new THREE.Mesh(NOSE_GEO, mat); nose.rotation.x = Math.PI / 2; nose.position.set(0, 1.3, -0.4); group.add(nose); const entry = { group, mat, name: user.name, statusLabel: user.label || '', help: !!user.help, label: null, target: { p: [...user.p], rot: user.rot || 0 }, cur: { p: [...user.p], rot: user.rot || 0 }, }; entry.label = makeLabel(labelText(entry), entry.help); group.add(entry.label); group.position.set(user.p[0], user.p[1], user.p[2]); group.rotation.y = user.rot || 0; this.scene.add(group); this.map.set(user.id, entry); } remove(id) { const e = this.map.get(id); if (!e) return; this.scene.remove(e.group); e.label.material.map.dispose(); this.map.delete(id); } setTarget(id, p, rot) { const e = this.map.get(id); if (e) { e.target.p = p; e.target.rot = rot; } } setStatus(id, label) { const e = this.map.get(id); if (e) { e.statusLabel = label; this._refresh(e); } } setHelp(id, on) { const e = this.map.get(id); if (e) { e.help = on; this._refresh(e); } } /** Pro Frame: fremde Avatare weich zur letzten bekannten Zielposition interpolieren. */ update(dt) { const a = 1 - Math.pow(0.001, dt); // framerate-unabhängiges exponentielles Glätten for (const e of this.map.values()) { e.cur.p[0] += (e.target.p[0] - e.cur.p[0]) * a; e.cur.p[1] += (e.target.p[1] - e.cur.p[1]) * a; e.cur.p[2] += (e.target.p[2] - e.cur.p[2]) * a; let d = e.target.rot - e.cur.rot; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; e.cur.rot += d * a; e.group.position.set(e.cur.p[0], e.cur.p[1], e.cur.p[2]); e.group.rotation.y = e.cur.rot; } } _refresh(e) { e.group.remove(e.label); e.label.material.map.dispose(); e.label = makeLabel(labelText(e), e.help); e.group.add(e.label); } } // ── Hilfsfunktionen ── function labelText(e) { return e.name + (e.statusLabel ? ' · ' + e.statusLabel : ''); } function colorFor(id) { const c = new THREE.Color(); c.setHSL((id * 0.13) % 1, 0.55, 0.62); return c; } function makeLabel(text, help) { const cv = document.createElement('canvas'); cv.width = 256; cv.height = 72; const g = cv.getContext('2d'); g.fillStyle = help ? 'rgba(243,139,168,.92)' : 'rgba(17,23,41,.82)'; roundRect(g, 4, 4, 248, 64, 12); g.fill(); g.strokeStyle = help ? '#ffd7de' : 'rgba(137,180,250,.6)'; g.lineWidth = 2; g.stroke(); g.fillStyle = help ? '#3a1420' : '#cdd6f4'; g.font = 'bold 30px system-ui, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText((help ? '✋ ' : '') + text, 128, 38); const tex = new THREE.CanvasTexture(cv); tex.anisotropy = 4; const spr = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false })); spr.scale.set(1.8, 0.5, 1); spr.position.y = 2.35; return spr; } function roundRect(g, x, y, w, h, r) { g.beginPath(); g.moveTo(x + r, y); g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r); g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath(); }