privateboard 0.1.32 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot.js +482 -208
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +482 -208
- package/dist/cli.js.map +1 -1
- package/dist/server.js +482 -208
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -2
- package/public/adjourn-overlay.css +8 -0
- package/public/agent-profile.css +46 -0
- package/public/agent-profile.js +247 -48
- package/public/app.js +799 -717
- package/public/avatars/chair-blink.svg +1 -0
- package/public/home-3d-loader.js +6 -0
- package/public/home.html +1 -1
- package/public/i18n.js +122 -0
- package/public/icons/folded-sidebar.png +0 -0
- package/public/index.html +898 -990
- package/public/report.html +27 -7
- package/public/room-settings.css +18 -0
- package/public/themes.css +11 -0
- package/public/user-settings.js +37 -20
- package/public/voice-3d-banner.js +110 -36
- package/public/voice-3d.js +206 -6
- package/public/icons/share.png +0 -0
package/public/voice-3d.js
CHANGED
|
@@ -62,6 +62,20 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
62
62
|
// burn on low-end machines.
|
|
63
63
|
let intersectionObserver = null;
|
|
64
64
|
|
|
65
|
+
/* ── Loading overlay state ──────────────────────────────────
|
|
66
|
+
Shown synchronously inside mount() BEFORE WebGL init so the
|
|
67
|
+
user sees a "something is loading" cue instead of a black
|
|
68
|
+
box during the ~200-800 ms gap until the first frame paints
|
|
69
|
+
with avatar textures. Three surfaces benefit: marketing home
|
|
70
|
+
hero, in-app round-table on room open (mobile + desktop),
|
|
71
|
+
and the onboarding voice banner. Hidden on the first
|
|
72
|
+
`update()` call (which is when the populated scene is about
|
|
73
|
+
to paint) with a safety-net timeout in case `update()` is
|
|
74
|
+
never reached. */
|
|
75
|
+
let loadingEl = null;
|
|
76
|
+
let loadingHideTimer = 0;
|
|
77
|
+
let loadingFirstUpdateSeen = false;
|
|
78
|
+
|
|
65
79
|
/** Voxel pixel resolution · how many world units per "voxel cell".
|
|
66
80
|
* Drives the visual chunkiness — bigger value = chunkier blocks. */
|
|
67
81
|
const VOXEL = 0.18;
|
|
@@ -118,14 +132,29 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
118
132
|
* · brainstorm · sky-blue garden walls (open / outdoor)
|
|
119
133
|
* · constructive · graphite glass partitions (modern corporate)
|
|
120
134
|
* · research · light-oak library walls
|
|
121
|
-
* · debate ·
|
|
135
|
+
* · debate · warm oak wainscot (forum / chamber)
|
|
122
136
|
* · critique · mahogany executive panel
|
|
123
|
-
* Each entry: { wall, trim (baseboard), rail (chair-rail accent) }.
|
|
137
|
+
* Each entry: { wall, trim (baseboard), rail (chair-rail accent) }.
|
|
138
|
+
* NOTE · brainstorm / constructive / critique entries are LEGACY
|
|
139
|
+
* fallbacks · refreshWallColors swaps in procedural textures
|
|
140
|
+
* (red brick / mossy stone / sandstone) for those tones. The
|
|
141
|
+
* palette stays around so any client that hits the painted-
|
|
142
|
+
* material branch (texture build failure, future reduced-detail
|
|
143
|
+
* mode) still gets a coherent fallback colour. debate uses the
|
|
144
|
+
* flat painted-wall path · the bright warm-oak tone reads as a
|
|
145
|
+
* lit forum panel without needing a procedural texture. */
|
|
124
146
|
const WALL_PALETTE_BY_TONE = {
|
|
125
147
|
brainstorm: { wall: 0xA3B8C4, trim: 0x5F7A8A, rail: 0x7B97A8 },
|
|
126
148
|
constructive: { wall: 0x6E7480, trim: 0x42454D, rail: 0x575C66 },
|
|
127
149
|
research: { wall: 0xD9CBAD, trim: 0x8B7355, rail: 0xB29874 },
|
|
128
|
-
|
|
150
|
+
// Bumped from 0x4E3A2A · the prior tone read as nearly black under
|
|
151
|
+
// the scene's ambient + key lighting, making the debate room feel
|
|
152
|
+
// closed and lightless. 0x856444 is a clear step up the warm-oak
|
|
153
|
+
// ladder: still distinctly darker than research's light oak, but
|
|
154
|
+
// bright enough that walls + trim + rail all register as wood
|
|
155
|
+
// colour rather than silhouette. Trim and rail nudged in
|
|
156
|
+
// proportion so the contrast ratio between layers stays consistent.
|
|
157
|
+
debate: { wall: 0x856444, trim: 0x4A3422, rail: 0x6B4F36 },
|
|
129
158
|
critique: { wall: 0x6B3F2F, trim: 0x2A1612, rail: 0x4A2A20 },
|
|
130
159
|
};
|
|
131
160
|
|
|
@@ -212,6 +241,12 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
212
241
|
* the billboard / position-update loop. */
|
|
213
242
|
const projVec = (typeof THREE !== "undefined") ? new THREE.Vector3() : null;
|
|
214
243
|
|
|
244
|
+
/** Reduced-motion · honour the OS setting by holding the figure at
|
|
245
|
+
* full height (no squash-blink). Read once at load; matches the 2D
|
|
246
|
+
* path's `@media (prefers-reduced-motion: reduce)` blink opt-out. */
|
|
247
|
+
const prefersReducedMotion = (typeof matchMedia === "function")
|
|
248
|
+
&& matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
249
|
+
|
|
215
250
|
/** Picking · click anywhere on a director sprite to open the
|
|
216
251
|
* agent overlay (same modal the 2D head-cast avatar opens via
|
|
217
252
|
* document delegation). One Raycaster + one Vector2 reused across
|
|
@@ -274,6 +309,87 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
274
309
|
* features toward the camera. Sprite-head era needed this so the
|
|
275
310
|
* texture stayed visible; voxel head doesn't. */
|
|
276
311
|
|
|
312
|
+
/* ── Loading overlay ───────────────────────────────────────────
|
|
313
|
+
Painted into `host` synchronously from mount() so the user
|
|
314
|
+
sees a pulsing indicator instead of a black box while WebGL
|
|
315
|
+
warms up and avatar SVGs decode. Hidden when the first
|
|
316
|
+
`update()` call lands (scene is about to populate) with a
|
|
317
|
+
safety-net auto-hide at 2.5 s in case update() never fires
|
|
318
|
+
(e.g. caller mounts then immediately unmounts). */
|
|
319
|
+
function ensureLoadingStyle() {
|
|
320
|
+
if (document.getElementById("voice-3d-loading-style")) return;
|
|
321
|
+
const st = document.createElement("style");
|
|
322
|
+
st.id = "voice-3d-loading-style";
|
|
323
|
+
st.textContent = `
|
|
324
|
+
[data-rt-3d-loading] {
|
|
325
|
+
position: absolute; inset: 0; z-index: 3;
|
|
326
|
+
display: flex; align-items: center; justify-content: center;
|
|
327
|
+
background: rgba(0, 0, 0, 0.42);
|
|
328
|
+
pointer-events: none;
|
|
329
|
+
opacity: 1; transition: opacity 320ms ease;
|
|
330
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, "JetBrains Mono", monospace;
|
|
331
|
+
}
|
|
332
|
+
[data-rt-3d-loading].is-hiding { opacity: 0; }
|
|
333
|
+
[data-rt-3d-loading-inner] {
|
|
334
|
+
display: inline-flex; flex-direction: column; align-items: center; gap: 12px;
|
|
335
|
+
}
|
|
336
|
+
[data-rt-3d-loading-bar] {
|
|
337
|
+
display: inline-flex; gap: 5px; image-rendering: pixelated;
|
|
338
|
+
}
|
|
339
|
+
[data-rt-3d-loading-bar] i {
|
|
340
|
+
width: 8px; height: 8px;
|
|
341
|
+
background: rgba(255, 255, 255, 0.92);
|
|
342
|
+
display: inline-block;
|
|
343
|
+
animation: rt3d-load-pulse 1.05s ease-in-out infinite;
|
|
344
|
+
}
|
|
345
|
+
[data-rt-3d-loading-bar] i:nth-child(2) { animation-delay: 0.14s; }
|
|
346
|
+
[data-rt-3d-loading-bar] i:nth-child(3) { animation-delay: 0.28s; }
|
|
347
|
+
[data-rt-3d-loading-label] {
|
|
348
|
+
font-size: 10px;
|
|
349
|
+
letter-spacing: 0.22em;
|
|
350
|
+
text-transform: uppercase;
|
|
351
|
+
color: rgba(255, 255, 255, 0.78);
|
|
352
|
+
}
|
|
353
|
+
@keyframes rt3d-load-pulse {
|
|
354
|
+
0%, 70%, 100% { opacity: 0.22; transform: translateY(0); }
|
|
355
|
+
35% { opacity: 1; transform: translateY(-4px); }
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
document.head.appendChild(st);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function showLoadingOverlay(host, label) {
|
|
362
|
+
ensureLoadingStyle();
|
|
363
|
+
const el = document.createElement("div");
|
|
364
|
+
el.setAttribute("data-rt-3d-loading", "");
|
|
365
|
+
// Use textContent for the label · caller-supplied string, must
|
|
366
|
+
// not interpolate as HTML. The 8-bit dot trio is fixed markup.
|
|
367
|
+
const inner = document.createElement("span");
|
|
368
|
+
inner.setAttribute("data-rt-3d-loading-inner", "");
|
|
369
|
+
inner.innerHTML = `<span data-rt-3d-loading-bar><i></i><i></i><i></i></span>`;
|
|
370
|
+
if (label) {
|
|
371
|
+
const cap = document.createElement("span");
|
|
372
|
+
cap.setAttribute("data-rt-3d-loading-label", "");
|
|
373
|
+
cap.textContent = String(label);
|
|
374
|
+
inner.appendChild(cap);
|
|
375
|
+
}
|
|
376
|
+
el.appendChild(inner);
|
|
377
|
+
host.appendChild(el);
|
|
378
|
+
return el;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function hideLoadingOverlay() {
|
|
382
|
+
if (loadingHideTimer) {
|
|
383
|
+
clearTimeout(loadingHideTimer);
|
|
384
|
+
loadingHideTimer = 0;
|
|
385
|
+
}
|
|
386
|
+
const el = loadingEl;
|
|
387
|
+
loadingEl = null;
|
|
388
|
+
if (!el) return;
|
|
389
|
+
el.classList.add("is-hiding");
|
|
390
|
+
setTimeout(() => { try { el.remove(); } catch (_) {} }, 380);
|
|
391
|
+
}
|
|
392
|
+
|
|
277
393
|
/* ── Public API ─────────────────────────────────────────────── */
|
|
278
394
|
/** WebGL availability cache · the test creates a throw-away canvas
|
|
279
395
|
* + GL context, both of which count against Chrome's per-tab WebGL
|
|
@@ -321,6 +437,24 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
321
437
|
// 2D fallback path can take over instantly if we ever unmount.
|
|
322
438
|
stageEl.classList.add("is-3d");
|
|
323
439
|
|
|
440
|
+
// Loading overlay · drop in BEFORE we touch WebGL so the user
|
|
441
|
+
// never sees a black void during the shader-compile + avatar-
|
|
442
|
+
// fetch gap. Optional `opts.loadingLabel` lets callers stamp a
|
|
443
|
+
// context-appropriate caption (e.g. "Convening" on onboarding).
|
|
444
|
+
// Set `opts.loading: false` to suppress entirely · the marketing
|
|
445
|
+
// home uses this because the poster img already covers the gap
|
|
446
|
+
// (overlaying a darkening veil on the poster would just dim it).
|
|
447
|
+
loadingFirstUpdateSeen = false;
|
|
448
|
+
const wantsLoading = !(opts && opts.loading === false);
|
|
449
|
+
if (wantsLoading) {
|
|
450
|
+
const loadingLabel = (opts && typeof opts.loadingLabel === "string") ? opts.loadingLabel : "";
|
|
451
|
+
loadingEl = showLoadingOverlay(stageEl, loadingLabel);
|
|
452
|
+
// Safety net · if the caller mounts but never calls update()
|
|
453
|
+
// (e.g. they unmount in the same tick due to a race), the
|
|
454
|
+
// overlay would linger forever. Auto-hide after 2.5 s.
|
|
455
|
+
loadingHideTimer = setTimeout(() => hideLoadingOverlay(), 2500);
|
|
456
|
+
}
|
|
457
|
+
|
|
324
458
|
// DOM overlay layer · created BEFORE the canvas so the canvas
|
|
325
459
|
// ends up the first child of the stage (background). The overlay
|
|
326
460
|
// hosts every per-seat HTML element (nameplate, bubble) that we
|
|
@@ -570,6 +704,27 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
570
704
|
try { unmount(); } catch (_) {}
|
|
571
705
|
});
|
|
572
706
|
|
|
707
|
+
// Pre-warm the WebGL pipeline · the first WebGLRenderer created
|
|
708
|
+
// after an Electron / browser restart pays a one-time cost to
|
|
709
|
+
// initialise the GL context, compile shaders, and upload mesh /
|
|
710
|
+
// material buffers to the GPU. If the first rAF tick fires while
|
|
711
|
+
// any of that is still happening, `renderer.render()` silently
|
|
712
|
+
// produces an empty (pure black) frame and the user sees nothing
|
|
713
|
+
// until something nudges another render — which in practice means
|
|
714
|
+
// the user closes the overlay and reopens it. Forcing a synchronous
|
|
715
|
+
// `compile()` + `render()` here means the warm-up cost is paid
|
|
716
|
+
// BEFORE startRaf, and the very first rAF tick produces a real
|
|
717
|
+
// frame. Wrapped in try/catch so a WebGL hiccup doesn't abort
|
|
718
|
+
// mount · the rAF tick would retry the render anyway. The follow-
|
|
719
|
+
// up render produces output even when the scene has only the
|
|
720
|
+
// static chrome (walls / table / plants) because no chairs /
|
|
721
|
+
// avatars have been added yet · those land via the first
|
|
722
|
+
// VS3D.update() call after mount returns. */
|
|
723
|
+
try {
|
|
724
|
+
renderer.compile(scene, camera);
|
|
725
|
+
renderer.render(scene, camera);
|
|
726
|
+
} catch (_) { /* warm-up best-effort */ }
|
|
727
|
+
|
|
573
728
|
// RAF loop · animations + render each frame.
|
|
574
729
|
startRaf();
|
|
575
730
|
|
|
@@ -633,6 +788,20 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
633
788
|
}
|
|
634
789
|
|
|
635
790
|
function unmount() {
|
|
791
|
+
// Loading overlay cleanup · if we're unmounting before the
|
|
792
|
+
// overlay had a chance to fade, yank it now so it doesn't
|
|
793
|
+
// outlive the stage (the host's child list survives unmount
|
|
794
|
+
// for the 2D fallback path; a stranded overlay would sit on
|
|
795
|
+
// top of the 2D pixel art).
|
|
796
|
+
if (loadingHideTimer) {
|
|
797
|
+
clearTimeout(loadingHideTimer);
|
|
798
|
+
loadingHideTimer = 0;
|
|
799
|
+
}
|
|
800
|
+
if (loadingEl && loadingEl.parentNode) {
|
|
801
|
+
try { loadingEl.parentNode.removeChild(loadingEl); } catch (_) {}
|
|
802
|
+
}
|
|
803
|
+
loadingEl = null;
|
|
804
|
+
loadingFirstUpdateSeen = false;
|
|
636
805
|
stopRaf();
|
|
637
806
|
if (resizeObserver) {
|
|
638
807
|
try { resizeObserver.disconnect(); } catch (_) {}
|
|
@@ -744,6 +913,16 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
744
913
|
* }} state */
|
|
745
914
|
function update(state) {
|
|
746
915
|
if (!scene) return;
|
|
916
|
+
// First update after mount = scene is about to populate (seats,
|
|
917
|
+
// avatars, speaker indicator). Schedule the loading overlay to
|
|
918
|
+
// fade out shortly after so the first populated frame has a
|
|
919
|
+
// chance to paint underneath. 320 ms covers the CSS transition
|
|
920
|
+
// duration; the overlay sits on z-index 3 (above the canvas /
|
|
921
|
+
// DOM overlay) so it veils the scene crisply during the gap.
|
|
922
|
+
if (loadingEl && !loadingFirstUpdateSeen) {
|
|
923
|
+
loadingFirstUpdateSeen = true;
|
|
924
|
+
setTimeout(() => hideLoadingOverlay(), 320);
|
|
925
|
+
}
|
|
747
926
|
const mode = (state && state.mode) || "constructive";
|
|
748
927
|
rebuildFloor(mode);
|
|
749
928
|
refreshWallColors(mode);
|
|
@@ -2719,10 +2898,20 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
2719
2898
|
|
|
2720
2899
|
/** Per-frame animation tick for seats:
|
|
2721
2900
|
* · Non-speaker figures · gentle y-bob to look alive.
|
|
2722
|
-
* · Active speaker · figure pinned at base y (no bob)
|
|
2723
|
-
*
|
|
2724
|
-
*
|
|
2901
|
+
* · Active speaker · figure pinned at base y (no bob), with an
|
|
2902
|
+
* occasional quick scaleY squash that reads as a blink (the
|
|
2903
|
+
* billboard's eyes are baked into the texture, so a vertical
|
|
2904
|
+
* squash is the cheapest "alive while talking" cue); the floor
|
|
2905
|
+
* glow ring's opacity pulses too so the eye gets drawn to the
|
|
2906
|
+
* right seat.
|
|
2725
2907
|
* Cheap: one sin + a couple of property writes per seat. */
|
|
2908
|
+
// Squash-blink envelope · most of the period the figure is full
|
|
2909
|
+
// height; a short dip near the start of each period reads as a
|
|
2910
|
+
// blink. Per-seat phase offset (bobPhase) desyncs the cast so they
|
|
2911
|
+
// never blink in unison.
|
|
2912
|
+
const BLINK_PERIOD = 4.2; // seconds between blinks
|
|
2913
|
+
const BLINK_DUR = 0.18; // seconds the close+open takes
|
|
2914
|
+
const BLINK_DEPTH = 0.12; // peak scaleY reduction (1 → 0.88)
|
|
2726
2915
|
function tickSeatAnimations() {
|
|
2727
2916
|
if (!overlaySeats.length) return;
|
|
2728
2917
|
const t = (typeof performance !== "undefined" ? performance.now() : Date.now()) * 0.001;
|
|
@@ -2733,6 +2922,17 @@ import { OrbitControls } from "/vendor/OrbitControls.js";
|
|
|
2733
2922
|
seat.fig.position.y = isSpeaking
|
|
2734
2923
|
? 0
|
|
2735
2924
|
: Math.sin(t * 2.6 + seat.bobPhase) * 0.025;
|
|
2925
|
+
// Speaking squash-blink · scale the figure group's height
|
|
2926
|
+
// down briefly (anchored at the chair seat, so it compresses
|
|
2927
|
+
// toward the body). Non-speakers and reduced-motion hold at 1.
|
|
2928
|
+
let sy = 1;
|
|
2929
|
+
if (isSpeaking && !prefersReducedMotion) {
|
|
2930
|
+
const phase = (t + seat.bobPhase) % BLINK_PERIOD;
|
|
2931
|
+
if (phase < BLINK_DUR) {
|
|
2932
|
+
sy = 1 - BLINK_DEPTH * Math.sin((phase / BLINK_DUR) * Math.PI);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
if (seat.fig.scale.y !== sy) seat.fig.scale.y = sy;
|
|
2736
2936
|
}
|
|
2737
2937
|
// Speaker halo pulse · 0.35 ↔ 0.75 at ~0.8 Hz (matches the
|
|
2738
2938
|
// 2D `.rt-seat-speaking::before` glow rhythm).
|
package/public/icons/share.png
DELETED
|
Binary file
|