Erste lauffähige Fassung: 3D-Raum, Echtzeit-Multiplayer (WebSocket), Python-IDE via Pyodide im Browser, eigener Arbeitsplatz mit Live-Code-Monitor. Modularer, self-hostbarer Aufbau (Three.js + Node/Express + ws), ohne CDN- Abhängigkeit zur Laufzeit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
3.8 KiB
JavaScript
110 lines
3.8 KiB
JavaScript
// 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();
|
|
}
|