privateboard 0.1.38 → 0.1.41
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 +327 -50
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +327 -50
- package/dist/cli.js.map +1 -1
- package/dist/server.js +201 -40
- 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 +1 -1
- package/public/__avatar3d_test.html +156 -0
- package/public/agent-overlay.css +14 -6
- package/public/agent-overlay.js +6 -6
- package/public/agent-profile.css +9 -12
- package/public/agent-profile.js +91 -38
- package/public/app.js +471 -528
- package/public/avatar-3d-snap.js +205 -0
- package/public/avatar-3d.js +836 -0
- package/public/avatar-customizer.html +274 -0
- package/public/avatar3d-editor.css +240 -0
- package/public/avatar3d-editor.js +484 -0
- package/public/avatars/3d/chair.png +0 -0
- package/public/avatars/3d/first-principles.png +0 -0
- package/public/avatars/3d/historian.png +0 -0
- package/public/avatars/3d/long-horizon.png +0 -0
- package/public/avatars/3d/phenomenologist.png +0 -0
- package/public/avatars/3d/socrates.png +0 -0
- package/public/avatars/3d/user-empathy.png +0 -0
- package/public/avatars/3d/value-investor.png +0 -0
- package/public/core-avatars.js +86 -0
- package/public/home-3d-loader.js +15 -4
- package/public/home-3d-mock.js +25 -14
- package/public/home.html +78 -16
- package/public/i18n.js +8 -4
- package/public/icons/avatar_1779855104027.glb +0 -0
- package/public/icons/logo.png +0 -0
- package/public/icons/new-style.glb +0 -0
- package/public/icons/new-style2.glb +0 -0
- package/public/icons/new-style3.glb +0 -0
- package/public/icons/new-style4.glb +0 -0
- package/public/icons/new-style5.glb +0 -0
- package/public/icons/new-style6.glb +0 -0
- package/public/icons/office.glb +0 -0
- package/public/icons/stuff.glb +0 -0
- package/public/index.html +169 -141
- package/public/magazine.html +1 -1
- package/public/new-agent.js +46 -20
- package/public/newspaper.html +1 -1
- package/public/office-viewer.html +340 -0
- package/public/ppt.html +1 -1
- package/public/stuff-viewer.html +330 -0
- package/public/thread.css +16 -15
- package/public/user-settings.css +7 -31
- package/public/user-settings.js +75 -89
- package/public/vendor/BufferGeometryUtils.js +1434 -0
- package/public/vendor/DRACOLoader.js +739 -0
- package/public/vendor/GLTFLoader.js +4860 -0
- package/public/vendor/RoomEnvironment.js +185 -0
- package/public/vendor/SkeletonUtils.js +496 -0
- package/public/vendor/draco/draco_decoder.js +34 -0
- package/public/vendor/draco/draco_decoder.wasm +0 -0
- package/public/vendor/draco/draco_encoder.js +33 -0
- package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
- package/public/vendor/meshopt_decoder.module.js +196 -0
- package/public/voice-3d-banner.js +19 -7
- package/public/voice-3d.js +1407 -432
- package/public/voice-replay.js +21 -0
- package/public/avatar-skill.js +0 -629
- package/public/avatars/chair-blink.svg +0 -1
- package/public/avatars/chair.svg +0 -1
- package/public/avatars/first-principles.svg +0 -1
- package/public/avatars/historian.svg +0 -1
- package/public/avatars/long-horizon.svg +0 -1
- package/public/avatars/phenomenologist.svg +0 -1
- package/public/avatars/socrates.svg +0 -1
- package/public/avatars/user-empathy.svg +0 -1
- package/public/avatars/value-investor.svg +0 -1
- package/public/icons/folded-sidebar.png +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh"><head><meta charset="utf-8"><title>捏 avatar · customizer</title>
|
|
3
|
+
<style>
|
|
4
|
+
html,body{margin:0;height:100%;background:#1c1e22;overflow:hidden;font-family:-apple-system,"PingFang SC",system-ui,sans-serif}
|
|
5
|
+
#status{position:fixed;top:8px;left:10px;color:#9fe;font:12px/1.5 monospace;z-index:9;white-space:pre-wrap;max-width:60vw}
|
|
6
|
+
#panel{position:fixed;top:0;right:0;bottom:0;width:260px;z-index:9;background:rgba(18,20,24,.92);border-left:1px solid #333;
|
|
7
|
+
padding:18px 16px;color:#dde;overflow-y:auto;backdrop-filter:blur(6px)}
|
|
8
|
+
#panel h2{font-size:13px;margin:0 0 14px;letter-spacing:.04em;color:#9fe;text-transform:uppercase}
|
|
9
|
+
.grp{margin-bottom:18px}
|
|
10
|
+
.grp .lab{font-size:11px;color:#9aa;text-transform:uppercase;letter-spacing:.08em;margin-bottom:7px}
|
|
11
|
+
.sw{display:flex;flex-wrap:wrap;gap:7px}
|
|
12
|
+
.sw button{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer;padding:0;outline:none;transition:transform .1s,border-color .1s}
|
|
13
|
+
.sw button:hover{transform:scale(1.12)}
|
|
14
|
+
.sw button.on{border-color:#9fe;box-shadow:0 0 0 2px rgba(153,238,255,.25)}
|
|
15
|
+
#sw-style button,#sw-hairstyle button,#sw-outfitstyle button,#sw-acc button{width:auto;height:auto;border-radius:6px;padding:6px 11px;font:11px/1 monospace;background:#283140;color:#cde;border:1px solid #44506a}
|
|
16
|
+
#sw-style button:hover,#sw-hairstyle button:hover,#sw-outfitstyle button:hover,#sw-acc button:hover{transform:none;background:#33405a}
|
|
17
|
+
#sw-style button.on,#sw-hairstyle button.on,#sw-outfitstyle button.on,#sw-acc button.on{color:#9fe;box-shadow:none}
|
|
18
|
+
.row{display:flex;align-items:center;gap:8px;font-size:12px;color:#cde;margin-bottom:14px}
|
|
19
|
+
.row input{accent-color:#9fe}
|
|
20
|
+
.btns{display:flex;gap:8px;margin-top:6px}
|
|
21
|
+
.btns button{flex:1;padding:7px;background:#283140;color:#cde;border:1px solid #44506a;border-radius:6px;cursor:pointer;font:12px/1 monospace}
|
|
22
|
+
.btns button:hover{background:#33405a}
|
|
23
|
+
#out{margin-top:12px;font:10px/1.5 monospace;color:#8aa;word-break:break-all}
|
|
24
|
+
</style>
|
|
25
|
+
</head><body>
|
|
26
|
+
<div id="status">booting…</div>
|
|
27
|
+
<div id="panel">
|
|
28
|
+
<h2>捏 avatar</h2>
|
|
29
|
+
<div class="grp"><div class="lab">风格 Style</div><div class="sw" id="sw-style"></div></div>
|
|
30
|
+
<div class="grp"><div class="lab">发型 Hair style</div><div class="sw" id="sw-hairstyle"></div></div>
|
|
31
|
+
<div class="grp"><div class="lab">衣服款式 Outfit style</div><div class="sw" id="sw-outfitstyle"></div></div>
|
|
32
|
+
<div class="grp"><div class="lab">装饰 Accessory</div><div class="sw" id="sw-acc"></div></div>
|
|
33
|
+
<div class="grp"><div class="lab">肤色 Skin</div><div class="sw" id="sw-skin"></div></div>
|
|
34
|
+
<div class="grp"><div class="lab">发色 Hair</div><div class="sw" id="sw-hair"></div></div>
|
|
35
|
+
<div class="grp"><div class="lab">眉色 Brow</div><div class="sw" id="sw-brow"></div></div>
|
|
36
|
+
<div class="grp"><div class="lab">衣色 Outfit color</div><div class="sw" id="sw-outfit"></div></div>
|
|
37
|
+
<div class="btns"><button id="rand">🎲 随机</button><button id="copy">复制配置</button></div>
|
|
38
|
+
<div id="out"></div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
window.__s = function (m, e) { var el = document.getElementById("status"); if (el) { el.textContent = m; el.style.color = e ? "#f88" : "#9fe"; } };
|
|
43
|
+
if (location.protocol === "file:") __s("⚠ 用 file:// 打开无法加载模块/GLB,请在运行中的 app 里用 http 访问。", true);
|
|
44
|
+
window.addEventListener("error", function (e) { __s("JS/资源错误: " + (e.message || (e.target && (e.target.src || e.target.href)) || "?"), true); }, true);
|
|
45
|
+
window.addEventListener("unhandledrejection", function (e) { __s("Promise 拒绝: " + ((e.reason && e.reason.message) || e.reason), true); });
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<script type="module">
|
|
49
|
+
const S = window.__s;
|
|
50
|
+
(async () => {
|
|
51
|
+
try {
|
|
52
|
+
S("加载 three…");
|
|
53
|
+
const THREE = await import("/vendor/three.module.min.js");
|
|
54
|
+
const { OrbitControls } = await import("/vendor/OrbitControls.js");
|
|
55
|
+
const { RoomEnvironment } = await import("/vendor/RoomEnvironment.js");
|
|
56
|
+
const av = await import("/avatar-3d.js");
|
|
57
|
+
|
|
58
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
59
|
+
const w = innerWidth - 260, h = innerHeight;
|
|
60
|
+
renderer.setSize(w, h);
|
|
61
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
62
|
+
renderer.shadowMap.enabled = true;
|
|
63
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
64
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
65
|
+
renderer.toneMappingExposure = 0.7; // tuned brightness
|
|
66
|
+
document.body.appendChild(renderer.domElement);
|
|
67
|
+
renderer.domElement.style.position = "fixed";
|
|
68
|
+
renderer.domElement.style.left = "0"; renderer.domElement.style.top = "0";
|
|
69
|
+
|
|
70
|
+
const scene = new THREE.Scene();
|
|
71
|
+
scene.background = new THREE.Color(0x1c1e22);
|
|
72
|
+
const camera = new THREE.PerspectiveCamera(32, w / h, 0.1, 100);
|
|
73
|
+
camera.position.set(0.7, 1.55, 2.6);
|
|
74
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
75
|
+
controls.target.set(0, 0.95, 0);
|
|
76
|
+
controls.enableDamping = true; controls.dampingFactor = 0.1;
|
|
77
|
+
controls.minDistance = 1.4; controls.maxDistance = 5;
|
|
78
|
+
controls.update();
|
|
79
|
+
|
|
80
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
81
|
+
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
82
|
+
scene.environmentIntensity = 0.35;
|
|
83
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x33384a, 0.18));
|
|
84
|
+
const key = new THREE.DirectionalLight(0xffffff, 0.7);
|
|
85
|
+
key.position.set(3, 6, 4); key.castShadow = true; scene.add(key);
|
|
86
|
+
const rim = new THREE.DirectionalLight(0xbfd4ff, 0.3);
|
|
87
|
+
rim.position.set(-4, 3, -3); scene.add(rim);
|
|
88
|
+
const ground = new THREE.Mesh(
|
|
89
|
+
new THREE.CircleGeometry(2.0, 48),
|
|
90
|
+
new THREE.MeshStandardMaterial({ color: 0x26282e, roughness: 1 }),
|
|
91
|
+
);
|
|
92
|
+
ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground);
|
|
93
|
+
|
|
94
|
+
window.addEventListener("resize", () => {
|
|
95
|
+
const W = innerWidth - 260, H = innerHeight;
|
|
96
|
+
renderer.setSize(W, H); camera.aspect = W / H; camera.updateProjectionMatrix();
|
|
97
|
+
});
|
|
98
|
+
renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
|
|
99
|
+
|
|
100
|
+
S("下载 + 解析 GLB(两个风格)…");
|
|
101
|
+
// Preload BOTH models so cross-model hair (e.g. classic body + 丸子头)
|
|
102
|
+
// is available without a wait when the user picks it.
|
|
103
|
+
await Promise.all(av.AVATAR_MODELS.map((m) => av.loadAvatar3D(m.id)));
|
|
104
|
+
|
|
105
|
+
const P = av.AVATAR_PALETTES;
|
|
106
|
+
const MODELS = av.AVATAR_MODELS;
|
|
107
|
+
const HAIRS = av.HAIR_STYLES;
|
|
108
|
+
const OUTFITS = av.OUTFIT_STYLES;
|
|
109
|
+
const ACCS = av.ACCESSORY_STYLES;
|
|
110
|
+
// hairStyle + outfitStyle + accStyle are INDEPENDENT dimensions
|
|
111
|
+
// (default to the starting body's own hair / clothing / accessory).
|
|
112
|
+
const sel = { model: "classic", hairStyle: "classic", outfitStyle: "classic", accStyle: "hat", skin: P.skin[1], hair: P.hair[4], brow: P.hair[4], outfit: P.outfit[0] };
|
|
113
|
+
const curModel = () => MODELS.find((m) => m.id === sel.model) || MODELS[0];
|
|
114
|
+
|
|
115
|
+
let group = null;
|
|
116
|
+
function rebuild() {
|
|
117
|
+
if (group) { scene.remove(group); group = null; }
|
|
118
|
+
group = av.buildAvatar3D("customizer", {
|
|
119
|
+
model: sel.model, hairStyle: sel.hairStyle, outfitStyle: sel.outfitStyle,
|
|
120
|
+
accessory: sel.accStyle, height: 1.7,
|
|
121
|
+
skin: sel.skin, hair: sel.hair, brow: sel.brow, outfit: sel.outfit,
|
|
122
|
+
});
|
|
123
|
+
scene.add(group);
|
|
124
|
+
}
|
|
125
|
+
rebuild();
|
|
126
|
+
|
|
127
|
+
// Colour swatch rows · click recolours the live instance.
|
|
128
|
+
function swatches(containerId, list, role) {
|
|
129
|
+
const c = document.getElementById(containerId);
|
|
130
|
+
c.innerHTML = "";
|
|
131
|
+
list.forEach((hex) => {
|
|
132
|
+
const b = document.createElement("button");
|
|
133
|
+
b.style.background = hex;
|
|
134
|
+
b.title = hex;
|
|
135
|
+
if (hex.toLowerCase() === String(sel[role]).toLowerCase()) b.classList.add("on");
|
|
136
|
+
b.addEventListener("click", () => {
|
|
137
|
+
sel[role] = hex;
|
|
138
|
+
av.recolorAvatar(group, { [role]: hex });
|
|
139
|
+
c.querySelectorAll("button").forEach((x) => x.classList.remove("on"));
|
|
140
|
+
b.classList.add("on");
|
|
141
|
+
updateOut();
|
|
142
|
+
});
|
|
143
|
+
c.appendChild(b);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function refreshSwatches() {
|
|
147
|
+
swatches("sw-skin", P.skin, "skin");
|
|
148
|
+
swatches("sw-hair", P.hair, "hair");
|
|
149
|
+
swatches("sw-brow", P.brow, "brow");
|
|
150
|
+
swatches("sw-outfit", P.outfit, "outfit");
|
|
151
|
+
}
|
|
152
|
+
refreshSwatches();
|
|
153
|
+
|
|
154
|
+
// Style buttons · switching loads that model's GLB (once) + rebuilds
|
|
155
|
+
// with the current colours so the look carries across styles.
|
|
156
|
+
function styleButtons() {
|
|
157
|
+
const c = document.getElementById("sw-style");
|
|
158
|
+
c.innerHTML = "";
|
|
159
|
+
MODELS.forEach((m) => {
|
|
160
|
+
const b = document.createElement("button");
|
|
161
|
+
b.textContent = m.label;
|
|
162
|
+
if (m.id === sel.model) b.classList.add("on");
|
|
163
|
+
b.addEventListener("click", async () => {
|
|
164
|
+
if (sel.model === m.id) return;
|
|
165
|
+
sel.model = m.id;
|
|
166
|
+
c.querySelectorAll("button").forEach((x) => x.classList.remove("on"));
|
|
167
|
+
b.classList.add("on");
|
|
168
|
+
S("加载风格 " + m.label + "…");
|
|
169
|
+
try { await av.loadAvatar3D(m.id); }
|
|
170
|
+
catch (err) { S("风格加载失败: " + ((err && err.message) || err), true); return; }
|
|
171
|
+
rebuild();
|
|
172
|
+
updateOut();
|
|
173
|
+
S("✓ " + m.label + "(拖动旋转)");
|
|
174
|
+
});
|
|
175
|
+
c.appendChild(b);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
styleButtons();
|
|
179
|
+
|
|
180
|
+
// Hair-style buttons · independent dimension (body keeps its style;
|
|
181
|
+
// only the hair swaps). Both models are preloaded so any combo is
|
|
182
|
+
// instant. A full rebuild is the simplest way to re-overlay hair.
|
|
183
|
+
function hairButtons() {
|
|
184
|
+
const c = document.getElementById("sw-hairstyle");
|
|
185
|
+
c.innerHTML = "";
|
|
186
|
+
HAIRS.forEach((hs) => {
|
|
187
|
+
const b = document.createElement("button");
|
|
188
|
+
b.textContent = hs.label;
|
|
189
|
+
if (hs.id === sel.hairStyle) b.classList.add("on");
|
|
190
|
+
b.addEventListener("click", () => {
|
|
191
|
+
if (sel.hairStyle === hs.id) return;
|
|
192
|
+
sel.hairStyle = hs.id;
|
|
193
|
+
c.querySelectorAll("button").forEach((x) => x.classList.remove("on"));
|
|
194
|
+
b.classList.add("on");
|
|
195
|
+
rebuild();
|
|
196
|
+
updateOut();
|
|
197
|
+
});
|
|
198
|
+
c.appendChild(b);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
hairButtons();
|
|
202
|
+
|
|
203
|
+
// Outfit-style buttons · independent dimension (body keeps its style;
|
|
204
|
+
// only the clothing swaps, overlaid from the chosen model). Colour is
|
|
205
|
+
// still applied on top via the 衣色 swatches.
|
|
206
|
+
function outfitStyleButtons() {
|
|
207
|
+
const c = document.getElementById("sw-outfitstyle");
|
|
208
|
+
c.innerHTML = "";
|
|
209
|
+
OUTFITS.forEach((os) => {
|
|
210
|
+
const b = document.createElement("button");
|
|
211
|
+
b.textContent = os.label;
|
|
212
|
+
if (os.id === sel.outfitStyle) b.classList.add("on");
|
|
213
|
+
b.addEventListener("click", () => {
|
|
214
|
+
if (sel.outfitStyle === os.id) return;
|
|
215
|
+
sel.outfitStyle = os.id;
|
|
216
|
+
c.querySelectorAll("button").forEach((x) => x.classList.remove("on"));
|
|
217
|
+
b.classList.add("on");
|
|
218
|
+
rebuild();
|
|
219
|
+
updateOut();
|
|
220
|
+
});
|
|
221
|
+
c.appendChild(b);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
outfitStyleButtons();
|
|
225
|
+
|
|
226
|
+
// Accessory buttons · independent dimension (无 / 眼镜 / 帽子). The
|
|
227
|
+
// accessory is overlaid from whichever model carries it, so e.g. the
|
|
228
|
+
// hatless body can wear glasses and vice-versa.
|
|
229
|
+
function accButtons() {
|
|
230
|
+
const c = document.getElementById("sw-acc");
|
|
231
|
+
c.innerHTML = "";
|
|
232
|
+
ACCS.forEach((a) => {
|
|
233
|
+
const b = document.createElement("button");
|
|
234
|
+
b.textContent = a.label;
|
|
235
|
+
if (a.id === sel.accStyle) b.classList.add("on");
|
|
236
|
+
b.addEventListener("click", () => {
|
|
237
|
+
if (sel.accStyle === a.id) return;
|
|
238
|
+
sel.accStyle = a.id;
|
|
239
|
+
c.querySelectorAll("button").forEach((x) => x.classList.remove("on"));
|
|
240
|
+
b.classList.add("on");
|
|
241
|
+
rebuild();
|
|
242
|
+
updateOut();
|
|
243
|
+
});
|
|
244
|
+
c.appendChild(b);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
accButtons();
|
|
248
|
+
|
|
249
|
+
document.getElementById("rand").addEventListener("click", () => {
|
|
250
|
+
const r = (a) => a[Math.floor(Math.random() * a.length)];
|
|
251
|
+
sel.skin = r(P.skin); sel.hair = r(P.hair); sel.brow = r(P.brow); sel.outfit = r(P.outfit);
|
|
252
|
+
av.recolorAvatar(group, sel);
|
|
253
|
+
refreshSwatches();
|
|
254
|
+
updateOut();
|
|
255
|
+
});
|
|
256
|
+
function updateOut() {
|
|
257
|
+
const t = JSON.stringify({ model: sel.model, hairStyle: sel.hairStyle, outfitStyle: sel.outfitStyle, accessory: sel.accStyle, skin: sel.skin, hair: sel.hair, brow: sel.brow, outfit: sel.outfit });
|
|
258
|
+
document.getElementById("out").textContent = t;
|
|
259
|
+
return t;
|
|
260
|
+
}
|
|
261
|
+
document.getElementById("copy").addEventListener("click", () => {
|
|
262
|
+
const t = updateOut();
|
|
263
|
+
if (navigator.clipboard) navigator.clipboard.writeText(t);
|
|
264
|
+
document.getElementById("copy").textContent = "已复制";
|
|
265
|
+
setTimeout(() => { document.getElementById("copy").textContent = "复制配置"; }, 1200);
|
|
266
|
+
});
|
|
267
|
+
updateOut();
|
|
268
|
+
S("✓ 就绪 · 点色板捏脸(拖动旋转)");
|
|
269
|
+
} catch (e) {
|
|
270
|
+
S("ERROR: " + (e && e.stack ? e.stack : (e && e.message) || e), true);
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
273
|
+
</script>
|
|
274
|
+
</body></html>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════
|
|
2
|
+
3D AVATAR EDITOR OVERLAY (捏 avatar)
|
|
3
|
+
Floating modal: left = three.js stage, right = control panel.
|
|
4
|
+
Mirrors the room-settings overlay chrome (warm-dark palette,
|
|
5
|
+
--lime accent, 0.5px hairlines, corner brackets). Opened from the
|
|
6
|
+
director profile's ⋯ menu via window.openAvatar3DEditor(slug).
|
|
7
|
+
═══════════════════════════════════════════ */
|
|
8
|
+
|
|
9
|
+
.avatar3d-overlay {
|
|
10
|
+
position: fixed;
|
|
11
|
+
inset: 0;
|
|
12
|
+
background: rgba(0, 0, 0, 0.78);
|
|
13
|
+
-webkit-backdrop-filter: blur(4px);
|
|
14
|
+
backdrop-filter: blur(4px);
|
|
15
|
+
z-index: 9600;
|
|
16
|
+
display: none;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
padding: 24px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
22
|
+
}
|
|
23
|
+
.avatar3d-overlay.open {
|
|
24
|
+
display: flex;
|
|
25
|
+
animation: av3d-fade 0.14s ease-out;
|
|
26
|
+
}
|
|
27
|
+
/* Electron macOS · reclaim the subtree as no-drag so clicks land (see the
|
|
28
|
+
same note in room-settings.css). */
|
|
29
|
+
html.is-electron-mac .avatar3d-overlay,
|
|
30
|
+
html.is-electron-mac .avatar3d-overlay * {
|
|
31
|
+
-webkit-app-region: no-drag;
|
|
32
|
+
}
|
|
33
|
+
@keyframes av3d-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
34
|
+
@keyframes av3d-rise { from { transform: translateY(10px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
35
|
+
|
|
36
|
+
.avatar3d-modal {
|
|
37
|
+
position: relative;
|
|
38
|
+
width: 100%;
|
|
39
|
+
max-width: 880px;
|
|
40
|
+
height: 560px;
|
|
41
|
+
max-height: calc(100vh - 48px);
|
|
42
|
+
background: var(--panel);
|
|
43
|
+
border: 0.5px solid var(--line-strong);
|
|
44
|
+
color: var(--text);
|
|
45
|
+
animation: av3d-rise 0.18s ease-out;
|
|
46
|
+
display: flex;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
}
|
|
49
|
+
/* Signature corner brackets · same gesture as the room-settings modal.
|
|
50
|
+
The modal intentionally has no `overflow: hidden` so the -1px
|
|
51
|
+
bracket offset isn't clipped; the stage + panel each clip their own
|
|
52
|
+
content. */
|
|
53
|
+
.avatar3d-modal::before,
|
|
54
|
+
.avatar3d-modal::after {
|
|
55
|
+
content: "";
|
|
56
|
+
position: absolute;
|
|
57
|
+
width: 10px;
|
|
58
|
+
height: 10px;
|
|
59
|
+
border: 1.5px solid var(--lime);
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
z-index: 2;
|
|
62
|
+
}
|
|
63
|
+
.avatar3d-modal::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
|
|
64
|
+
.avatar3d-modal::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
|
|
65
|
+
|
|
66
|
+
/* Left · three.js stage. The canvas is appended here and sized to fit. */
|
|
67
|
+
.av3d-stage {
|
|
68
|
+
position: relative;
|
|
69
|
+
flex: 1 1 auto;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
background:
|
|
72
|
+
radial-gradient(120% 90% at 50% 0%, var(--panel-3) 0%, var(--panel) 68%, var(--bg) 100%);
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
.av3d-stage canvas { display: block; }
|
|
76
|
+
.av3d-status {
|
|
77
|
+
position: absolute;
|
|
78
|
+
left: 14px;
|
|
79
|
+
bottom: 11px;
|
|
80
|
+
font-family: var(--mono);
|
|
81
|
+
font-size: 9px;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
letter-spacing: 0.14em;
|
|
84
|
+
text-transform: uppercase;
|
|
85
|
+
color: var(--text-faint);
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
}
|
|
88
|
+
.av3d-hint {
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: 11px;
|
|
91
|
+
left: 14px;
|
|
92
|
+
font-family: var(--mono);
|
|
93
|
+
font-size: 10px;
|
|
94
|
+
letter-spacing: 0.04em;
|
|
95
|
+
color: var(--text-dim);
|
|
96
|
+
pointer-events: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Right · control panel. */
|
|
100
|
+
.av3d-panel {
|
|
101
|
+
flex: 0 0 264px;
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
min-height: 0;
|
|
105
|
+
border-left: 0.5px solid var(--line-bright);
|
|
106
|
+
background: var(--panel-2);
|
|
107
|
+
}
|
|
108
|
+
.av3d-head {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
padding: 13px 16px 11px;
|
|
113
|
+
background: var(--strip-bg);
|
|
114
|
+
border-bottom: 0.5px dashed var(--line-bright);
|
|
115
|
+
}
|
|
116
|
+
.av3d-kicker {
|
|
117
|
+
font-family: var(--mono);
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
font-weight: 700;
|
|
120
|
+
letter-spacing: 0.16em;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
color: var(--lime);
|
|
123
|
+
margin: 0;
|
|
124
|
+
}
|
|
125
|
+
.av3d-kicker::before { content: "▸ "; }
|
|
126
|
+
.av3d-close {
|
|
127
|
+
appearance: none;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: 0.5px solid var(--line-bright);
|
|
130
|
+
color: var(--text-dim);
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
line-height: 1;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
padding: 3px 7px;
|
|
135
|
+
border-radius: 3px;
|
|
136
|
+
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
|
137
|
+
}
|
|
138
|
+
.av3d-close:hover { border-color: var(--lime); color: var(--lime); }
|
|
139
|
+
|
|
140
|
+
.av3d-body {
|
|
141
|
+
flex: 1 1 auto;
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
padding: 14px 16px;
|
|
144
|
+
}
|
|
145
|
+
.av3d-grp { margin-bottom: 16px; }
|
|
146
|
+
.av3d-lab {
|
|
147
|
+
font-family: var(--mono);
|
|
148
|
+
font-size: 9px;
|
|
149
|
+
font-weight: 700;
|
|
150
|
+
text-transform: uppercase;
|
|
151
|
+
letter-spacing: 0.16em;
|
|
152
|
+
color: var(--text-dim);
|
|
153
|
+
margin-bottom: 8px;
|
|
154
|
+
}
|
|
155
|
+
.av3d-sw { display: flex; flex-wrap: wrap; gap: 7px; }
|
|
156
|
+
|
|
157
|
+
/* Colour swatches · round chips. */
|
|
158
|
+
.av3d-sw.is-color button {
|
|
159
|
+
width: 26px;
|
|
160
|
+
height: 26px;
|
|
161
|
+
border-radius: 50%;
|
|
162
|
+
border: 2px solid transparent;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
padding: 0;
|
|
165
|
+
outline: none;
|
|
166
|
+
transition: transform 0.1s, border-color 0.1s, box-shadow 0.1s;
|
|
167
|
+
}
|
|
168
|
+
.av3d-sw.is-color button:hover { transform: scale(1.12); }
|
|
169
|
+
.av3d-sw.is-color button.on {
|
|
170
|
+
border-color: var(--lime);
|
|
171
|
+
box-shadow: 0 0 0 2px rgba(201, 164, 107, 0.3);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Style / hair / outfit / accessory pills · mirror the .rs-chip-mini
|
|
175
|
+
register (square hairline chip, ghost by default, lime-filled when
|
|
176
|
+
active). Body-size 12px since labels are descriptive, not meta. */
|
|
177
|
+
.av3d-sw.is-pill button {
|
|
178
|
+
appearance: none;
|
|
179
|
+
border-radius: 3px;
|
|
180
|
+
padding: 5px 10px;
|
|
181
|
+
font-family: var(--mono);
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
line-height: 1.15;
|
|
184
|
+
background: var(--bg);
|
|
185
|
+
color: var(--text-soft);
|
|
186
|
+
border: 0.5px solid var(--line-strong);
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
|
189
|
+
}
|
|
190
|
+
.av3d-sw.is-pill button:hover { border-color: var(--lime-dim); color: var(--lime); }
|
|
191
|
+
.av3d-sw.is-pill button.on {
|
|
192
|
+
background: var(--lime);
|
|
193
|
+
color: var(--bg);
|
|
194
|
+
border-color: var(--lime);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.av3d-foot {
|
|
198
|
+
display: flex;
|
|
199
|
+
gap: 8px;
|
|
200
|
+
padding: 10px 16px;
|
|
201
|
+
background: var(--panel-2);
|
|
202
|
+
border-top: 0.5px solid var(--line-bright);
|
|
203
|
+
}
|
|
204
|
+
.av3d-foot button {
|
|
205
|
+
flex: 1;
|
|
206
|
+
padding: 8px;
|
|
207
|
+
font-family: var(--mono);
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
font-weight: 700;
|
|
210
|
+
text-transform: uppercase;
|
|
211
|
+
letter-spacing: 0.12em;
|
|
212
|
+
border-radius: 3px;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
border: 0.5px solid var(--line-strong);
|
|
215
|
+
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
|
216
|
+
}
|
|
217
|
+
.av3d-btn-rand {
|
|
218
|
+
background: transparent;
|
|
219
|
+
color: var(--text-soft);
|
|
220
|
+
flex: 0 0 auto;
|
|
221
|
+
padding: 8px 11px;
|
|
222
|
+
letter-spacing: 0;
|
|
223
|
+
}
|
|
224
|
+
.av3d-btn-rand:hover { border-color: var(--lime); color: var(--lime); }
|
|
225
|
+
.av3d-btn-cancel { background: transparent; color: var(--text-soft); }
|
|
226
|
+
.av3d-btn-cancel:hover { border-color: var(--lime); color: var(--lime); }
|
|
227
|
+
.av3d-btn-save {
|
|
228
|
+
background: var(--lime);
|
|
229
|
+
color: var(--bg);
|
|
230
|
+
border-color: var(--lime);
|
|
231
|
+
}
|
|
232
|
+
.av3d-btn-save:hover { background: transparent; color: var(--lime); }
|
|
233
|
+
.av3d-btn-save[disabled] { opacity: 0.5; cursor: default; }
|
|
234
|
+
.av3d-btn-save[disabled]:hover { background: var(--lime); color: var(--bg); }
|
|
235
|
+
|
|
236
|
+
/* Narrow viewports · stack stage over panel. */
|
|
237
|
+
@media (max-width: 700px) {
|
|
238
|
+
.avatar3d-modal { flex-direction: column; height: calc(100vh - 48px); }
|
|
239
|
+
.av3d-panel { flex: 0 0 auto; border-left: 0; border-top: 0.5px solid var(--line-bright); }
|
|
240
|
+
}
|