@firetable/project-xiaochun 0.1.13 → 0.1.15
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/README-CN.md +144 -18
- package/README.md +144 -18
- package/dist/avatar-element.d.ts +23 -6
- package/dist/chunks/{chunk-UB2HFUBS.js → chunk-KKRZ3SUD.js} +50 -2
- package/dist/chunks/chunk-KKRZ3SUD.js.map +7 -0
- package/dist/chunks/chunk-KQSKHPVF.js +957 -0
- package/dist/chunks/chunk-KQSKHPVF.js.map +7 -0
- package/dist/chunks/{chunk-NKR4JG3C.js → chunk-T6EHFEEZ.js} +83 -9
- package/dist/chunks/chunk-T6EHFEEZ.js.map +7 -0
- package/dist/client.d.ts +134 -6
- package/dist/element.cjs +572 -71
- package/dist/element.cjs.map +4 -4
- package/dist/element.js +4 -4
- package/dist/gesture-box.d.ts +72 -0
- package/dist/index.cjs +599 -71
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -4
- package/dist/index.js.map +1 -1
- package/dist/loader.global.js +2 -2
- package/dist/loader.global.js.map +4 -4
- package/dist/protocol.cjs +49 -1
- package/dist/protocol.cjs.map +2 -2
- package/dist/protocol.d.ts +180 -9
- package/dist/protocol.js +20 -2
- package/dist/react.cjs +540 -77
- package/dist/react.cjs.map +4 -4
- package/dist/react.d.ts +17 -1
- package/dist/react.js +50 -15
- package/dist/react.js.map +2 -2
- package/package.json +5 -3
- package/dist/chunks/chunk-NKR4JG3C.js.map +0 -7
- package/dist/chunks/chunk-UB2HFUBS.js.map +0 -7
- package/dist/chunks/chunk-XE2YKXRA.js +0 -544
- package/dist/chunks/chunk-XE2YKXRA.js.map +0 -7
package/dist/element.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @firetable/project-xiaochun v0.1.
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.15 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -30,6 +30,27 @@ module.exports = __toCommonJS(element_exports);
|
|
|
30
30
|
var XC_PROTOCOL_VERSION = 1;
|
|
31
31
|
var XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
|
|
32
32
|
var XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;
|
|
33
|
+
var XC_WINDOW_CORNER_RADIUS = 20;
|
|
34
|
+
var XC_UI_PARTS = ["chat", "bubble", "outfit", "scene"];
|
|
35
|
+
var XC_UI_PART_SET = new Set(XC_UI_PARTS);
|
|
36
|
+
var XC_UI_LEGACY_PARTS = ["chat", "bubble"];
|
|
37
|
+
function parseXcUiList(items) {
|
|
38
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39
|
+
const unknown = [];
|
|
40
|
+
for (const it of items) {
|
|
41
|
+
const name = typeof it === "string" ? it.trim().toLowerCase() : "";
|
|
42
|
+
if (XC_UI_PART_SET.has(name)) seen.add(name);
|
|
43
|
+
else unknown.push(typeof it === "string" ? it.slice(0, 40) : String(typeof it));
|
|
44
|
+
}
|
|
45
|
+
return { parts: XC_UI_PARTS.filter((p) => seen.has(p)), unknown, legacy: false };
|
|
46
|
+
}
|
|
47
|
+
function normalizeXcUiOption(v) {
|
|
48
|
+
if (v === true) return { parts: [...XC_UI_LEGACY_PARTS], unknown: [], legacy: true };
|
|
49
|
+
if (Array.isArray(v)) return parseXcUiList(v);
|
|
50
|
+
return { parts: [], unknown: [], legacy: false };
|
|
51
|
+
}
|
|
52
|
+
var XC_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
53
|
+
var isXcId = (v) => typeof v === "string" && XC_ID_RE.test(v);
|
|
33
54
|
function xcMessage(type, payload, id) {
|
|
34
55
|
const m = { type, v: XC_PROTOCOL_VERSION, payload };
|
|
35
56
|
if (id !== void 0) m.id = id;
|
|
@@ -51,6 +72,95 @@ function normalizeOrigin(input) {
|
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
|
|
75
|
+
// src/gesture-box.ts
|
|
76
|
+
var DEFAULT_RESIZE_LIMITS = Object.freeze({
|
|
77
|
+
minWidth: 120,
|
|
78
|
+
minHeight: 180,
|
|
79
|
+
maxWidth: Infinity,
|
|
80
|
+
maxHeight: Infinity
|
|
81
|
+
});
|
|
82
|
+
var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
83
|
+
function normalizeResizable(v) {
|
|
84
|
+
if (!v) return null;
|
|
85
|
+
const o = v === true ? {} : v;
|
|
86
|
+
const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
|
|
87
|
+
const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
|
|
88
|
+
const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
|
|
89
|
+
const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
|
|
90
|
+
return { minWidth, minHeight, maxWidth, maxHeight };
|
|
91
|
+
}
|
|
92
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
93
|
+
function moveBox(start, totalDx, totalDy, vp) {
|
|
94
|
+
return {
|
|
95
|
+
left: clamp(start.left + totalDx, 0, vp.width - start.width),
|
|
96
|
+
top: clamp(start.top + totalDy, 0, vp.height - start.height),
|
|
97
|
+
width: start.width,
|
|
98
|
+
height: start.height
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
|
|
102
|
+
const west = corner === "NW" || corner === "SW";
|
|
103
|
+
const north = corner === "NW" || corner === "NE";
|
|
104
|
+
const right = start.left + start.width, bottom = start.top + start.height;
|
|
105
|
+
const wantW = west ? start.width - totalDx : start.width + totalDx;
|
|
106
|
+
const wantH = north ? start.height - totalDy : start.height + totalDy;
|
|
107
|
+
const roomW = west ? right : vp.width - start.left;
|
|
108
|
+
const roomH = north ? bottom : vp.height - start.top;
|
|
109
|
+
const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
|
|
110
|
+
const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
|
|
111
|
+
return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
|
|
112
|
+
}
|
|
113
|
+
function fitBox(b, vp) {
|
|
114
|
+
const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
|
|
115
|
+
return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
|
|
116
|
+
}
|
|
117
|
+
var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
|
|
118
|
+
var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
|
|
119
|
+
var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
|
|
120
|
+
var MAX_GESTURE_DELTA = 2e4;
|
|
121
|
+
var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
|
|
122
|
+
var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
123
|
+
function parseGesturePayload(kind, raw) {
|
|
124
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
125
|
+
const p = raw;
|
|
126
|
+
if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
|
|
127
|
+
if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
|
|
128
|
+
if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
|
|
129
|
+
const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
|
|
130
|
+
if (kind === "resize") {
|
|
131
|
+
if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
|
|
132
|
+
out.corner = p.corner;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
var GestureGate = class {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.lastGesture = 0;
|
|
139
|
+
this.lastSeq = -1;
|
|
140
|
+
this.open = false;
|
|
141
|
+
}
|
|
142
|
+
get active() {
|
|
143
|
+
return this.open;
|
|
144
|
+
}
|
|
145
|
+
accept(p) {
|
|
146
|
+
if (p.phase === "start") {
|
|
147
|
+
if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
|
|
148
|
+
this.lastGesture = p.gesture;
|
|
149
|
+
this.lastSeq = 0;
|
|
150
|
+
this.open = true;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
|
|
154
|
+
this.lastSeq = p.seq;
|
|
155
|
+
if (p.phase === "end") this.open = false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
/** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
|
|
159
|
+
close() {
|
|
160
|
+
this.open = false;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
54
164
|
// src/client.ts
|
|
55
165
|
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
56
166
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 180"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f6b8ae"/><stop offset="1" stop-color="#ea8377"/></linearGradient></defs><ellipse cx="60" cy="170" rx="34" ry="6" fill="#000" opacity=".12"/><circle cx="60" cy="48" r="26" fill="url(#g)" opacity=".75"/><path d="M26 160c2-40 18-62 34-62s32 22 34 62z" fill="url(#g)" opacity=".6"/></svg>'
|
|
@@ -77,8 +187,46 @@ function createXiaochun(options) {
|
|
|
77
187
|
if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
|
|
78
188
|
trusted.add(n);
|
|
79
189
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
190
|
+
const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
|
|
191
|
+
const readPrefs = () => {
|
|
192
|
+
if (!prefsKey) return {};
|
|
193
|
+
try {
|
|
194
|
+
const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
|
|
195
|
+
const out = {};
|
|
196
|
+
if (raw && typeof raw === "object") {
|
|
197
|
+
if (isXcId(raw.outfit)) out.outfit = raw.outfit;
|
|
198
|
+
if (isXcId(raw.scene)) out.scene = raw.scene;
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
} catch {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const writePrefs = (patch) => {
|
|
206
|
+
if (!prefsKey) return;
|
|
207
|
+
try {
|
|
208
|
+
const next = { ...readPrefs() };
|
|
209
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
210
|
+
if (v) next[k] = v;
|
|
211
|
+
else delete next[k];
|
|
212
|
+
}
|
|
213
|
+
window.localStorage.setItem(prefsKey, JSON.stringify(next));
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const prefs = readPrefs();
|
|
218
|
+
if (options.model !== void 0 && options.outfit === void 0) {
|
|
219
|
+
console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
|
|
220
|
+
}
|
|
221
|
+
const uiParse = normalizeXcUiOption(options.ui);
|
|
222
|
+
const uiParts = uiParse.parts;
|
|
223
|
+
if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
|
|
224
|
+
if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
|
|
225
|
+
const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
|
|
226
|
+
const initialOutfit = outfitOpt ?? prefs.outfit;
|
|
227
|
+
const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
|
|
228
|
+
let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
|
|
229
|
+
let passthrough = options.passthrough ?? transparent;
|
|
82
230
|
const lazy = options.lazy ?? true;
|
|
83
231
|
const position = options.position ?? "inline";
|
|
84
232
|
const handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
@@ -91,9 +239,20 @@ function createXiaochun(options) {
|
|
|
91
239
|
ws.maxWidth = "100%";
|
|
92
240
|
ws.overflow = "visible";
|
|
93
241
|
ws.contain = "layout style";
|
|
94
|
-
ws.borderRadius = "var(--xc-radius, 0)";
|
|
95
242
|
ws.boxShadow = "var(--xc-shadow, none)";
|
|
96
|
-
|
|
243
|
+
const applyWrapperBg = () => {
|
|
244
|
+
ws.background = transparent ? "" : "var(--xc-bg, transparent)";
|
|
245
|
+
};
|
|
246
|
+
applyWrapperBg();
|
|
247
|
+
let radiusOpt = options.borderRadius;
|
|
248
|
+
const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
|
|
249
|
+
const applyRadius = () => {
|
|
250
|
+
const v = `var(--xc-radius, ${radiusValue()})`;
|
|
251
|
+
ws.borderRadius = v;
|
|
252
|
+
if (placeholderEl) placeholderEl.style.borderRadius = v;
|
|
253
|
+
if (iframe) iframe.style.borderRadius = v;
|
|
254
|
+
};
|
|
255
|
+
ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
97
256
|
if (position === "bottom-right") {
|
|
98
257
|
ws.right = "var(--xc-offset-x, 16px)";
|
|
99
258
|
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
@@ -120,7 +279,7 @@ function createXiaochun(options) {
|
|
|
120
279
|
}
|
|
121
280
|
const ps = placeholderEl.style;
|
|
122
281
|
placeholderEl.setAttribute("part", "placeholder");
|
|
123
|
-
ps.borderRadius =
|
|
282
|
+
ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
124
283
|
ps.position = "absolute";
|
|
125
284
|
ps.inset = "0";
|
|
126
285
|
ps.width = "100%";
|
|
@@ -157,25 +316,45 @@ function createXiaochun(options) {
|
|
|
157
316
|
let autoPaused = false;
|
|
158
317
|
let loaded = false;
|
|
159
318
|
let lastHit = false;
|
|
319
|
+
let caps = null;
|
|
320
|
+
let currentOutfit = null;
|
|
321
|
+
let currentScene = null;
|
|
160
322
|
const queue = [];
|
|
161
323
|
const pending = /* @__PURE__ */ new Map();
|
|
162
324
|
let idSeq = 0;
|
|
163
325
|
let handshakeTimer = null;
|
|
164
326
|
let io = null;
|
|
165
327
|
let idleHandle = null;
|
|
166
|
-
|
|
328
|
+
let resolveHandshake;
|
|
329
|
+
const handshaked = new Promise((res) => {
|
|
330
|
+
resolveHandshake = res;
|
|
331
|
+
});
|
|
332
|
+
const failLocal = (type, id, code, message) => {
|
|
333
|
+
emit("error", { code, message, command: type });
|
|
334
|
+
const p = id ? pending.get(id) : void 0;
|
|
335
|
+
if (p && id) {
|
|
336
|
+
pending.delete(id);
|
|
337
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const unsupportedByFrame = (type, id) => failLocal(type, id, "unsupported", `${type} is not supported by this /embed deployment (older version; its xc.ready has no capabilities for it)`);
|
|
341
|
+
const postToFrame = (type, payload, id, transfer, requires) => {
|
|
167
342
|
if (!port) {
|
|
168
|
-
queue.push({ type, payload, id, transfer });
|
|
343
|
+
queue.push({ type, payload, id, transfer, requires });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (requires && !caps?.[requires]) {
|
|
347
|
+
unsupportedByFrame(type, id);
|
|
169
348
|
return;
|
|
170
349
|
}
|
|
171
350
|
port.postMessage(xcMessage(type, payload, id), transfer ?? []);
|
|
172
351
|
};
|
|
173
|
-
const send = (type, payload, wait = false, transfer, fixedId) => {
|
|
352
|
+
const send = (type, payload, wait = false, transfer, fixedId, requires) => {
|
|
174
353
|
if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
175
354
|
if (!iframe) activate();
|
|
176
355
|
const id = fixedId ?? `c${++idSeq}`;
|
|
177
356
|
const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
|
|
178
|
-
postToFrame(type, payload, id, transfer);
|
|
357
|
+
postToFrame(type, payload, id, transfer, requires);
|
|
179
358
|
return p;
|
|
180
359
|
};
|
|
181
360
|
const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
|
|
@@ -225,7 +404,7 @@ function createXiaochun(options) {
|
|
|
225
404
|
let ended = false;
|
|
226
405
|
let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
|
|
227
406
|
let done = Promise.resolve();
|
|
228
|
-
const
|
|
407
|
+
const handle = {
|
|
229
408
|
write(data) {
|
|
230
409
|
if (ended) throw new Error("[project-xiaochun] audio stream already ended");
|
|
231
410
|
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
@@ -259,17 +438,18 @@ function createXiaochun(options) {
|
|
|
259
438
|
return done;
|
|
260
439
|
}
|
|
261
440
|
};
|
|
262
|
-
return
|
|
441
|
+
return handle;
|
|
263
442
|
};
|
|
264
443
|
function buildUrl() {
|
|
265
444
|
const u = new URL(srcUrl.href);
|
|
266
445
|
u.searchParams.set("host", window.location.origin);
|
|
267
446
|
if (transparent) u.searchParams.set("transparent", "1");
|
|
268
|
-
if (
|
|
447
|
+
if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
|
|
269
448
|
if (options.lang) u.searchParams.set("lang", options.lang);
|
|
270
449
|
if (options.heavy) u.searchParams.set("heavy", options.heavy);
|
|
271
|
-
if (options.controls) u.searchParams.set("controls", "
|
|
272
|
-
if (
|
|
450
|
+
if (options.controls === false) u.searchParams.set("controls", "0");
|
|
451
|
+
if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
|
|
452
|
+
if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
|
|
273
453
|
return u.href;
|
|
274
454
|
}
|
|
275
455
|
function activate() {
|
|
@@ -280,7 +460,7 @@ function createXiaochun(options) {
|
|
|
280
460
|
const f = document.createElement("iframe");
|
|
281
461
|
f.title = "Project XiaoChun";
|
|
282
462
|
f.loading = "lazy";
|
|
283
|
-
f.allow = "microphone; autoplay";
|
|
463
|
+
f.allow = options.crossOriginIsolated === true ? "microphone; autoplay; cross-origin-isolated" : "microphone; autoplay";
|
|
284
464
|
f.referrerPolicy = "strict-origin-when-cross-origin";
|
|
285
465
|
if (options.sandbox !== false) {
|
|
286
466
|
f.setAttribute("sandbox", options.sandbox ?? "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox");
|
|
@@ -295,7 +475,7 @@ function createXiaochun(options) {
|
|
|
295
475
|
s.border = "0";
|
|
296
476
|
s.background = "transparent";
|
|
297
477
|
s.colorScheme = "normal";
|
|
298
|
-
s.borderRadius =
|
|
478
|
+
s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
299
479
|
f.setAttribute("part", "iframe");
|
|
300
480
|
s.opacity = "0";
|
|
301
481
|
s.transition = "opacity .25s ease";
|
|
@@ -308,7 +488,7 @@ function createXiaochun(options) {
|
|
|
308
488
|
if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
|
|
309
489
|
}, handshakeTimeout);
|
|
310
490
|
setupAutoPause();
|
|
311
|
-
|
|
491
|
+
if (passthrough) enablePassthrough();
|
|
312
492
|
}
|
|
313
493
|
const onWindowMessage = (ev) => {
|
|
314
494
|
if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
|
|
@@ -321,20 +501,39 @@ function createXiaochun(options) {
|
|
|
321
501
|
const ch = new MessageChannel();
|
|
322
502
|
port = ch.port1;
|
|
323
503
|
port.onmessage = onPortMessage;
|
|
504
|
+
caps = data.payload?.capabilities ?? null;
|
|
505
|
+
const gWant = wantGestures();
|
|
506
|
+
sentGestures = gWant;
|
|
507
|
+
const initConfig = {
|
|
508
|
+
...options.allowCustomModel ? { allowCustomModel: true } : {},
|
|
509
|
+
...gWant.move || gWant.resize ? { gestures: gWant } : {}
|
|
510
|
+
};
|
|
324
511
|
iframe.contentWindow.postMessage(
|
|
325
|
-
xcMessage("xc.init", { hostOrigin: window.location.origin }),
|
|
512
|
+
xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
|
|
326
513
|
ev.origin,
|
|
327
514
|
[ch.port2]
|
|
328
515
|
);
|
|
516
|
+
warnGestureUnsupported();
|
|
329
517
|
if (handshakeTimer) {
|
|
330
518
|
clearTimeout(handshakeTimer);
|
|
331
519
|
handshakeTimer = null;
|
|
332
520
|
}
|
|
333
521
|
emit("handshake", data.payload);
|
|
334
|
-
|
|
522
|
+
resolveHandshake();
|
|
523
|
+
for (const m of queue.splice(0)) {
|
|
524
|
+
if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
|
|
525
|
+
else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
|
|
526
|
+
}
|
|
335
527
|
syncPause();
|
|
336
528
|
};
|
|
337
529
|
window.addEventListener("message", onWindowMessage);
|
|
530
|
+
function ack(id, value) {
|
|
531
|
+
const p = id ? pending.get(id) : void 0;
|
|
532
|
+
if (p && id) {
|
|
533
|
+
pending.delete(id);
|
|
534
|
+
p.resolve(value);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
338
537
|
function onPortMessage(ev) {
|
|
339
538
|
const data = ev.data;
|
|
340
539
|
if (!isXcEnvelope(data)) return;
|
|
@@ -345,6 +544,11 @@ function createXiaochun(options) {
|
|
|
345
544
|
case "xc.loaded":
|
|
346
545
|
if (!loaded) {
|
|
347
546
|
loaded = true;
|
|
547
|
+
if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
|
|
548
|
+
const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
|
|
549
|
+
void api.prefetch(ids).catch(() => {
|
|
550
|
+
});
|
|
551
|
+
}
|
|
348
552
|
if (iframe) iframe.style.opacity = "1";
|
|
349
553
|
if (placeholderEl) {
|
|
350
554
|
const el = placeholderEl;
|
|
@@ -357,6 +561,30 @@ function createXiaochun(options) {
|
|
|
357
561
|
}
|
|
358
562
|
emit("ready", data.payload);
|
|
359
563
|
break;
|
|
564
|
+
case "xc.outfit-changed": {
|
|
565
|
+
const pl = data.payload;
|
|
566
|
+
if (!pl.noop) {
|
|
567
|
+
currentOutfit = pl.id;
|
|
568
|
+
emit("outfit-changed", pl);
|
|
569
|
+
if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
|
|
570
|
+
}
|
|
571
|
+
ack(data.id);
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case "xc.scene-changed": {
|
|
575
|
+
const pl = data.payload;
|
|
576
|
+
currentScene = pl.id;
|
|
577
|
+
if (pl.transparent !== transparent) applyTransparency(pl.transparent);
|
|
578
|
+
if (!pl.noop) {
|
|
579
|
+
emit("scene-changed", pl);
|
|
580
|
+
if (!pl.initial) writePrefs({ scene: pl.id });
|
|
581
|
+
}
|
|
582
|
+
ack(data.id);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "xc.prefetched":
|
|
586
|
+
ack(data.id, data.payload);
|
|
587
|
+
break;
|
|
360
588
|
case "xc.state":
|
|
361
589
|
emit("state", data.payload);
|
|
362
590
|
break;
|
|
@@ -381,9 +609,16 @@ function createXiaochun(options) {
|
|
|
381
609
|
}
|
|
382
610
|
break;
|
|
383
611
|
}
|
|
612
|
+
case "xc.gesture-move":
|
|
613
|
+
onGesture("move", data.payload);
|
|
614
|
+
break;
|
|
615
|
+
case "xc.gesture-resize":
|
|
616
|
+
onGesture("resize", data.payload);
|
|
617
|
+
break;
|
|
384
618
|
case "xc.error": {
|
|
385
619
|
const { code, message, command } = data.payload;
|
|
386
620
|
emit("error", { code, message, command });
|
|
621
|
+
if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
|
|
387
622
|
const p = data.id ? pending.get(data.id) : void 0;
|
|
388
623
|
if (p && data.id) {
|
|
389
624
|
pending.delete(data.id);
|
|
@@ -395,8 +630,26 @@ function createXiaochun(options) {
|
|
|
395
630
|
break;
|
|
396
631
|
}
|
|
397
632
|
}
|
|
633
|
+
const POINTER_MIN_INTERVAL_MS = 32;
|
|
634
|
+
const POINTER_MIN_MOVE_PX = 2;
|
|
398
635
|
let pointerRaf = 0;
|
|
399
636
|
let lastPtr = null;
|
|
637
|
+
let lastPosted = null;
|
|
638
|
+
let rectCache = null;
|
|
639
|
+
let rectRo = null;
|
|
640
|
+
let rectAt = 0;
|
|
641
|
+
const invalidateRect = () => {
|
|
642
|
+
rectCache = null;
|
|
643
|
+
};
|
|
644
|
+
const getRect = () => {
|
|
645
|
+
if (!iframe) return null;
|
|
646
|
+
const now = performance.now();
|
|
647
|
+
if (!rectCache || now - rectAt > 400) {
|
|
648
|
+
rectCache = iframe.getBoundingClientRect();
|
|
649
|
+
rectAt = now;
|
|
650
|
+
}
|
|
651
|
+
return rectCache;
|
|
652
|
+
};
|
|
400
653
|
const onHostPointer = (e) => {
|
|
401
654
|
if (!iframe || !port || iframe.style.pointerEvents === "auto") return;
|
|
402
655
|
lastPtr = { x: e.clientX, y: e.clientY };
|
|
@@ -404,20 +657,63 @@ function createXiaochun(options) {
|
|
|
404
657
|
pointerRaf = requestAnimationFrame(() => {
|
|
405
658
|
pointerRaf = 0;
|
|
406
659
|
if (!iframe || !port || !lastPtr) return;
|
|
407
|
-
const r =
|
|
660
|
+
const r = getRect();
|
|
661
|
+
if (!r) return;
|
|
408
662
|
const x = lastPtr.x - r.left, y = lastPtr.y - r.top;
|
|
409
663
|
if (x < 0 || y < 0 || x > r.width || y > r.height) {
|
|
664
|
+
lastPosted = null;
|
|
410
665
|
if (lastHit) {
|
|
411
666
|
lastHit = false;
|
|
412
667
|
iframe.style.pointerEvents = "none";
|
|
413
668
|
}
|
|
414
669
|
return;
|
|
415
670
|
}
|
|
671
|
+
const now = performance.now();
|
|
672
|
+
if (lastPosted && now - lastPosted.t < POINTER_MIN_INTERVAL_MS) return;
|
|
673
|
+
if (lastPosted && Math.abs(x - lastPosted.x) < POINTER_MIN_MOVE_PX && Math.abs(y - lastPosted.y) < POINTER_MIN_MOVE_PX) return;
|
|
674
|
+
lastPosted = { x, y, t: now };
|
|
416
675
|
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
417
676
|
});
|
|
418
677
|
};
|
|
419
|
-
|
|
420
|
-
|
|
678
|
+
let passthroughActive = false;
|
|
679
|
+
function enablePassthrough() {
|
|
680
|
+
if (passthroughActive || !iframe) return;
|
|
681
|
+
passthroughActive = true;
|
|
682
|
+
window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
683
|
+
window.addEventListener("pointerdown", onHostPointer, { passive: true });
|
|
684
|
+
window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
|
|
685
|
+
window.addEventListener("resize", invalidateRect, { passive: true });
|
|
686
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
687
|
+
rectRo = new ResizeObserver(invalidateRect);
|
|
688
|
+
rectRo.observe(iframe);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function disablePassthrough() {
|
|
692
|
+
if (!passthroughActive) return;
|
|
693
|
+
passthroughActive = false;
|
|
694
|
+
window.removeEventListener("pointermove", onHostPointer);
|
|
695
|
+
window.removeEventListener("pointerdown", onHostPointer);
|
|
696
|
+
window.removeEventListener("scroll", invalidateRect, true);
|
|
697
|
+
window.removeEventListener("resize", invalidateRect);
|
|
698
|
+
rectRo?.disconnect();
|
|
699
|
+
rectRo = null;
|
|
700
|
+
if (pointerRaf) {
|
|
701
|
+
cancelAnimationFrame(pointerRaf);
|
|
702
|
+
pointerRaf = 0;
|
|
703
|
+
}
|
|
704
|
+
lastPtr = null;
|
|
705
|
+
}
|
|
706
|
+
function applyTransparency(next) {
|
|
707
|
+
transparent = next;
|
|
708
|
+
applyWrapperBg();
|
|
709
|
+
applyRadius();
|
|
710
|
+
passthrough = next ? options.passthrough ?? true : false;
|
|
711
|
+
lastHit = false;
|
|
712
|
+
lastPosted = null;
|
|
713
|
+
rectCache = null;
|
|
714
|
+
if (passthrough) enablePassthrough();
|
|
715
|
+
else disablePassthrough();
|
|
716
|
+
if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
|
|
421
717
|
}
|
|
422
718
|
let pauseIo = null;
|
|
423
719
|
function setupAutoPause() {
|
|
@@ -470,49 +766,123 @@ function createXiaochun(options) {
|
|
|
470
766
|
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
471
767
|
io.observe(wrapper);
|
|
472
768
|
}
|
|
473
|
-
let
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (!drag) return;
|
|
504
|
-
ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
|
|
505
|
-
ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
|
|
769
|
+
let draggableOn = options.draggable === true;
|
|
770
|
+
let resizeLimits = normalizeResizable(options.resizable);
|
|
771
|
+
const gates = { move: new GestureGate(), resize: new GestureGate() };
|
|
772
|
+
let activeGesture = null;
|
|
773
|
+
let inlineShift = { x: 0, y: 0 };
|
|
774
|
+
let userMoved = false;
|
|
775
|
+
let sentGestures = null;
|
|
776
|
+
let warnedGesture = false;
|
|
777
|
+
const viewport = () => ({
|
|
778
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
779
|
+
height: document.documentElement.clientHeight || window.innerHeight
|
|
780
|
+
});
|
|
781
|
+
const readBox = () => {
|
|
782
|
+
const r = wrapper.getBoundingClientRect();
|
|
783
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
784
|
+
};
|
|
785
|
+
const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
|
|
786
|
+
function applyBox(b, inlineBase, withSize) {
|
|
787
|
+
if (withSize) {
|
|
788
|
+
ws.width = `${b.width}px`;
|
|
789
|
+
ws.height = `${b.height}px`;
|
|
790
|
+
ws.maxWidth = "none";
|
|
791
|
+
}
|
|
792
|
+
if (position === "inline") {
|
|
793
|
+
const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
|
|
794
|
+
inlineShift = { x: b.left - base.left, y: b.top - base.top };
|
|
795
|
+
ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
|
|
796
|
+
} else {
|
|
797
|
+
ws.left = `${b.left}px`;
|
|
798
|
+
ws.top = `${b.top}px`;
|
|
506
799
|
ws.right = "auto";
|
|
507
800
|
ws.bottom = "auto";
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
801
|
+
}
|
|
802
|
+
userMoved = true;
|
|
803
|
+
invalidateRect();
|
|
804
|
+
}
|
|
805
|
+
let selectionLock = null;
|
|
806
|
+
function lockSelection(on) {
|
|
807
|
+
const de = document.documentElement;
|
|
808
|
+
if (on && !selectionLock) {
|
|
809
|
+
selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
|
|
810
|
+
de.style.setProperty("user-select", "none");
|
|
811
|
+
de.style.setProperty("-webkit-user-select", "none");
|
|
812
|
+
try {
|
|
813
|
+
window.getSelection()?.removeAllRanges();
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
} else if (!on && selectionLock) {
|
|
817
|
+
de.style.setProperty("user-select", selectionLock.us);
|
|
818
|
+
de.style.setProperty("-webkit-user-select", selectionLock.wus);
|
|
819
|
+
selectionLock = null;
|
|
820
|
+
}
|
|
515
821
|
}
|
|
822
|
+
function endActiveGesture(kind) {
|
|
823
|
+
if (!activeGesture || kind && activeGesture.kind !== kind) return;
|
|
824
|
+
gates[activeGesture.kind].close();
|
|
825
|
+
const b = readBox();
|
|
826
|
+
emit(activeGesture.kind, { phase: "end", ...b });
|
|
827
|
+
activeGesture = null;
|
|
828
|
+
lockSelection(false);
|
|
829
|
+
}
|
|
830
|
+
function onGesture(kind, raw) {
|
|
831
|
+
if (destroyed || !caps?.gestures) return;
|
|
832
|
+
if (kind === "move" ? !draggableOn : resizeLimits === null) return;
|
|
833
|
+
const p = parseGesturePayload(kind, raw);
|
|
834
|
+
if (!p) return;
|
|
835
|
+
if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
|
|
836
|
+
if (!gates[kind].accept(p)) return;
|
|
837
|
+
if (p.phase === "start") {
|
|
838
|
+
const start = readBox();
|
|
839
|
+
activeGesture = {
|
|
840
|
+
kind,
|
|
841
|
+
start,
|
|
842
|
+
corner: p.corner,
|
|
843
|
+
inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
|
|
844
|
+
};
|
|
845
|
+
lockSelection(true);
|
|
846
|
+
emit(kind, { phase: "start", ...start });
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const a = activeGesture;
|
|
850
|
+
const vp = viewport();
|
|
851
|
+
const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
|
|
852
|
+
applyBox(next, a.inlineBase, kind === "resize");
|
|
853
|
+
emit(kind, { phase: p.phase, ...next });
|
|
854
|
+
if (p.phase === "end") {
|
|
855
|
+
activeGesture = null;
|
|
856
|
+
lockSelection(false);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function syncGestures() {
|
|
860
|
+
if (destroyed) return;
|
|
861
|
+
const want = wantGestures();
|
|
862
|
+
if (!want.move) endActiveGesture("move");
|
|
863
|
+
if (!want.resize) endActiveGesture("resize");
|
|
864
|
+
if (!port || !caps) return;
|
|
865
|
+
warnGestureUnsupported();
|
|
866
|
+
if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
|
|
867
|
+
sentGestures = want;
|
|
868
|
+
if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
|
|
869
|
+
}
|
|
870
|
+
function warnGestureUnsupported() {
|
|
871
|
+
if (warnedGesture || !caps || caps.gestures) return;
|
|
872
|
+
const needMove = draggableOn;
|
|
873
|
+
if (!needMove && resizeLimits === null) return;
|
|
874
|
+
warnedGesture = true;
|
|
875
|
+
const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
|
|
876
|
+
emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
|
|
877
|
+
}
|
|
878
|
+
const onWindowResize = () => {
|
|
879
|
+
if (!userMoved || activeGesture || destroyed) return;
|
|
880
|
+
const cur = readBox();
|
|
881
|
+
const fit = fitBox(cur, viewport());
|
|
882
|
+
if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
|
|
883
|
+
applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
|
|
884
|
+
};
|
|
885
|
+
window.addEventListener("resize", onWindowResize, { passive: true });
|
|
516
886
|
scheduleLazy();
|
|
517
887
|
const api = {
|
|
518
888
|
element: wrapper,
|
|
@@ -527,8 +897,65 @@ function createXiaochun(options) {
|
|
|
527
897
|
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
528
898
|
expression: (name) => send("xc.expression", { name }),
|
|
529
899
|
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
530
|
-
|
|
900
|
+
setOutfit: (id) => {
|
|
901
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
|
|
902
|
+
return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
|
|
903
|
+
},
|
|
904
|
+
setScene: (id) => {
|
|
905
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
|
|
906
|
+
return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
|
|
907
|
+
},
|
|
908
|
+
prefetch: (ids) => {
|
|
909
|
+
if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
|
|
910
|
+
return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
|
|
911
|
+
}
|
|
912
|
+
return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
|
|
913
|
+
},
|
|
914
|
+
getOutfits: async () => {
|
|
915
|
+
if (!iframe) activate();
|
|
916
|
+
await handshaked;
|
|
917
|
+
return caps?.outfits ? caps.outfits.slice() : [];
|
|
918
|
+
},
|
|
919
|
+
getScenes: async () => {
|
|
920
|
+
if (!iframe) activate();
|
|
921
|
+
await handshaked;
|
|
922
|
+
return caps?.scenes ? caps.scenes.slice() : [];
|
|
923
|
+
},
|
|
924
|
+
get outfit() {
|
|
925
|
+
return currentOutfit;
|
|
926
|
+
},
|
|
927
|
+
get scene() {
|
|
928
|
+
return currentScene;
|
|
929
|
+
},
|
|
930
|
+
setModel: (m) => {
|
|
931
|
+
const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
|
|
932
|
+
if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
|
|
933
|
+
const message = "custom model URLs are disabled; pass allowCustomModel: true to createXiaochun() (only for URLs you trust) or use setOutfit() with a built-in id";
|
|
934
|
+
emit("error", { code: "unsupported", message, command: "xc.setModel" });
|
|
935
|
+
return Promise.reject(new Error(`[unsupported] ${message}`));
|
|
936
|
+
}
|
|
937
|
+
return send("xc.setModel", msg);
|
|
938
|
+
},
|
|
531
939
|
setConfig: (cfg) => send("xc.setConfig", cfg),
|
|
940
|
+
setSize(width, height) {
|
|
941
|
+
if (destroyed) return;
|
|
942
|
+
ws.width = css(width, "320px");
|
|
943
|
+
ws.height = css(height, "480px");
|
|
944
|
+
invalidateRect();
|
|
945
|
+
},
|
|
946
|
+
getBox: () => readBox(),
|
|
947
|
+
setBorderRadius(radius) {
|
|
948
|
+
radiusOpt = radius;
|
|
949
|
+
applyRadius();
|
|
950
|
+
},
|
|
951
|
+
setDraggable(on) {
|
|
952
|
+
draggableOn = on === true;
|
|
953
|
+
syncGestures();
|
|
954
|
+
},
|
|
955
|
+
setResizable(on) {
|
|
956
|
+
resizeLimits = normalizeResizable(on);
|
|
957
|
+
syncGestures();
|
|
958
|
+
},
|
|
532
959
|
startListening: () => send("xc.mic", { enabled: true }),
|
|
533
960
|
stopListening: () => send("xc.mic", { enabled: false }),
|
|
534
961
|
mic: (enabled) => send("xc.mic", { enabled }),
|
|
@@ -553,7 +980,10 @@ function createXiaochun(options) {
|
|
|
553
980
|
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
554
981
|
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
555
982
|
window.removeEventListener("message", onWindowMessage);
|
|
556
|
-
window.removeEventListener("
|
|
983
|
+
window.removeEventListener("resize", onWindowResize);
|
|
984
|
+
lockSelection(false);
|
|
985
|
+
disablePassthrough();
|
|
986
|
+
resolveHandshake();
|
|
557
987
|
try {
|
|
558
988
|
port?.close();
|
|
559
989
|
} catch {
|
|
@@ -586,11 +1016,17 @@ function createXiaochun(options) {
|
|
|
586
1016
|
// src/avatar-element.ts
|
|
587
1017
|
var OBSERVED = [
|
|
588
1018
|
"src",
|
|
1019
|
+
"outfit",
|
|
1020
|
+
"scene",
|
|
589
1021
|
"model",
|
|
590
1022
|
"lang",
|
|
591
1023
|
"mic",
|
|
592
1024
|
"transparent",
|
|
593
1025
|
"draggable",
|
|
1026
|
+
"resizable",
|
|
1027
|
+
"min-size",
|
|
1028
|
+
"max-size",
|
|
1029
|
+
"border-radius",
|
|
594
1030
|
"position",
|
|
595
1031
|
"size",
|
|
596
1032
|
"lazy",
|
|
@@ -599,14 +1035,37 @@ var OBSERVED = [
|
|
|
599
1035
|
"heavy",
|
|
600
1036
|
"ui",
|
|
601
1037
|
"controls",
|
|
602
|
-
"allowed-origins"
|
|
1038
|
+
"allowed-origins",
|
|
1039
|
+
"cross-origin-isolated",
|
|
1040
|
+
"persist",
|
|
1041
|
+
"allow-custom-model",
|
|
1042
|
+
"prefetch"
|
|
603
1043
|
];
|
|
604
|
-
var REBUILD = /* @__PURE__ */ new Set(["src", "
|
|
1044
|
+
var REBUILD = /* @__PURE__ */ new Set(["src", "position", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent", "cross-origin-isolated", "persist", "allow-custom-model", "prefetch"]);
|
|
1045
|
+
function radiusAttr(v) {
|
|
1046
|
+
if (v === null || v.trim() === "") return void 0;
|
|
1047
|
+
const t = v.trim();
|
|
1048
|
+
return /^\d+(\.\d+)?$/.test(t) ? Number(t) : t;
|
|
1049
|
+
}
|
|
605
1050
|
function boolAttr(el, name, dflt) {
|
|
606
1051
|
const v = el.getAttribute(name);
|
|
607
1052
|
if (v === null) return dflt;
|
|
608
1053
|
return v !== "false";
|
|
609
1054
|
}
|
|
1055
|
+
function prefetchAttr(v) {
|
|
1056
|
+
if (v === null || v === "false") return false;
|
|
1057
|
+
if (v === "" || v === "true") return true;
|
|
1058
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1059
|
+
}
|
|
1060
|
+
function uiAttr(v) {
|
|
1061
|
+
if (v === null || v === "false" || v === "0") return void 0;
|
|
1062
|
+
if (v === "" || v === "true" || v === "1") return true;
|
|
1063
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1064
|
+
}
|
|
1065
|
+
function parsePx(v) {
|
|
1066
|
+
const m = v?.trim().match(/^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/i);
|
|
1067
|
+
return m ? { w: Number(m[1]), h: Number(m[2]) } : void 0;
|
|
1068
|
+
}
|
|
610
1069
|
function parseSize(v) {
|
|
611
1070
|
if (!v) return { width: "320px", height: "480px" };
|
|
612
1071
|
const m = v.trim().split(/x/i);
|
|
@@ -654,10 +1113,20 @@ function createElementClass() {
|
|
|
654
1113
|
return;
|
|
655
1114
|
}
|
|
656
1115
|
const c = this.client;
|
|
657
|
-
if (name === "
|
|
1116
|
+
if ((name === "outfit" || name === "model") && newV) {
|
|
1117
|
+
const want = name === "model" && this.hasAttribute("outfit") ? null : newV;
|
|
1118
|
+
if (want) void (isXcId(want) ? c.setOutfit(want) : c.setModel(want)).catch(() => {
|
|
1119
|
+
});
|
|
1120
|
+
} else if (name === "scene" && newV) void c.setScene(newV).catch(() => {
|
|
658
1121
|
});
|
|
659
1122
|
else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
|
|
660
1123
|
});
|
|
1124
|
+
else if (name === "size") {
|
|
1125
|
+
const { width, height } = parseSize(newV);
|
|
1126
|
+
c.setSize(width, height);
|
|
1127
|
+
} else if (name === "border-radius") c.setBorderRadius(radiusAttr(newV));
|
|
1128
|
+
else if (name === "draggable") c.setDraggable(boolAttr(this, "draggable", false));
|
|
1129
|
+
else if (name === "resizable" || name === "min-size" || name === "max-size") c.setResizable(this.resizableOption());
|
|
661
1130
|
else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
|
|
662
1131
|
});
|
|
663
1132
|
else if (name === "paused") {
|
|
@@ -684,11 +1153,18 @@ function createElementClass() {
|
|
|
684
1153
|
height,
|
|
685
1154
|
position: this.getAttribute("position") ?? "inline",
|
|
686
1155
|
draggable: boolAttr(this, "draggable", false),
|
|
1156
|
+
resizable: this.resizableOption(),
|
|
1157
|
+
borderRadius: radiusAttr(this.getAttribute("border-radius")),
|
|
687
1158
|
lang: this.getAttribute("lang") ?? void 0,
|
|
688
|
-
|
|
1159
|
+
outfit: this.getAttribute("outfit") ?? this.getAttribute("model") ?? void 0,
|
|
1160
|
+
scene: this.getAttribute("scene") ?? void 0,
|
|
1161
|
+
persist: this.getAttribute("persist") || void 0,
|
|
1162
|
+
allowCustomModel: boolAttr(this, "allow-custom-model", false),
|
|
1163
|
+
prefetch: prefetchAttr(this.getAttribute("prefetch")),
|
|
689
1164
|
heavy: this.getAttribute("heavy") ?? void 0,
|
|
690
|
-
ui:
|
|
691
|
-
controls: boolAttr(this, "controls",
|
|
1165
|
+
ui: uiAttr(this.getAttribute("ui")),
|
|
1166
|
+
controls: boolAttr(this, "controls", true),
|
|
1167
|
+
crossOriginIsolated: boolAttr(this, "cross-origin-isolated", false)
|
|
692
1168
|
});
|
|
693
1169
|
this.client = c;
|
|
694
1170
|
const fwd = (ev, name) => c.on(ev, (detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })));
|
|
@@ -698,10 +1174,20 @@ function createElementClass() {
|
|
|
698
1174
|
fwd("stt", "xc-stt");
|
|
699
1175
|
fwd("utterance", "xc-utterance");
|
|
700
1176
|
fwd("error", "xc-error");
|
|
1177
|
+
fwd("outfit-changed", "xc-outfit-changed");
|
|
1178
|
+
fwd("scene-changed", "xc-scene-changed");
|
|
1179
|
+
fwd("move", "xc-move");
|
|
1180
|
+
fwd("resize", "xc-resize");
|
|
701
1181
|
if (boolAttr(this, "paused", false)) c.pause();
|
|
702
1182
|
if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
|
|
703
1183
|
});
|
|
704
1184
|
}
|
|
1185
|
+
/** resizable / min-size / max-size 属性 → SDK 的 resizable 选项 (缺省 / "false" = 关)。 */
|
|
1186
|
+
resizableOption() {
|
|
1187
|
+
if (!boolAttr(this, "resizable", false)) return false;
|
|
1188
|
+
const min = parsePx(this.getAttribute("min-size")), max = parsePx(this.getAttribute("max-size"));
|
|
1189
|
+
return { minWidth: min?.w, minHeight: min?.h, maxWidth: max?.w, maxHeight: max?.h };
|
|
1190
|
+
}
|
|
705
1191
|
say(text, opts) {
|
|
706
1192
|
return this.requireClient().say(text, opts);
|
|
707
1193
|
}
|
|
@@ -717,6 +1203,21 @@ function createElementClass() {
|
|
|
717
1203
|
expression(name) {
|
|
718
1204
|
return this.requireClient().expression(name);
|
|
719
1205
|
}
|
|
1206
|
+
setOutfit(id) {
|
|
1207
|
+
return this.requireClient().setOutfit(id);
|
|
1208
|
+
}
|
|
1209
|
+
setScene(id) {
|
|
1210
|
+
return this.requireClient().setScene(id);
|
|
1211
|
+
}
|
|
1212
|
+
getOutfits() {
|
|
1213
|
+
return this.requireClient().getOutfits();
|
|
1214
|
+
}
|
|
1215
|
+
getScenes() {
|
|
1216
|
+
return this.requireClient().getScenes();
|
|
1217
|
+
}
|
|
1218
|
+
prefetch(ids) {
|
|
1219
|
+
return this.requireClient().prefetch(ids);
|
|
1220
|
+
}
|
|
720
1221
|
destroy() {
|
|
721
1222
|
this.client?.destroy();
|
|
722
1223
|
this.client = null;
|