@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
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.15 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
|
+
import {
|
|
3
|
+
XC_DEFAULT_SRC,
|
|
4
|
+
XC_UI_PARTS,
|
|
5
|
+
XC_WINDOW_CORNER_RADIUS,
|
|
6
|
+
isXcEnvelope,
|
|
7
|
+
isXcId,
|
|
8
|
+
normalizeOrigin,
|
|
9
|
+
normalizeXcUiOption,
|
|
10
|
+
xcMessage
|
|
11
|
+
} from "./chunk-KKRZ3SUD.js";
|
|
12
|
+
|
|
13
|
+
// src/gesture-box.ts
|
|
14
|
+
var DEFAULT_RESIZE_LIMITS = Object.freeze({
|
|
15
|
+
minWidth: 120,
|
|
16
|
+
minHeight: 180,
|
|
17
|
+
maxWidth: Infinity,
|
|
18
|
+
maxHeight: Infinity
|
|
19
|
+
});
|
|
20
|
+
var finitePositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
21
|
+
function normalizeResizable(v) {
|
|
22
|
+
if (!v) return null;
|
|
23
|
+
const o = v === true ? {} : v;
|
|
24
|
+
const minWidth = finitePositive(o.minWidth) ? o.minWidth : DEFAULT_RESIZE_LIMITS.minWidth;
|
|
25
|
+
const minHeight = finitePositive(o.minHeight) ? o.minHeight : DEFAULT_RESIZE_LIMITS.minHeight;
|
|
26
|
+
const maxWidth = finitePositive(o.maxWidth) ? Math.max(o.maxWidth, minWidth) : Infinity;
|
|
27
|
+
const maxHeight = finitePositive(o.maxHeight) ? Math.max(o.maxHeight, minHeight) : Infinity;
|
|
28
|
+
return { minWidth, minHeight, maxWidth, maxHeight };
|
|
29
|
+
}
|
|
30
|
+
var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
|
|
31
|
+
function moveBox(start, totalDx, totalDy, vp) {
|
|
32
|
+
return {
|
|
33
|
+
left: clamp(start.left + totalDx, 0, vp.width - start.width),
|
|
34
|
+
top: clamp(start.top + totalDy, 0, vp.height - start.height),
|
|
35
|
+
width: start.width,
|
|
36
|
+
height: start.height
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resizeBox(start, corner, totalDx, totalDy, limits, vp) {
|
|
40
|
+
const west = corner === "NW" || corner === "SW";
|
|
41
|
+
const north = corner === "NW" || corner === "NE";
|
|
42
|
+
const right = start.left + start.width, bottom = start.top + start.height;
|
|
43
|
+
const wantW = west ? start.width - totalDx : start.width + totalDx;
|
|
44
|
+
const wantH = north ? start.height - totalDy : start.height + totalDy;
|
|
45
|
+
const roomW = west ? right : vp.width - start.left;
|
|
46
|
+
const roomH = north ? bottom : vp.height - start.top;
|
|
47
|
+
const width = clamp(wantW, limits.minWidth, Math.min(limits.maxWidth, roomW));
|
|
48
|
+
const height = clamp(wantH, limits.minHeight, Math.min(limits.maxHeight, roomH));
|
|
49
|
+
return { left: west ? right - width : start.left, top: north ? bottom - height : start.top, width, height };
|
|
50
|
+
}
|
|
51
|
+
function fitBox(b, vp) {
|
|
52
|
+
const width = Math.min(b.width, Math.max(1, vp.width)), height = Math.min(b.height, Math.max(1, vp.height));
|
|
53
|
+
return { left: clamp(b.left, 0, vp.width - width), top: clamp(b.top, 0, vp.height - height), width, height };
|
|
54
|
+
}
|
|
55
|
+
var CORNERS = /* @__PURE__ */ new Set(["NW", "NE", "SW", "SE"]);
|
|
56
|
+
var PHASES = /* @__PURE__ */ new Set(["start", "move", "end"]);
|
|
57
|
+
var REASONS = /* @__PURE__ */ new Set(["up", "cancel", "blur"]);
|
|
58
|
+
var MAX_GESTURE_DELTA = 2e4;
|
|
59
|
+
var delta = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= MAX_GESTURE_DELTA;
|
|
60
|
+
var counter = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
61
|
+
function parseGesturePayload(kind, raw) {
|
|
62
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
63
|
+
const p = raw;
|
|
64
|
+
if (!counter(p.gesture) || !counter(p.seq) || typeof p.phase !== "string" || !PHASES.has(p.phase)) return null;
|
|
65
|
+
if (!delta(p.dx) || !delta(p.dy) || !delta(p.totalDx) || !delta(p.totalDy)) return null;
|
|
66
|
+
if (p.reason !== void 0 && (typeof p.reason !== "string" || !REASONS.has(p.reason))) return null;
|
|
67
|
+
const out = { gesture: p.gesture, seq: p.seq, phase: p.phase, totalDx: p.totalDx, totalDy: p.totalDy };
|
|
68
|
+
if (kind === "resize") {
|
|
69
|
+
if (typeof p.corner !== "string" || !CORNERS.has(p.corner)) return null;
|
|
70
|
+
out.corner = p.corner;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
var GestureGate = class {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.lastGesture = 0;
|
|
77
|
+
this.lastSeq = -1;
|
|
78
|
+
this.open = false;
|
|
79
|
+
}
|
|
80
|
+
get active() {
|
|
81
|
+
return this.open;
|
|
82
|
+
}
|
|
83
|
+
accept(p) {
|
|
84
|
+
if (p.phase === "start") {
|
|
85
|
+
if (p.gesture <= this.lastGesture || p.seq !== 0) return false;
|
|
86
|
+
this.lastGesture = p.gesture;
|
|
87
|
+
this.lastSeq = 0;
|
|
88
|
+
this.open = true;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (!this.open || p.gesture !== this.lastGesture || p.seq <= this.lastSeq) return false;
|
|
92
|
+
this.lastSeq = p.seq;
|
|
93
|
+
if (p.phase === "end") this.open = false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** 宿主主动结束 (关闭手势 / 销毁): 之后同一手势的迟到消息被拒绝。 */
|
|
97
|
+
close() {
|
|
98
|
+
this.open = false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/client.ts
|
|
103
|
+
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
104
|
+
'<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>'
|
|
105
|
+
);
|
|
106
|
+
var css = (v, d) => v === void 0 ? d : typeof v === "number" ? `${v}px` : v;
|
|
107
|
+
function createXiaochun(options) {
|
|
108
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
109
|
+
throw new Error("[project-xiaochun] createXiaochun must run in a browser");
|
|
110
|
+
}
|
|
111
|
+
const container = typeof options.container === "string" ? document.querySelector(options.container) : options.container;
|
|
112
|
+
if (!container) throw new Error(`[project-xiaochun] container not found: ${String(options.container)}`);
|
|
113
|
+
const src = options.src ?? XC_DEFAULT_SRC;
|
|
114
|
+
let srcUrl;
|
|
115
|
+
try {
|
|
116
|
+
srcUrl = new URL(src, window.location.href);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error(`[project-xiaochun] invalid src: ${src}`);
|
|
119
|
+
}
|
|
120
|
+
const frameOrigin = normalizeOrigin(options.origin ?? srcUrl.origin);
|
|
121
|
+
if (!frameOrigin) throw new Error("[project-xiaochun] invalid origin (wildcards are not allowed)");
|
|
122
|
+
const trusted = /* @__PURE__ */ new Set([frameOrigin]);
|
|
123
|
+
for (const o of options.allowedOrigins ?? []) {
|
|
124
|
+
const n = normalizeOrigin(o);
|
|
125
|
+
if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
|
|
126
|
+
trusted.add(n);
|
|
127
|
+
}
|
|
128
|
+
const prefsKey = options.persist === "host" ? "xiaochun:prefs" : typeof options.persist === "string" && options.persist ? options.persist : null;
|
|
129
|
+
const readPrefs = () => {
|
|
130
|
+
if (!prefsKey) return {};
|
|
131
|
+
try {
|
|
132
|
+
const raw = JSON.parse(window.localStorage.getItem(prefsKey) ?? "null");
|
|
133
|
+
const out = {};
|
|
134
|
+
if (raw && typeof raw === "object") {
|
|
135
|
+
if (isXcId(raw.outfit)) out.outfit = raw.outfit;
|
|
136
|
+
if (isXcId(raw.scene)) out.scene = raw.scene;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
} catch {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const writePrefs = (patch) => {
|
|
144
|
+
if (!prefsKey) return;
|
|
145
|
+
try {
|
|
146
|
+
const next = { ...readPrefs() };
|
|
147
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
148
|
+
if (v) next[k] = v;
|
|
149
|
+
else delete next[k];
|
|
150
|
+
}
|
|
151
|
+
window.localStorage.setItem(prefsKey, JSON.stringify(next));
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const prefs = readPrefs();
|
|
156
|
+
if (options.model !== void 0 && options.outfit === void 0) {
|
|
157
|
+
console.warn('[project-xiaochun] option "model" is deprecated, use "outfit" (built-in outfit id).');
|
|
158
|
+
}
|
|
159
|
+
const uiParse = normalizeXcUiOption(options.ui);
|
|
160
|
+
const uiParts = uiParse.parts;
|
|
161
|
+
if (uiParse.legacy) console.warn(`[project-xiaochun] option "ui: true" is deprecated, use an array of parts, e.g. ui: ['chat', 'bubble', 'outfit', 'scene'].`);
|
|
162
|
+
if (uiParse.unknown.length) console.warn(`[project-xiaochun] ignoring unknown ui part(s): ${uiParse.unknown.join(", ")} (known: ${XC_UI_PARTS.join(", ")})`);
|
|
163
|
+
const outfitOpt = options.outfit ?? (isXcId(options.model) ? options.model : void 0);
|
|
164
|
+
const initialOutfit = outfitOpt ?? prefs.outfit;
|
|
165
|
+
const initialScene = options.scene ?? prefs.scene ?? (options.transparent ? "transparent" : void 0);
|
|
166
|
+
let transparent = initialScene === "transparent" || initialScene !== "light" && initialScene !== "dark" && (options.transparent ?? false);
|
|
167
|
+
let passthrough = options.passthrough ?? transparent;
|
|
168
|
+
const lazy = options.lazy ?? true;
|
|
169
|
+
const position = options.position ?? "inline";
|
|
170
|
+
const handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
171
|
+
const wrapper = document.createElement("div");
|
|
172
|
+
wrapper.setAttribute("data-xiaochun", "");
|
|
173
|
+
const ws = wrapper.style;
|
|
174
|
+
ws.position = position === "inline" ? "relative" : "fixed";
|
|
175
|
+
ws.width = css(options.width, "320px");
|
|
176
|
+
ws.height = css(options.height, "480px");
|
|
177
|
+
ws.maxWidth = "100%";
|
|
178
|
+
ws.overflow = "visible";
|
|
179
|
+
ws.contain = "layout style";
|
|
180
|
+
ws.boxShadow = "var(--xc-shadow, none)";
|
|
181
|
+
const applyWrapperBg = () => {
|
|
182
|
+
ws.background = transparent ? "" : "var(--xc-bg, transparent)";
|
|
183
|
+
};
|
|
184
|
+
applyWrapperBg();
|
|
185
|
+
let radiusOpt = options.borderRadius;
|
|
186
|
+
const radiusValue = () => radiusOpt !== void 0 ? css(radiusOpt, "0px") : transparent ? "0px" : `${XC_WINDOW_CORNER_RADIUS}px`;
|
|
187
|
+
const applyRadius = () => {
|
|
188
|
+
const v = `var(--xc-radius, ${radiusValue()})`;
|
|
189
|
+
ws.borderRadius = v;
|
|
190
|
+
if (placeholderEl) placeholderEl.style.borderRadius = v;
|
|
191
|
+
if (iframe) iframe.style.borderRadius = v;
|
|
192
|
+
};
|
|
193
|
+
ws.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
194
|
+
if (position === "bottom-right") {
|
|
195
|
+
ws.right = "var(--xc-offset-x, 16px)";
|
|
196
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
197
|
+
}
|
|
198
|
+
if (position === "bottom-left") {
|
|
199
|
+
ws.left = "var(--xc-offset-x, 16px)";
|
|
200
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
201
|
+
}
|
|
202
|
+
if (position !== "inline") ws.zIndex = `var(--xc-z-index, ${options.zIndex ?? 2147483e3})`;
|
|
203
|
+
ws.pointerEvents = "none";
|
|
204
|
+
wrapper.setAttribute("part", "wrapper");
|
|
205
|
+
container.appendChild(wrapper);
|
|
206
|
+
let placeholderEl = null;
|
|
207
|
+
if (options.placeholder !== false) {
|
|
208
|
+
if (options.placeholder instanceof HTMLElement) placeholderEl = options.placeholder;
|
|
209
|
+
else {
|
|
210
|
+
const img = document.createElement("img");
|
|
211
|
+
img.src = options.placeholder || FALLBACK_PLACEHOLDER;
|
|
212
|
+
img.alt = "Project XiaoChun";
|
|
213
|
+
img.decoding = "async";
|
|
214
|
+
img.width = 320;
|
|
215
|
+
img.height = 480;
|
|
216
|
+
placeholderEl = img;
|
|
217
|
+
}
|
|
218
|
+
const ps = placeholderEl.style;
|
|
219
|
+
placeholderEl.setAttribute("part", "placeholder");
|
|
220
|
+
ps.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
221
|
+
ps.position = "absolute";
|
|
222
|
+
ps.inset = "0";
|
|
223
|
+
ps.width = "100%";
|
|
224
|
+
ps.height = "100%";
|
|
225
|
+
ps.objectFit = "contain";
|
|
226
|
+
ps.transition = "opacity .25s ease";
|
|
227
|
+
ps.pointerEvents = "auto";
|
|
228
|
+
ps.cursor = lazy === false ? "default" : "pointer";
|
|
229
|
+
placeholderEl.addEventListener("click", () => activate());
|
|
230
|
+
wrapper.appendChild(placeholderEl);
|
|
231
|
+
}
|
|
232
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
233
|
+
const emit = (ev, payload) => {
|
|
234
|
+
listeners.get(ev)?.forEach((cb) => {
|
|
235
|
+
try {
|
|
236
|
+
cb(payload);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error("[project-xiaochun] listener error", e);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
let resolveReady;
|
|
243
|
+
let rejectReady;
|
|
244
|
+
const ready = new Promise((res, rej) => {
|
|
245
|
+
resolveReady = res;
|
|
246
|
+
rejectReady = rej;
|
|
247
|
+
});
|
|
248
|
+
ready.catch(() => {
|
|
249
|
+
});
|
|
250
|
+
let iframe = null;
|
|
251
|
+
let port = null;
|
|
252
|
+
let destroyed = false;
|
|
253
|
+
let userPaused = false;
|
|
254
|
+
let autoPaused = false;
|
|
255
|
+
let loaded = false;
|
|
256
|
+
let lastHit = false;
|
|
257
|
+
let caps = null;
|
|
258
|
+
let currentOutfit = null;
|
|
259
|
+
let currentScene = null;
|
|
260
|
+
const queue = [];
|
|
261
|
+
const pending = /* @__PURE__ */ new Map();
|
|
262
|
+
let idSeq = 0;
|
|
263
|
+
let handshakeTimer = null;
|
|
264
|
+
let io = null;
|
|
265
|
+
let idleHandle = null;
|
|
266
|
+
let resolveHandshake;
|
|
267
|
+
const handshaked = new Promise((res) => {
|
|
268
|
+
resolveHandshake = res;
|
|
269
|
+
});
|
|
270
|
+
const failLocal = (type, id, code, message) => {
|
|
271
|
+
emit("error", { code, message, command: type });
|
|
272
|
+
const p = id ? pending.get(id) : void 0;
|
|
273
|
+
if (p && id) {
|
|
274
|
+
pending.delete(id);
|
|
275
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
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)`);
|
|
279
|
+
const postToFrame = (type, payload, id, transfer, requires) => {
|
|
280
|
+
if (!port) {
|
|
281
|
+
queue.push({ type, payload, id, transfer, requires });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (requires && !caps?.[requires]) {
|
|
285
|
+
unsupportedByFrame(type, id);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
port.postMessage(xcMessage(type, payload, id), transfer ?? []);
|
|
289
|
+
};
|
|
290
|
+
const send = (type, payload, wait = false, transfer, fixedId, requires) => {
|
|
291
|
+
if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
292
|
+
if (!iframe) activate();
|
|
293
|
+
const id = fixedId ?? `c${++idSeq}`;
|
|
294
|
+
const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
|
|
295
|
+
postToFrame(type, payload, id, transfer, requires);
|
|
296
|
+
return p;
|
|
297
|
+
};
|
|
298
|
+
const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
|
|
299
|
+
const bindAbort = (signal, id) => {
|
|
300
|
+
if (!signal) return;
|
|
301
|
+
const abort = () => {
|
|
302
|
+
if (!destroyed) postToFrame("xc.audio.end", { abort: true }, id);
|
|
303
|
+
};
|
|
304
|
+
if (signal.aborted) abort();
|
|
305
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
306
|
+
};
|
|
307
|
+
const speakAudio = async (source, opts = {}) => {
|
|
308
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
309
|
+
if (!iframe) activate();
|
|
310
|
+
const { signal, transfer, fetch: fetchWhere, ...rest } = opts;
|
|
311
|
+
const payload = { ...rest };
|
|
312
|
+
const list = [];
|
|
313
|
+
if (typeof source === "string") {
|
|
314
|
+
if (fetchWhere === "frame") payload.source = source;
|
|
315
|
+
else {
|
|
316
|
+
const res = await fetch(source);
|
|
317
|
+
if (!res.ok) throw new Error(`[project-xiaochun] audio fetch failed: HTTP ${res.status}`);
|
|
318
|
+
const buf = await res.arrayBuffer();
|
|
319
|
+
payload.source = buf;
|
|
320
|
+
list.push(buf);
|
|
321
|
+
payload.mimeType ?? (payload.mimeType = res.headers.get("content-type") ?? void 0);
|
|
322
|
+
}
|
|
323
|
+
} else if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
324
|
+
payload.source = source;
|
|
325
|
+
payload.mimeType ?? (payload.mimeType = source.type || void 0);
|
|
326
|
+
} else {
|
|
327
|
+
const buf = toBuffer(source);
|
|
328
|
+
payload.source = buf;
|
|
329
|
+
if (!(source instanceof ArrayBuffer) || transfer !== false) list.push(buf);
|
|
330
|
+
}
|
|
331
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
332
|
+
const id = `c${++idSeq}`;
|
|
333
|
+
const done = send("xc.audio", payload, true, list, id);
|
|
334
|
+
bindAbort(signal, id);
|
|
335
|
+
return done;
|
|
336
|
+
};
|
|
337
|
+
const speakAudioStream = (opts) => {
|
|
338
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
339
|
+
const { signal, transfer, fetch: _f, mimeType: _m, ...rest } = opts;
|
|
340
|
+
const id = `c${++idSeq}`;
|
|
341
|
+
let started = false;
|
|
342
|
+
let ended = false;
|
|
343
|
+
let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
|
|
344
|
+
let done = Promise.resolve();
|
|
345
|
+
const handle = {
|
|
346
|
+
write(data) {
|
|
347
|
+
if (ended) throw new Error("[project-xiaochun] audio stream already ended");
|
|
348
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
349
|
+
if (data instanceof Int16Array) fmt ?? (fmt = "pcm16");
|
|
350
|
+
else if (data instanceof Float32Array) fmt ?? (fmt = "float32");
|
|
351
|
+
if (!fmt) throw new Error("[project-xiaochun] speakAudioStream: set opts.format when writing raw ArrayBuffers");
|
|
352
|
+
const buf = toBuffer(data);
|
|
353
|
+
const body = { data: buf, format: fmt, sampleRate: rest.sampleRate, channels: rest.channels };
|
|
354
|
+
if (!started) {
|
|
355
|
+
started = true;
|
|
356
|
+
body.text = rest.text;
|
|
357
|
+
body.motion = rest.motion;
|
|
358
|
+
body.lipsync = rest.lipsync;
|
|
359
|
+
done = send("xc.audio.chunk", body, true, [buf], id);
|
|
360
|
+
bindAbort(signal, id);
|
|
361
|
+
} else {
|
|
362
|
+
postToFrame("xc.audio.chunk", body, id, !(data instanceof ArrayBuffer) || transfer !== false ? [buf] : []);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
end() {
|
|
366
|
+
if (ended) return;
|
|
367
|
+
ended = true;
|
|
368
|
+
if (started) postToFrame("xc.audio.end", {}, id);
|
|
369
|
+
},
|
|
370
|
+
abort() {
|
|
371
|
+
if (ended && !started) return;
|
|
372
|
+
ended = true;
|
|
373
|
+
if (started) postToFrame("xc.audio.end", { abort: true }, id);
|
|
374
|
+
},
|
|
375
|
+
get done() {
|
|
376
|
+
return done;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
return handle;
|
|
380
|
+
};
|
|
381
|
+
function buildUrl() {
|
|
382
|
+
const u = new URL(srcUrl.href);
|
|
383
|
+
u.searchParams.set("host", window.location.origin);
|
|
384
|
+
if (transparent) u.searchParams.set("transparent", "1");
|
|
385
|
+
if (uiParts.length) u.searchParams.set("ui", uiParts.join(","));
|
|
386
|
+
if (options.lang) u.searchParams.set("lang", options.lang);
|
|
387
|
+
if (options.heavy) u.searchParams.set("heavy", options.heavy);
|
|
388
|
+
if (options.controls === false) u.searchParams.set("controls", "0");
|
|
389
|
+
if (initialScene && isXcId(initialScene)) u.searchParams.set("scene", initialScene);
|
|
390
|
+
if (initialOutfit) u.searchParams.set("outfit", initialOutfit);
|
|
391
|
+
return u.href;
|
|
392
|
+
}
|
|
393
|
+
function activate() {
|
|
394
|
+
if (destroyed || iframe) return;
|
|
395
|
+
cancelIdle();
|
|
396
|
+
io?.disconnect();
|
|
397
|
+
io = null;
|
|
398
|
+
const f = document.createElement("iframe");
|
|
399
|
+
f.title = "Project XiaoChun";
|
|
400
|
+
f.loading = "lazy";
|
|
401
|
+
f.allow = options.crossOriginIsolated === true ? "microphone; autoplay; cross-origin-isolated" : "microphone; autoplay";
|
|
402
|
+
f.referrerPolicy = "strict-origin-when-cross-origin";
|
|
403
|
+
if (options.sandbox !== false) {
|
|
404
|
+
f.setAttribute("sandbox", options.sandbox ?? "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox");
|
|
405
|
+
}
|
|
406
|
+
f.setAttribute("allowtransparency", "true");
|
|
407
|
+
f.setAttribute("scrolling", "no");
|
|
408
|
+
const s = f.style;
|
|
409
|
+
s.position = "absolute";
|
|
410
|
+
s.inset = "0";
|
|
411
|
+
s.width = "100%";
|
|
412
|
+
s.height = "100%";
|
|
413
|
+
s.border = "0";
|
|
414
|
+
s.background = "transparent";
|
|
415
|
+
s.colorScheme = "normal";
|
|
416
|
+
s.borderRadius = `var(--xc-radius, ${radiusValue()})`;
|
|
417
|
+
f.setAttribute("part", "iframe");
|
|
418
|
+
s.opacity = "0";
|
|
419
|
+
s.transition = "opacity .25s ease";
|
|
420
|
+
s.pointerEvents = passthrough ? "none" : "auto";
|
|
421
|
+
f.src = buildUrl();
|
|
422
|
+
iframe = f;
|
|
423
|
+
wrapper.appendChild(f);
|
|
424
|
+
if (placeholderEl) placeholderEl.style.cursor = "default";
|
|
425
|
+
handshakeTimer = setTimeout(() => {
|
|
426
|
+
if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
|
|
427
|
+
}, handshakeTimeout);
|
|
428
|
+
setupAutoPause();
|
|
429
|
+
if (passthrough) enablePassthrough();
|
|
430
|
+
}
|
|
431
|
+
const onWindowMessage = (ev) => {
|
|
432
|
+
if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
|
|
433
|
+
if (!trusted.has(ev.origin)) {
|
|
434
|
+
emit("error", { code: "origin_denied", message: `ignored message from untrusted origin ${ev.origin}` });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const data = ev.data;
|
|
438
|
+
if (!isXcEnvelope(data) || data.type !== "xc.ready" || port) return;
|
|
439
|
+
const ch = new MessageChannel();
|
|
440
|
+
port = ch.port1;
|
|
441
|
+
port.onmessage = onPortMessage;
|
|
442
|
+
caps = data.payload?.capabilities ?? null;
|
|
443
|
+
const gWant = wantGestures();
|
|
444
|
+
sentGestures = gWant;
|
|
445
|
+
const initConfig = {
|
|
446
|
+
...options.allowCustomModel ? { allowCustomModel: true } : {},
|
|
447
|
+
...gWant.move || gWant.resize ? { gestures: gWant } : {}
|
|
448
|
+
};
|
|
449
|
+
iframe.contentWindow.postMessage(
|
|
450
|
+
xcMessage("xc.init", { hostOrigin: window.location.origin, ...Object.keys(initConfig).length ? { config: initConfig } : {} }),
|
|
451
|
+
ev.origin,
|
|
452
|
+
[ch.port2]
|
|
453
|
+
);
|
|
454
|
+
warnGestureUnsupported();
|
|
455
|
+
if (handshakeTimer) {
|
|
456
|
+
clearTimeout(handshakeTimer);
|
|
457
|
+
handshakeTimer = null;
|
|
458
|
+
}
|
|
459
|
+
emit("handshake", data.payload);
|
|
460
|
+
resolveHandshake();
|
|
461
|
+
for (const m of queue.splice(0)) {
|
|
462
|
+
if (m.requires && !caps?.[m.requires]) unsupportedByFrame(m.type, m.id);
|
|
463
|
+
else port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
|
|
464
|
+
}
|
|
465
|
+
syncPause();
|
|
466
|
+
};
|
|
467
|
+
window.addEventListener("message", onWindowMessage);
|
|
468
|
+
function ack(id, value) {
|
|
469
|
+
const p = id ? pending.get(id) : void 0;
|
|
470
|
+
if (p && id) {
|
|
471
|
+
pending.delete(id);
|
|
472
|
+
p.resolve(value);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function onPortMessage(ev) {
|
|
476
|
+
const data = ev.data;
|
|
477
|
+
if (!isXcEnvelope(data)) return;
|
|
478
|
+
switch (data.type) {
|
|
479
|
+
case "xc.load.progress":
|
|
480
|
+
emit("progress", data.payload);
|
|
481
|
+
break;
|
|
482
|
+
case "xc.loaded":
|
|
483
|
+
if (!loaded) {
|
|
484
|
+
loaded = true;
|
|
485
|
+
if (options.prefetch && options.heavy === "eager" && caps?.prefetch) {
|
|
486
|
+
const ids = Array.isArray(options.prefetch) ? options.prefetch : void 0;
|
|
487
|
+
void api.prefetch(ids).catch(() => {
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (iframe) iframe.style.opacity = "1";
|
|
491
|
+
if (placeholderEl) {
|
|
492
|
+
const el = placeholderEl;
|
|
493
|
+
el.style.opacity = "0";
|
|
494
|
+
el.style.pointerEvents = "none";
|
|
495
|
+
setTimeout(() => el.remove(), 300);
|
|
496
|
+
placeholderEl = null;
|
|
497
|
+
}
|
|
498
|
+
resolveReady();
|
|
499
|
+
}
|
|
500
|
+
emit("ready", data.payload);
|
|
501
|
+
break;
|
|
502
|
+
case "xc.outfit-changed": {
|
|
503
|
+
const pl = data.payload;
|
|
504
|
+
if (!pl.noop) {
|
|
505
|
+
currentOutfit = pl.id;
|
|
506
|
+
emit("outfit-changed", pl);
|
|
507
|
+
if (!pl.initial && pl.id) writePrefs({ outfit: pl.id });
|
|
508
|
+
}
|
|
509
|
+
ack(data.id);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
case "xc.scene-changed": {
|
|
513
|
+
const pl = data.payload;
|
|
514
|
+
currentScene = pl.id;
|
|
515
|
+
if (pl.transparent !== transparent) applyTransparency(pl.transparent);
|
|
516
|
+
if (!pl.noop) {
|
|
517
|
+
emit("scene-changed", pl);
|
|
518
|
+
if (!pl.initial) writePrefs({ scene: pl.id });
|
|
519
|
+
}
|
|
520
|
+
ack(data.id);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case "xc.prefetched":
|
|
524
|
+
ack(data.id, data.payload);
|
|
525
|
+
break;
|
|
526
|
+
case "xc.state":
|
|
527
|
+
emit("state", data.payload);
|
|
528
|
+
break;
|
|
529
|
+
case "xc.stt":
|
|
530
|
+
emit("stt", data.payload);
|
|
531
|
+
break;
|
|
532
|
+
case "xc.utterance": {
|
|
533
|
+
emit("utterance", data.payload);
|
|
534
|
+
const p = data.id ? pending.get(data.id) : void 0;
|
|
535
|
+
if (p && data.payload.phase === "end") {
|
|
536
|
+
pending.delete(data.id);
|
|
537
|
+
p.resolve();
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case "xc.hit-region": {
|
|
542
|
+
const { hit } = data.payload;
|
|
543
|
+
emit("hit-region", data.payload);
|
|
544
|
+
if (passthrough && iframe && hit !== lastHit) {
|
|
545
|
+
lastHit = hit;
|
|
546
|
+
iframe.style.pointerEvents = hit ? "auto" : "none";
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case "xc.gesture-move":
|
|
551
|
+
onGesture("move", data.payload);
|
|
552
|
+
break;
|
|
553
|
+
case "xc.gesture-resize":
|
|
554
|
+
onGesture("resize", data.payload);
|
|
555
|
+
break;
|
|
556
|
+
case "xc.error": {
|
|
557
|
+
const { code, message, command } = data.payload;
|
|
558
|
+
emit("error", { code, message, command });
|
|
559
|
+
if (code === "unknown_id" && (command === "outfit" || command === "scene")) writePrefs({ [command]: null });
|
|
560
|
+
const p = data.id ? pending.get(data.id) : void 0;
|
|
561
|
+
if (p && data.id) {
|
|
562
|
+
pending.delete(data.id);
|
|
563
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const POINTER_MIN_INTERVAL_MS = 32;
|
|
572
|
+
const POINTER_MIN_MOVE_PX = 2;
|
|
573
|
+
let pointerRaf = 0;
|
|
574
|
+
let lastPtr = null;
|
|
575
|
+
let lastPosted = null;
|
|
576
|
+
let rectCache = null;
|
|
577
|
+
let rectRo = null;
|
|
578
|
+
let rectAt = 0;
|
|
579
|
+
const invalidateRect = () => {
|
|
580
|
+
rectCache = null;
|
|
581
|
+
};
|
|
582
|
+
const getRect = () => {
|
|
583
|
+
if (!iframe) return null;
|
|
584
|
+
const now = performance.now();
|
|
585
|
+
if (!rectCache || now - rectAt > 400) {
|
|
586
|
+
rectCache = iframe.getBoundingClientRect();
|
|
587
|
+
rectAt = now;
|
|
588
|
+
}
|
|
589
|
+
return rectCache;
|
|
590
|
+
};
|
|
591
|
+
const onHostPointer = (e) => {
|
|
592
|
+
if (!iframe || !port || iframe.style.pointerEvents === "auto") return;
|
|
593
|
+
lastPtr = { x: e.clientX, y: e.clientY };
|
|
594
|
+
if (pointerRaf) return;
|
|
595
|
+
pointerRaf = requestAnimationFrame(() => {
|
|
596
|
+
pointerRaf = 0;
|
|
597
|
+
if (!iframe || !port || !lastPtr) return;
|
|
598
|
+
const r = getRect();
|
|
599
|
+
if (!r) return;
|
|
600
|
+
const x = lastPtr.x - r.left, y = lastPtr.y - r.top;
|
|
601
|
+
if (x < 0 || y < 0 || x > r.width || y > r.height) {
|
|
602
|
+
lastPosted = null;
|
|
603
|
+
if (lastHit) {
|
|
604
|
+
lastHit = false;
|
|
605
|
+
iframe.style.pointerEvents = "none";
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const now = performance.now();
|
|
610
|
+
if (lastPosted && now - lastPosted.t < POINTER_MIN_INTERVAL_MS) return;
|
|
611
|
+
if (lastPosted && Math.abs(x - lastPosted.x) < POINTER_MIN_MOVE_PX && Math.abs(y - lastPosted.y) < POINTER_MIN_MOVE_PX) return;
|
|
612
|
+
lastPosted = { x, y, t: now };
|
|
613
|
+
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
let passthroughActive = false;
|
|
617
|
+
function enablePassthrough() {
|
|
618
|
+
if (passthroughActive || !iframe) return;
|
|
619
|
+
passthroughActive = true;
|
|
620
|
+
window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
621
|
+
window.addEventListener("pointerdown", onHostPointer, { passive: true });
|
|
622
|
+
window.addEventListener("scroll", invalidateRect, { passive: true, capture: true });
|
|
623
|
+
window.addEventListener("resize", invalidateRect, { passive: true });
|
|
624
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
625
|
+
rectRo = new ResizeObserver(invalidateRect);
|
|
626
|
+
rectRo.observe(iframe);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function disablePassthrough() {
|
|
630
|
+
if (!passthroughActive) return;
|
|
631
|
+
passthroughActive = false;
|
|
632
|
+
window.removeEventListener("pointermove", onHostPointer);
|
|
633
|
+
window.removeEventListener("pointerdown", onHostPointer);
|
|
634
|
+
window.removeEventListener("scroll", invalidateRect, true);
|
|
635
|
+
window.removeEventListener("resize", invalidateRect);
|
|
636
|
+
rectRo?.disconnect();
|
|
637
|
+
rectRo = null;
|
|
638
|
+
if (pointerRaf) {
|
|
639
|
+
cancelAnimationFrame(pointerRaf);
|
|
640
|
+
pointerRaf = 0;
|
|
641
|
+
}
|
|
642
|
+
lastPtr = null;
|
|
643
|
+
}
|
|
644
|
+
function applyTransparency(next) {
|
|
645
|
+
transparent = next;
|
|
646
|
+
applyWrapperBg();
|
|
647
|
+
applyRadius();
|
|
648
|
+
passthrough = next ? options.passthrough ?? true : false;
|
|
649
|
+
lastHit = false;
|
|
650
|
+
lastPosted = null;
|
|
651
|
+
rectCache = null;
|
|
652
|
+
if (passthrough) enablePassthrough();
|
|
653
|
+
else disablePassthrough();
|
|
654
|
+
if (iframe) iframe.style.pointerEvents = passthrough ? "none" : "auto";
|
|
655
|
+
}
|
|
656
|
+
let pauseIo = null;
|
|
657
|
+
function setupAutoPause() {
|
|
658
|
+
if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
|
|
659
|
+
pauseIo = new IntersectionObserver((entries) => {
|
|
660
|
+
const visible = entries[entries.length - 1]?.isIntersecting ?? true;
|
|
661
|
+
autoPaused = !visible;
|
|
662
|
+
syncPause();
|
|
663
|
+
});
|
|
664
|
+
pauseIo.observe(wrapper);
|
|
665
|
+
}
|
|
666
|
+
let sentPaused = false;
|
|
667
|
+
function syncPause() {
|
|
668
|
+
if (!port) return;
|
|
669
|
+
const want = userPaused || autoPaused;
|
|
670
|
+
if (want === sentPaused) return;
|
|
671
|
+
sentPaused = want;
|
|
672
|
+
port.postMessage(xcMessage(want ? "xc.pause" : "xc.resume"));
|
|
673
|
+
}
|
|
674
|
+
function cancelIdle() {
|
|
675
|
+
if (idleHandle === null) return;
|
|
676
|
+
const w = window;
|
|
677
|
+
if (w.cancelIdleCallback) w.cancelIdleCallback(idleHandle);
|
|
678
|
+
else clearTimeout(idleHandle);
|
|
679
|
+
idleHandle = null;
|
|
680
|
+
}
|
|
681
|
+
function scheduleLazy() {
|
|
682
|
+
if (lazy === false) {
|
|
683
|
+
activate();
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (lazy === "click") return;
|
|
687
|
+
const go = () => {
|
|
688
|
+
const w = window;
|
|
689
|
+
idleHandle = w.requestIdleCallback ? w.requestIdleCallback(() => {
|
|
690
|
+
idleHandle = null;
|
|
691
|
+
activate();
|
|
692
|
+
}, { timeout: 3e3 }) : setTimeout(activate, 1200);
|
|
693
|
+
};
|
|
694
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
695
|
+
go();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
io = new IntersectionObserver((entries) => {
|
|
699
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
700
|
+
io?.disconnect();
|
|
701
|
+
io = null;
|
|
702
|
+
go();
|
|
703
|
+
}
|
|
704
|
+
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
705
|
+
io.observe(wrapper);
|
|
706
|
+
}
|
|
707
|
+
let draggableOn = options.draggable === true;
|
|
708
|
+
let resizeLimits = normalizeResizable(options.resizable);
|
|
709
|
+
const gates = { move: new GestureGate(), resize: new GestureGate() };
|
|
710
|
+
let activeGesture = null;
|
|
711
|
+
let inlineShift = { x: 0, y: 0 };
|
|
712
|
+
let userMoved = false;
|
|
713
|
+
let sentGestures = null;
|
|
714
|
+
let warnedGesture = false;
|
|
715
|
+
const viewport = () => ({
|
|
716
|
+
width: document.documentElement.clientWidth || window.innerWidth,
|
|
717
|
+
height: document.documentElement.clientHeight || window.innerHeight
|
|
718
|
+
});
|
|
719
|
+
const readBox = () => {
|
|
720
|
+
const r = wrapper.getBoundingClientRect();
|
|
721
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
722
|
+
};
|
|
723
|
+
const wantGestures = () => ({ move: draggableOn && caps?.gestures?.move === true, resize: resizeLimits !== null && caps?.gestures?.resize === true });
|
|
724
|
+
function applyBox(b, inlineBase, withSize) {
|
|
725
|
+
if (withSize) {
|
|
726
|
+
ws.width = `${b.width}px`;
|
|
727
|
+
ws.height = `${b.height}px`;
|
|
728
|
+
ws.maxWidth = "none";
|
|
729
|
+
}
|
|
730
|
+
if (position === "inline") {
|
|
731
|
+
const base = inlineBase ?? { left: b.left - inlineShift.x, top: b.top - inlineShift.y };
|
|
732
|
+
inlineShift = { x: b.left - base.left, y: b.top - base.top };
|
|
733
|
+
ws.setProperty("translate", `${inlineShift.x}px ${inlineShift.y}px`);
|
|
734
|
+
} else {
|
|
735
|
+
ws.left = `${b.left}px`;
|
|
736
|
+
ws.top = `${b.top}px`;
|
|
737
|
+
ws.right = "auto";
|
|
738
|
+
ws.bottom = "auto";
|
|
739
|
+
}
|
|
740
|
+
userMoved = true;
|
|
741
|
+
invalidateRect();
|
|
742
|
+
}
|
|
743
|
+
let selectionLock = null;
|
|
744
|
+
function lockSelection(on) {
|
|
745
|
+
const de = document.documentElement;
|
|
746
|
+
if (on && !selectionLock) {
|
|
747
|
+
selectionLock = { us: de.style.getPropertyValue("user-select"), wus: de.style.getPropertyValue("-webkit-user-select") };
|
|
748
|
+
de.style.setProperty("user-select", "none");
|
|
749
|
+
de.style.setProperty("-webkit-user-select", "none");
|
|
750
|
+
try {
|
|
751
|
+
window.getSelection()?.removeAllRanges();
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
} else if (!on && selectionLock) {
|
|
755
|
+
de.style.setProperty("user-select", selectionLock.us);
|
|
756
|
+
de.style.setProperty("-webkit-user-select", selectionLock.wus);
|
|
757
|
+
selectionLock = null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function endActiveGesture(kind) {
|
|
761
|
+
if (!activeGesture || kind && activeGesture.kind !== kind) return;
|
|
762
|
+
gates[activeGesture.kind].close();
|
|
763
|
+
const b = readBox();
|
|
764
|
+
emit(activeGesture.kind, { phase: "end", ...b });
|
|
765
|
+
activeGesture = null;
|
|
766
|
+
lockSelection(false);
|
|
767
|
+
}
|
|
768
|
+
function onGesture(kind, raw) {
|
|
769
|
+
if (destroyed || !caps?.gestures) return;
|
|
770
|
+
if (kind === "move" ? !draggableOn : resizeLimits === null) return;
|
|
771
|
+
const p = parseGesturePayload(kind, raw);
|
|
772
|
+
if (!p) return;
|
|
773
|
+
if (p.phase === "start" ? activeGesture !== null : activeGesture?.kind !== kind) return;
|
|
774
|
+
if (!gates[kind].accept(p)) return;
|
|
775
|
+
if (p.phase === "start") {
|
|
776
|
+
const start = readBox();
|
|
777
|
+
activeGesture = {
|
|
778
|
+
kind,
|
|
779
|
+
start,
|
|
780
|
+
corner: p.corner,
|
|
781
|
+
inlineBase: position === "inline" ? { left: start.left - inlineShift.x, top: start.top - inlineShift.y } : null
|
|
782
|
+
};
|
|
783
|
+
lockSelection(true);
|
|
784
|
+
emit(kind, { phase: "start", ...start });
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const a = activeGesture;
|
|
788
|
+
const vp = viewport();
|
|
789
|
+
const next = kind === "move" ? moveBox(a.start, p.totalDx, p.totalDy, vp) : resizeBox(a.start, a.corner, p.totalDx, p.totalDy, resizeLimits, vp);
|
|
790
|
+
applyBox(next, a.inlineBase, kind === "resize");
|
|
791
|
+
emit(kind, { phase: p.phase, ...next });
|
|
792
|
+
if (p.phase === "end") {
|
|
793
|
+
activeGesture = null;
|
|
794
|
+
lockSelection(false);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function syncGestures() {
|
|
798
|
+
if (destroyed) return;
|
|
799
|
+
const want = wantGestures();
|
|
800
|
+
if (!want.move) endActiveGesture("move");
|
|
801
|
+
if (!want.resize) endActiveGesture("resize");
|
|
802
|
+
if (!port || !caps) return;
|
|
803
|
+
warnGestureUnsupported();
|
|
804
|
+
if (sentGestures && sentGestures.move === want.move && sentGestures.resize === want.resize) return;
|
|
805
|
+
sentGestures = want;
|
|
806
|
+
if (caps.gestures) port.postMessage(xcMessage("xc.setConfig", { gestures: want }));
|
|
807
|
+
}
|
|
808
|
+
function warnGestureUnsupported() {
|
|
809
|
+
if (warnedGesture || !caps || caps.gestures) return;
|
|
810
|
+
const needMove = draggableOn;
|
|
811
|
+
if (!needMove && resizeLimits === null) return;
|
|
812
|
+
warnedGesture = true;
|
|
813
|
+
const what = [needMove ? "draggable" : "", resizeLimits !== null ? "resizable" : ""].filter(Boolean).join(" / ");
|
|
814
|
+
emit("error", { code: "unsupported", message: `${what} needs gesture support (capabilities.gestures), which this /embed deployment does not advertise (older version)`, command: "gestures" });
|
|
815
|
+
}
|
|
816
|
+
const onWindowResize = () => {
|
|
817
|
+
if (!userMoved || activeGesture || destroyed) return;
|
|
818
|
+
const cur = readBox();
|
|
819
|
+
const fit = fitBox(cur, viewport());
|
|
820
|
+
if (fit.left === cur.left && fit.top === cur.top && fit.width === cur.width && fit.height === cur.height) return;
|
|
821
|
+
applyBox(fit, position === "inline" ? { left: cur.left - inlineShift.x, top: cur.top - inlineShift.y } : null, fit.width !== cur.width || fit.height !== cur.height);
|
|
822
|
+
};
|
|
823
|
+
window.addEventListener("resize", onWindowResize, { passive: true });
|
|
824
|
+
scheduleLazy();
|
|
825
|
+
const api = {
|
|
826
|
+
element: wrapper,
|
|
827
|
+
get iframe() {
|
|
828
|
+
return iframe;
|
|
829
|
+
},
|
|
830
|
+
ready,
|
|
831
|
+
activate,
|
|
832
|
+
say: (text, opts) => send("xc.say", { text, mode: opts?.mode }, true),
|
|
833
|
+
speakAudio,
|
|
834
|
+
speakAudioStream,
|
|
835
|
+
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
836
|
+
expression: (name) => send("xc.expression", { name }),
|
|
837
|
+
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
838
|
+
setOutfit: (id) => {
|
|
839
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid outfit id"));
|
|
840
|
+
return send("xc.setOutfit", { id }, true, void 0, void 0, "outfits");
|
|
841
|
+
},
|
|
842
|
+
setScene: (id) => {
|
|
843
|
+
if (!isXcId(id)) return Promise.reject(new Error("[bad_request] invalid scene id"));
|
|
844
|
+
return send("xc.setScene", { id }, true, void 0, void 0, "scenes");
|
|
845
|
+
},
|
|
846
|
+
prefetch: (ids) => {
|
|
847
|
+
if (ids !== void 0 && (!Array.isArray(ids) || ids.some((x) => !isXcId(x)))) {
|
|
848
|
+
return Promise.reject(new Error("[bad_request] prefetch ids must be an array of outfit ids"));
|
|
849
|
+
}
|
|
850
|
+
return send("xc.prefetch", ids ? { ids } : {}, true, void 0, void 0, "prefetch");
|
|
851
|
+
},
|
|
852
|
+
getOutfits: async () => {
|
|
853
|
+
if (!iframe) activate();
|
|
854
|
+
await handshaked;
|
|
855
|
+
return caps?.outfits ? caps.outfits.slice() : [];
|
|
856
|
+
},
|
|
857
|
+
getScenes: async () => {
|
|
858
|
+
if (!iframe) activate();
|
|
859
|
+
await handshaked;
|
|
860
|
+
return caps?.scenes ? caps.scenes.slice() : [];
|
|
861
|
+
},
|
|
862
|
+
get outfit() {
|
|
863
|
+
return currentOutfit;
|
|
864
|
+
},
|
|
865
|
+
get scene() {
|
|
866
|
+
return currentScene;
|
|
867
|
+
},
|
|
868
|
+
setModel: (m) => {
|
|
869
|
+
const msg = typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m;
|
|
870
|
+
if (msg.url !== void 0 && msg.outfit === void 0 && !options.allowCustomModel) {
|
|
871
|
+
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";
|
|
872
|
+
emit("error", { code: "unsupported", message, command: "xc.setModel" });
|
|
873
|
+
return Promise.reject(new Error(`[unsupported] ${message}`));
|
|
874
|
+
}
|
|
875
|
+
return send("xc.setModel", msg);
|
|
876
|
+
},
|
|
877
|
+
setConfig: (cfg) => send("xc.setConfig", cfg),
|
|
878
|
+
setSize(width, height) {
|
|
879
|
+
if (destroyed) return;
|
|
880
|
+
ws.width = css(width, "320px");
|
|
881
|
+
ws.height = css(height, "480px");
|
|
882
|
+
invalidateRect();
|
|
883
|
+
},
|
|
884
|
+
getBox: () => readBox(),
|
|
885
|
+
setBorderRadius(radius) {
|
|
886
|
+
radiusOpt = radius;
|
|
887
|
+
applyRadius();
|
|
888
|
+
},
|
|
889
|
+
setDraggable(on) {
|
|
890
|
+
draggableOn = on === true;
|
|
891
|
+
syncGestures();
|
|
892
|
+
},
|
|
893
|
+
setResizable(on) {
|
|
894
|
+
resizeLimits = normalizeResizable(on);
|
|
895
|
+
syncGestures();
|
|
896
|
+
},
|
|
897
|
+
startListening: () => send("xc.mic", { enabled: true }),
|
|
898
|
+
stopListening: () => send("xc.mic", { enabled: false }),
|
|
899
|
+
mic: (enabled) => send("xc.mic", { enabled }),
|
|
900
|
+
pause() {
|
|
901
|
+
userPaused = true;
|
|
902
|
+
syncPause();
|
|
903
|
+
},
|
|
904
|
+
resume() {
|
|
905
|
+
userPaused = false;
|
|
906
|
+
syncPause();
|
|
907
|
+
},
|
|
908
|
+
destroy() {
|
|
909
|
+
if (destroyed) return;
|
|
910
|
+
try {
|
|
911
|
+
port?.postMessage(xcMessage("xc.destroy"));
|
|
912
|
+
} catch {
|
|
913
|
+
}
|
|
914
|
+
destroyed = true;
|
|
915
|
+
cancelIdle();
|
|
916
|
+
io?.disconnect();
|
|
917
|
+
pauseIo?.disconnect();
|
|
918
|
+
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
919
|
+
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
920
|
+
window.removeEventListener("message", onWindowMessage);
|
|
921
|
+
window.removeEventListener("resize", onWindowResize);
|
|
922
|
+
lockSelection(false);
|
|
923
|
+
disablePassthrough();
|
|
924
|
+
resolveHandshake();
|
|
925
|
+
try {
|
|
926
|
+
port?.close();
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
port = null;
|
|
930
|
+
for (const p of pending.values()) p.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
931
|
+
pending.clear();
|
|
932
|
+
queue.length = 0;
|
|
933
|
+
if (!loaded) rejectReady(new Error("[project-xiaochun] destroyed before ready"));
|
|
934
|
+
wrapper.remove();
|
|
935
|
+
iframe = null;
|
|
936
|
+
emit("destroy", void 0);
|
|
937
|
+
listeners.clear();
|
|
938
|
+
},
|
|
939
|
+
on(event, cb) {
|
|
940
|
+
let set = listeners.get(event);
|
|
941
|
+
if (!set) {
|
|
942
|
+
set = /* @__PURE__ */ new Set();
|
|
943
|
+
listeners.set(event, set);
|
|
944
|
+
}
|
|
945
|
+
set.add(cb);
|
|
946
|
+
return () => {
|
|
947
|
+
set.delete(cb);
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
return api;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export {
|
|
955
|
+
createXiaochun
|
|
956
|
+
};
|
|
957
|
+
//# sourceMappingURL=chunk-KQSKHPVF.js.map
|