FloodDiffusion2-Live / window_viewer.js
caiyiyi1998's picture
Initial commit
9a25493
Raw History Blame Contribute Delete
49.1 kB
(() => {
"use strict";
if (element.__motionViewer) return;
const stage = element.querySelector(".viewer-stage");
const actionLabel = element.querySelector(".viewer-action");
const statusLabel = element.querySelector(".viewer-status");
const slotLabel = element.querySelector(".viewer-slot-label");
const leadLabel = element.querySelector(".viewer-lead");
const historyLabel = element.querySelector(".viewer-history");
const bodyLabel = element.querySelector(".viewer-body-status");
const bodyRetry = element.querySelector(".viewer-body-retry");
const gamepadLabel = element.querySelector(".viewer-gamepad-status");
const joystickPad = element.querySelector(".viewer-joystick");
const joystickThumb = element.querySelector(".viewer-joystick-thumb");
const slotButtons = props.action_slots ? Array.from({length: 4}, (_, slot) => element.querySelector(`[data-action-slot="${slot}"]`)) : [];
const stat = Object.fromEntries(["runtime", "generation", "playback", "buffer"].map(k => [k, element.querySelector(`[data-stat="${k}"]`)]));
const slotColors = [0x397ad5, 0x8b5cdd, 0x2d9e72, 0xd55a9a];
const chains = [[0, 2, 5, 8, 11], [0, 1, 4, 7, 10], [0, 3, 6, 9, 12, 15], [9, 13, 16, 18, 20], [9, 14, 17, 19, 21]];
const edges = chains.flatMap(chain => chain.slice(1).map((b, i) => ({a: chain[i], b})));
const state = {session: null, retired: [], sequence: -1, accepted: -1, queue: [], meshTopology: null, body: null, playedItem: null, fps: 30, status: "idle", pose: null, displayedFrame: -1, displayedMeta: null, controls: [], target: null, spring: null, slots: ["", "", "", ""], speeds: [], serverSlot: 0, elapsed: 0, receivedAt: performance.now(), inference: 0, generated: 0, accumulator: 0, lastTick: 0, lastPlayedAt: null, count: 0, windowAt: performance.now(), playback: 0, lastStats: 0, disposed: false};
state.skeletonOnly = !!props.skeleton_only;
const keyboard = {held: new Set(), seq: 0, slot: 0, pendingSlot: false};
const gamepad = {index: null, horizontal: 0, forward: 0, run: false, buttons: [false, false, false, false], needsNeutral: false, enabled: false};
const joystick = {pointerId: null, horizontal: 0, forward: 0};
const cameraDefault = {yaw: Math.atan2(4.3, 5.3), pitch: Math.atan2(2.15, Math.hypot(4.3, 5.3)), radius: Math.hypot(4.3, 2.15, 5.3)};
const orbit = {...cameraDefault};
state.camera = orbit;
const cameraLimits = {minPitch: Math.PI / 36, maxPitch: Math.PI * 17 / 36, minRadius: 2, maxRadius: 30};
const fovRadians = 40 * Math.PI / 180;
let pointer = null, cameraControlTimer = null, lastCameraControlAt = -Infinity, lastControlAt = -Infinity;
const moveKeys = ["KeyW", "KeyA", "KeyS", "KeyD"];
const keys = new Set(moveKeys);
const finite = value => typeof value === "number" && Number.isFinite(value);
const halfScales = Array.from({length: 31}, (_, exponent) => 2 ** (exponent - 25));
const validSlot = value => Number.isInteger(value) && value >= 0 && value < 4;
const validPoint = point => point && finite(point.x) && finite(point.z);
const colorOf = point => slotColors[validSlot(point?.slot) ? point.slot : 0];
const cssColor = color => `#${color.toString(16).padStart(6, "0")}`;
const isLive = () => state.status === "start" || state.status === "running";
let view;
try { view = makeThree(); } catch (_) { stage.querySelectorAll("canvas").forEach(c => c.remove()); view = makeCanvas(); }
const canvas = view.canvas;
canvas.tabIndex = 0;
canvas.setAttribute("role", "application");
canvas.setAttribute("aria-label", "Motion control. WASD, on-screen joystick or left stick to move. Drag or right stick to orbit; scroll to zoom.");
element.__motionViewer = state;
let bodyLoadPending = false;
function loadBody() {
if (state.disposed) return;
bodyRetry.hidden = true;
if (state.skeletonOnly) {
bodyLabel.textContent = "";
trigger("body_ready", {ready: true});
return;
}
if (state.body) {
bodyLabel.textContent = "";
if (props.require_body_ready) trigger("body_ready", {ready: true});
return;
}
if (bodyLoadPending) { bodyLabel.textContent = "Loading body"; return; }
if (!view.supportsMesh) {
bodyLabel.textContent = props.require_body_ready ? "WebGL is required. Enable it and refresh this page." : "Skeleton view";
return;
}
if (!props.body_manifest_url || !props.body_data_url) return;
bodyLoadPending = true;
bodyLabel.textContent = "Loading body";
globalThis.MotionBody.load(props.body_manifest_url, props.body_data_url).then(model => {
if (state.disposed) return;
state.body = {...model.createDeformer(), topology: {vertexCount: model.vertexCount, faces: model.faces}, positions: new Float32Array(model.vertexCount * 3)};
bodyLabel.textContent = "";
if (state.playedItem) showPose(state.playedItem);
if (props.require_body_ready) trigger("body_ready", {ready: true});
}).catch(() => {
if (state.disposed) return;
if (state.skeletonOnly) { bodyLabel.textContent = ""; bodyRetry.hidden = true; return; }
bodyLabel.textContent = "Unable to load body"; bodyRetry.hidden = false;
}).finally(() => { bodyLoadPending = false; });
}
bodyRetry.addEventListener("click", loadBody); loadBody();
function setRepresentation(packet) {
const representation = typeof packet.representation === "string" ? packet.representation.toLowerCase() : "";
const model = typeof packet.model === "string" ? packet.model.toLowerCase() : "";
const requested = representation === "skeleton" ? true : representation === "mesh" ? false : model === "humanml3d" ? true : model === "seed" ? false : null;
const skeletonOnly = props.skeleton_only ? true : requested;
if (skeletonOnly === null || skeletonOnly === state.skeletonOnly) return;
state.skeletonOnly = skeletonOnly;
state.meshTopology = null;
// Preparation packets can change models before any new poses arrive.
// Discard the previous surface and all queued mesh attachments immediately.
view.clearMesh();
for (const item of [...state.queue, state.playedItem]) {
if (item) { item.mesh = null; item.meshPose = null; }
}
if (state.playedItem) showPose(state.playedItem);
loadBody();
}
function keyState() {
const keyboardMoves = moveKeys.some(code => keyboard.held.has(code));
const direction = joystick.pointerId !== null ? joystick : gamepad;
const horizontal = keyboardMoves ? Number(keyboard.held.has("KeyD")) - Number(keyboard.held.has("KeyA")) : direction.horizontal;
const forward = keyboardMoves ? Number(keyboard.held.has("KeyW")) - Number(keyboard.held.has("KeyS")) : direction.forward;
const norm = Math.max(1, Math.hypot(horizontal, forward));
const sine = Math.sin(orbit.yaw), cosine = Math.cos(orbit.yaw);
// Camera is on +front, so viewing-forward is -front on the ground plane.
// Pitch and zoom cannot change the requested ground speed.
const x = (cosine * horizontal - sine * forward) / norm;
const z = (-sine * horizontal - cosine * forward) / norm;
return {x: Math.abs(x) < 1e-12 ? 0 : x, z: Math.abs(z) < 1e-12 ? 0 : z, shift: false};
}
function cameraFrame() {
const sy = Math.sin(orbit.yaw), cy = Math.cos(orbit.yaw), sp = Math.sin(orbit.pitch), cp = Math.cos(orbit.pitch);
const front = [sy * cp, sp, cy * cp], right = [cy, 0, -sy], up = [-sy * sp, cp, -cy * sp];
const root = state.pose ? state.pose[0] : [0, 0, 0];
const target = [root[0], .85, root[2]];
return {front, right, up, target, position: target.map((v, i) => v + front[i] * orbit.radius)};
}
function cancelCameraControl() { if (cameraControlTimer !== null) clearTimeout(cameraControlTimer); cameraControlTimer = null; }
function cameraDirectionChanged(immediate = false) {
const direction = keyState();
if (!isLive() || (!direction.x && !direction.z)) { cancelCameraControl(); return; }
const now = performance.now(), remaining = 50 - (now - lastCameraControlAt);
if (immediate || remaining <= 0) { cancelCameraControl(); lastCameraControlAt = now; sendControl(); }
else if (cameraControlTimer === null) cameraControlTimer = setTimeout(() => { cameraControlTimer = null; cameraDirectionChanged(); }, remaining);
}
function updateKeyHUD() {
for (const code of moveKeys) {
element.querySelector(`[data-key="${code}"]`)?.classList.toggle("is-down", keyboard.held.has(code));
}
slotButtons.forEach((button, slot) => {
if (!button) return;
const selected = slot === keyboard.slot;
const prompt = state.slots[slot] || `Action ${slot + 1}`;
const title = `${slot + 1}: ${prompt}${finite(state.speeds[slot]) ? ` · Speed ${state.speeds[slot]}` : ""}`;
button.classList.toggle("is-selected", selected);
button.setAttribute("aria-pressed", String(selected));
button.setAttribute("aria-label", title); button.title = title;
});
}
function selectSlot(slot) {
if (!props.action_slots || !validSlot(slot) || slot === keyboard.slot) return;
keyboard.slot = slot; keyboard.pendingSlot = true;
updateKeyHUD(); sendControl();
}
function sendControl(force = false) {
if (!state.session || (!isLive() && !force)) return;
lastControlAt = performance.now();
trigger("control", {seq: ++keyboard.seq, ...keyState(), slot: props.action_slots ? keyboard.slot : 0, session_id: state.session});
}
function releaseKeys(force = false) {
const changed = keyboard.held.size > 0;
cancelCameraControl();
keyboard.held.clear(); updateKeyHUD();
if (changed || force) sendControl(true);
}
function clearGamepad(requireNeutral = false) {
const changed = !!(gamepad.horizontal || gamepad.forward || gamepad.run);
gamepad.horizontal = gamepad.forward = 0; gamepad.run = false; gamepad.enabled = false;
gamepad.buttons.fill(false);
if (requireNeutral) gamepad.needsNeutral = true;
return changed;
}
function clearJoystick() {
const id = joystick.pointerId;
joystick.pointerId = null; joystick.horizontal = joystick.forward = 0;
joystickPad?.classList.toggle("is-active", false);
if (joystickThumb) joystickThumb.style.transform = "translate(-50%, -50%)";
if (id !== null && joystickPad?.hasPointerCapture(id)) joystickPad.releasePointerCapture(id);
return id !== null;
}
function releaseInputs(force = false) { const changed = clearJoystick(); clearGamepad(true); releaseKeys(force || changed); }
function focusedElement() {
let focused = document.activeElement;
while (focused?.shadowRoot?.activeElement) focused = focused.shadowRoot.activeElement;
// The component can see its own shadow root even when document.activeElement
// exposes only that root's host rather than its focused canvas or editor.
const root = typeof canvas.getRootNode === "function" ? canvas.getRootNode() : null;
if (root?.activeElement && focused === root.host) focused = root.activeElement;
while (focused?.shadowRoot?.activeElement) focused = focused.shadowRoot.activeElement;
return focused;
}
function editableNode(node) {
return !!(node && (node.isContentEditable || (typeof node.matches === "function" && node.matches("input, textarea, select, [contenteditable]:not([contenteditable='false'])"))));
}
function pageHasFocus() { return !document.hidden && (typeof document.hasFocus !== "function" || document.hasFocus()); }
function gamepadFocus() {
return pageHasFocus() && !editableNode(focusedElement());
}
function stick(x, y) {
x = finite(x) ? Math.max(-1, Math.min(1, x)) : 0;
y = finite(y) ? Math.max(-1, Math.min(1, y)) : 0;
const length = Math.hypot(x, y), deadzone = .18;
if (length <= deadzone) return [0, 0];
const scale = Math.min(1, (length - deadzone) / (1 - deadzone)) / length;
return [x * scale, y * scale];
}
function gamepadStatus(text) { if (gamepadLabel && gamepadLabel.textContent !== text) gamepadLabel.textContent = text; }
function onGamepadDisconnected(event) {
if (event.gamepad?.index !== gamepad.index) return;
clearGamepad(true); gamepad.index = null; updateKeyHUD(); sendControl(true);
gamepadStatus("");
}
function pollGamepad(dt, now) {
let pads;
try {
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
if (clearGamepad()) { updateKeyHUD(); sendControl(true); }
gamepad.index = null; gamepadStatus("Controller unavailable in this browser"); return;
}
pads = Array.from(navigator.getGamepads() || []).filter(pad => pad && pad.connected !== false);
} catch (_) {
if (clearGamepad(true)) { updateKeyHUD(); sendControl(true); }
gamepad.index = null; gamepadStatus("Controller access unavailable"); return;
}
// Standard mapping is required: do not guess a vendor-specific axis layout.
const pad = pads.find(pad => pad.index === gamepad.index && pad.mapping === "standard") || pads.find(pad => pad.mapping === "standard");
if (!pad) {
if (gamepad.index !== null || gamepad.horizontal || gamepad.forward || gamepad.run) {
clearGamepad(true); gamepad.index = null; updateKeyHUD(); sendControl(true);
}
gamepadStatus(pads.length ? "Controller: standard mapping required" : "");
return;
}
if (pad.index !== gamepad.index) {
const changed = clearGamepad(gamepad.index !== null);
gamepad.index = pad.index;
if (changed) { updateKeyHUD(); sendControl(true); }
}
const left = stick(pad.axes?.[0], pad.axes?.[1]), right = stick(pad.axes?.[2], pad.axes?.[3]);
if (!gamepadFocus()) {
if (clearGamepad(true)) { updateKeyHUD(); sendControl(true); }
gamepadStatus(editableNode(focusedElement()) ? "Controller paused while editing" : "Controller paused · return to this page"); return;
}
if (gamepad.needsNeutral) {
if (left.some(Boolean) || right.some(Boolean)) {
gamepadStatus("Release sticks to resume"); return;
}
gamepad.needsNeutral = false;
}
gamepadStatus("Controller connected"); gamepad.enabled = true;
const previous = keyState();
gamepad.horizontal = left[0]; gamepad.forward = -left[1];
if (dt > 0 && right.some(Boolean)) {
orbit.yaw = ((orbit.yaw - right[0] * 2.2 * dt / 1000 + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
orbit.pitch = Math.max(cameraLimits.minPitch, Math.min(cameraLimits.maxPitch, orbit.pitch + right[1] * 1.5 * dt / 1000));
}
const current = keyState();
const releasedMotion = (previous.x || previous.z) && !current.x && !current.z;
if (releasedMotion || now - lastControlAt >= 50) sendControl();
}
function editable(event) {
const path = event.composedPath ? event.composedPath() : [event.target];
return path.some(editableNode);
}
function onKeyDown(event) {
const focused = focusedElement();
if (props.action_slots && /^(Digit|Numpad)[1-4]$/.test(event.code)) {
if (!pageHasFocus() || editable(event) || editableNode(focused) || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
event.preventDefault(); selectSlot(Number(event.code.at(-1)) - 1); return;
}
if (editable(event) || (focused !== canvas && (!joystickPad || focused !== joystickPad))) return;
const code = event.code;
if (!keys.has(code) && code !== "Escape") return;
event.preventDefault();
if (focused === joystickPad) canvas.focus({preventScroll: true});
if (code === "Escape") { releaseInputs(true); return; }
if (!keyboard.held.has(code)) { keyboard.held.add(code); updateKeyHUD(); sendControl(); }
}
function onKeyUp(event) {
// Always release a tracked key, including after focus moves into an editor.
if (keyboard.held.delete(event.code)) { if (!editable(event)) event.preventDefault(); updateKeyHUD(); sendControl(true); }
}
function endPointer() {
if (!pointer) return;
const id = pointer.id; pointer = null;
if (canvas.hasPointerCapture(id)) canvas.releasePointerCapture(id);
canvas.style.cursor = "grab";
}
function onBlur() { endPointer(); releaseInputs(true); }
function onCanvasBlur(event) {
endPointer(); clearJoystick();
if (!pageHasFocus() || editableNode(event.relatedTarget)) releaseInputs(true);
else releaseKeys(true);
}
function onFocusIn(event) { if (editable(event) || editableNode(focusedElement())) releaseInputs(true); }
function onVisibility() { if (document.hidden) onBlur(); }
function moveJoystick(event, force = false) {
const bounds = joystickPad.getBoundingClientRect();
const radius = Math.max(1, Math.min(bounds.width, bounds.height) * .35);
const dx = event.clientX - bounds.left - bounds.width / 2;
const dy = event.clientY - bounds.top - bounds.height / 2;
const scale = Math.max(radius, Math.hypot(dx, dy));
joystick.horizontal = dx / scale; joystick.forward = -dy / scale;
if (joystickThumb) joystickThumb.style.transform = `translate(calc(-50% + ${joystick.horizontal * radius}px), calc(-50% + ${-joystick.forward * radius}px))`;
if (force || (!joystick.horizontal && !joystick.forward) || performance.now() - lastControlAt >= 50) sendControl();
}
function onJoystickDown(event) {
event.preventDefault(); event.stopPropagation();
if (joystick.pointerId !== null || event.button !== 0) return;
endPointer(); canvas.focus({preventScroll: true});
joystick.pointerId = event.pointerId;
joystickPad.setPointerCapture(event.pointerId); joystickPad.classList.toggle("is-active", true);
moveJoystick(event, true);
}
function onJoystickMove(event) {
if (event.pointerId !== joystick.pointerId) return;
event.preventDefault(); event.stopPropagation(); moveJoystick(event);
}
function onJoystickUp(event) {
if (event.pointerId !== joystick.pointerId) return;
event.preventDefault(); event.stopPropagation(); clearJoystick(); sendControl(true);
}
function onJoystickLostCapture(event) {
if (event.pointerId === joystick.pointerId) { clearJoystick(); sendControl(true); }
}
function onPointerDown(event) {
if (editable(event) || pointer) return;
if (event.button === 1) { event.preventDefault(); return; }
if (![0, 2].includes(event.button)) return;
event.preventDefault(); event.stopPropagation(); canvas.focus({preventScroll: true});
pointer = {id: event.pointerId, button: event.button, x: event.clientX, y: event.clientY};
canvas.setPointerCapture(event.pointerId); canvas.style.cursor = "grabbing";
}
function onPointerMove(event) {
if (!pointer || event.pointerId !== pointer.id) return;
event.preventDefault(); event.stopPropagation();
const dx = event.clientX - pointer.x, dy = event.clientY - pointer.y;
pointer.x = event.clientX; pointer.y = event.clientY;
orbit.yaw = ((orbit.yaw - dx * .006 + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
orbit.pitch = Math.max(cameraLimits.minPitch, Math.min(cameraLimits.maxPitch, orbit.pitch + dy * .006));
if (dx) cameraDirectionChanged();
}
function onPointerUp(event) { if (pointer?.id === event.pointerId) { event.preventDefault(); endPointer(); cameraDirectionChanged(true); } }
function onPointerCancel(event) { if (pointer?.id === event.pointerId) { endPointer(); releaseInputs(true); } }
function onLostCapture(event) { if (pointer?.id === event.pointerId) { pointer = null; canvas.style.cursor = "grab"; cameraDirectionChanged(true); } }
function onWheel(event) {
if (editable(event)) return;
event.preventDefault(); event.stopPropagation();
const pixels = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? stage.clientHeight : 1);
orbit.radius = Math.max(cameraLimits.minRadius, Math.min(cameraLimits.maxRadius, orbit.radius * Math.exp(Math.max(-300, Math.min(300, pixels)) * .0015)));
}
function onResetView(event) {
if (editable(event)) return;
event.preventDefault(); endPointer(); Object.assign(orbit, cameraDefault); cameraDirectionChanged(true);
}
function onContextMenu(event) { event.preventDefault(); }
canvas.addEventListener("pointerdown", onPointerDown); canvas.addEventListener("pointermove", onPointerMove);
canvas.addEventListener("pointerup", onPointerUp); canvas.addEventListener("pointercancel", onPointerCancel);
canvas.addEventListener("lostpointercapture", onLostCapture); canvas.addEventListener("wheel", onWheel, {passive: false});
canvas.addEventListener("dblclick", onResetView); canvas.addEventListener("contextmenu", onContextMenu); canvas.addEventListener("blur", onCanvasBlur);
joystickPad?.addEventListener("pointerdown", onJoystickDown);
joystickPad?.addEventListener("pointermove", onJoystickMove);
joystickPad?.addEventListener("pointerup", onJoystickUp);
joystickPad?.addEventListener("pointercancel", onJoystickUp);
joystickPad?.addEventListener("lostpointercapture", onJoystickLostCapture);
joystickPad?.addEventListener("contextmenu", onContextMenu);
const slotClickHandlers = slotButtons.map((button, slot) => {
const handler = event => { event.preventDefault(); selectSlot(slot); };
button?.addEventListener("click", handler); return handler;
});
window.addEventListener("keydown", onKeyDown); window.addEventListener("keyup", onKeyUp); window.addEventListener("blur", onBlur);
window.addEventListener("gamepaddisconnected", onGamepadDisconnected);
document.addEventListener("visibilitychange", onVisibility);
document.addEventListener("focusin", onFocusIn);
const heartbeat = setInterval(() => {
if (!gamepadFocus()) { if (keyboard.held.size || joystick.pointerId !== null || gamepad.horizontal || gamepad.forward || gamepad.run) releaseInputs(true); return; }
if (focusedElement() !== canvas && keyboard.held.size) releaseKeys(true);
if ((keyboard.held.size || joystick.pointerId !== null || gamepad.enabled) && performance.now() - lastControlAt >= 50) sendControl();
}, 50);
function reset(session, packet) {
endPointer();
// The previous run has already stopped; discard its inputs locally.
clearJoystick(); clearGamepad(true); keyboard.held.clear(); updateKeyHUD();
if (state.session !== null) state.retired.push(state.session);
state.retired = state.retired.slice(-32);
Object.assign(state, {session, sequence: -1, accepted: -1, queue: [], meshTopology: null, playedItem: null, pose: null, displayedFrame: -1, displayedMeta: null, controls: [], target: null, spring: null, serverSlot: 0, elapsed: 0, inference: 0, generated: 0, accumulator: 0, lastTick: 0, lastPlayedAt: null, count: 0, playback: 0, windowAt: performance.now()});
keyboard.seq = 0; keyboard.slot = 0; keyboard.pendingSlot = false;
lastControlAt = -Infinity;
view.clear();
}
function binaryPayload(encoded, byteLength) {
if (typeof encoded !== "string" || encoded.length !== 4 * Math.ceil(byteLength / 3)) return null;
try {
const raw = atob(encoded);
if (raw.length !== byteLength) return null;
const bytes = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) bytes[i] = raw.charCodeAt(i);
return new DataView(bytes.buffer);
} catch (_) { return null; }
}
function receiveTopology(packet) {
if (state.skeletonOnly) return;
const topology = packet.mesh_topology;
if (!topology || topology.vertex_count !== 6890 || topology.face_count !== 13776) return;
if (state.meshTopology?.encoded === topology.faces) return;
const data = binaryPayload(topology.faces, topology.face_count * 3 * 2);
if (!data) return;
const faces = new Uint16Array(topology.face_count * 3);
for (let i = 0; i < faces.length; i++) {
faces[i] = data.getUint16(i * 2, true);
if (faces[i] >= topology.vertex_count) return;
}
state.meshTopology = {vertexCount: topology.vertex_count, faces, encoded: topology.faces};
}
function decodeMesh(packet) {
if (state.skeletonOnly) return null;
const mesh = packet.mesh, topology = state.meshTopology;
if (!mesh || !topology || !view.supportsMesh || mesh.vertex_count !== topology.vertexCount || !Array.isArray(packet.frames)) return null;
if (!Number.isInteger(mesh.frame_count) || mesh.frame_count < 1 || mesh.frame_count !== packet.frames.length) return null;
if (!Array.isArray(mesh.origins) || mesh.origins.length !== mesh.frame_count || !mesh.origins.every(p => Array.isArray(p) && p.length === 3 && p.every(finite))) return null;
const stride = mesh.vertex_count * 3, data = binaryPayload(mesh.vertices, mesh.frame_count * stride * 2);
if (!data) return null;
const frames = [];
for (let frame = 0; frame < mesh.frame_count; frame++) {
const positions = new Float32Array(stride), origin = mesh.origins[frame];
for (let i = 0; i < stride; i++) {
const bits = data.getUint16((frame * stride + i) * 2, true);
const exponent = (bits >>> 10) & 31, fraction = bits & 1023;
// The transport is root-relative IEEE float16; add the supplied world
// origin once, with no axis reflection, rescaling or root canonicalization.
if (exponent === 31) return null;
const relative = exponent ? (1024 + fraction) * halfScales[exponent] : fraction * 2 ** -24;
positions[i] = origin[i % 3] + ((bits & 0x8000) ? -relative : relative);
if (!Number.isFinite(positions[i])) return null;
}
frames.push({positions, topology});
}
return frames;
}
function decodeMeshPose(packet) {
if (state.skeletonOnly) return null;
const pose = packet.mesh_pose;
if (!pose || !view.supportsMesh || !Array.isArray(packet.frames) || !Number.isInteger(pose.frame_count) || pose.frame_count < 1 || pose.frame_count !== packet.frames.length) return null;
if (!Array.isArray(pose.origins) || pose.origins.length !== pose.frame_count || !pose.origins.every(p => Array.isArray(p) && p.length === 3 && p.every(finite))) return null;
const data = binaryPayload(pose.rotations, pose.frame_count * 22 * 9 * 4);
if (!data) return null;
return Array.from({length: pose.frame_count}, (_, frame) => {
const rotations = new Float32Array(198);
for (let i = 0; i < rotations.length; i++) {
rotations[i] = data.getFloat32((frame * 198 + i) * 4, true);
if (!Number.isFinite(rotations[i])) return null;
}
return {rotations, origin: pose.origins[frame]};
});
}
function receive(packet) {
if (!packet || typeof packet !== "object") return;
const session = String(packet.session_id ?? "");
if (state.retired.includes(session)) return;
const newSession = session !== state.session;
if (newSession) reset(session, packet);
const sequence = Number(packet.sequence);
if (Number.isFinite(sequence) && sequence < state.sequence) return;
const freshSequence = !Number.isFinite(sequence) || sequence > state.sequence;
if (newSession || freshSequence) setRepresentation(packet);
const previousStatus = state.status;
state.status = ["start", "running", "paused", "done", "error"].includes(packet.status) ? packet.status : "idle";
if (state.status === "paused" && previousStatus !== "paused") releaseInputs(true);
state.fps = finite(packet.fps) && packet.fps > 0 ? Math.min(120, packet.fps) : 30;
state.elapsed = finite(packet.elapsed_seconds) ? Math.max(0, packet.elapsed_seconds) : 0;
state.inference = finite(packet.inference_fps) ? Math.max(0, packet.inference_fps) : 0;
state.generated = finite(packet.generated_frames) ? Math.max(0, packet.generated_frames) : 0;
if (historyLabel && packet.prompt_history) {
const history = packet.prompt_history;
const frames = finite(history.visible_history) ? Math.max(0, history.visible_history) : 0;
historyLabel.textContent = `${frames} / ${history.max_history ?? 60}`;
} else if (historyLabel && packet.status === "start") {
historyLabel.textContent = "0 / 60";
}
state.receivedAt = performance.now();
if (Array.isArray(packet.slots)) state.slots = Array.from({length: 4}, (_, i) => typeof packet.slots[i] === "string" ? packet.slots[i] : "");
if (Array.isArray(packet.speeds)) state.speeds = packet.speeds;
if (freshSequence) {
if (validSlot(packet.active_slot)) state.serverSlot = packet.active_slot;
else if (validSlot(packet.base_slot)) state.serverSlot = packet.base_slot;
const serverSelection = validSlot(packet.base_slot) ? packet.base_slot : validSlot(packet.active_slot) ? packet.active_slot : null;
if (props.action_slots && !newSession && serverSelection !== null && (!keyboard.pendingSlot || serverSelection === keyboard.slot)) {
keyboard.slot = serverSelection; keyboard.pendingSlot = false;
}
}
if (!state.displayedMeta?.prompt) actionLabel.textContent = typeof packet.action === "string" && packet.action ? packet.action : "Motion";
if (Array.isArray(packet.control_points) && (!Number.isInteger(packet.control_replace_from_frame) || freshSequence)) {
// Only backend-accepted coordinates are drawn: no local integration,
// skeleton-position substitution, coordinate reflection or retiming.
const incoming = packet.control_points.filter(p => validPoint(p) && Number.isInteger(p.frame) && finite(p.heading));
if (Number.isInteger(packet.control_replace_from_frame) && packet.control_replace_from_frame >= 0) {
// A replan replaces the ENTIRE uncommitted suffix, not just coordinates
// present in both plans. Apply before accepting this packet's new motion:
// those new frames carry their final controls, while prior accepted frames
// (including ones still waiting for playback) must remain immutable.
const from = Math.max(packet.control_replace_from_frame, state.accepted + 1);
const byFrame = new Map(state.controls.filter(point => point.frame < from).map(point => [point.frame, point]));
incoming.filter(point => point.frame >= from).forEach(point => byFrame.set(point.frame, point));
state.controls = [...byFrame.values()].sort((a, b) => a.frame - b.frame).slice(-2000);
} else if (packet.control_points_delta === true) {
const byFrame = new Map(state.controls.map(point => [point.frame, point]));
incoming.forEach(point => byFrame.set(point.frame, point));
state.controls = [...byFrame.values()].sort((a, b) => a.frame - b.frame).slice(-2000);
} else {
const byFrame = new Map(incoming.map(point => [point.frame, point]));
state.controls = [...byFrame.values()].sort((a, b) => a.frame - b.frame).slice(-2000);
}
}
if (freshSequence) {
state.target = packet.target_point === null ? null : validPoint(packet.target_point) ? packet.target_point : state.target;
state.spring = packet.spring_point === null ? null : validPoint(packet.spring_point) ? packet.spring_point : state.controls.at(-1) ?? null;
}
view.controls(state.controls, state.displayedFrame, state.target, state.spring);
if (freshSequence) {
receiveTopology(packet);
const meshFrames = decodeMesh(packet);
const meshPoses = decodeMeshPose(packet);
const start = Math.max(0, Math.floor(Number(packet.start_frame) || 0));
if (Array.isArray(packet.frames)) packet.frames.forEach((frame, offset) => {
const index = start + offset;
if (index <= state.accepted || !Array.isArray(frame) || frame.length !== 22) return;
if (!frame.every(p => Array.isArray(p) && p.length === 3 && p.every(finite))) return;
const meta = Array.isArray(packet.frame_meta) ? packet.frame_meta[offset] : null;
state.queue.push({index, frame, meta: meta?.frame === index ? meta : null, mesh: meshFrames?.[offset] ?? null, meshPose: meshPoses?.[offset] ?? null});
state.accepted = index;
});
if (Number.isFinite(sequence)) state.sequence = sequence;
}
// Preserve the original playback/buffering policy by default for clean A/B.
// An optional cap affects only already generated poses, never the path plan.
const explicitMaximum = Number.isInteger(props.max_buffer_frames) && props.max_buffer_frames >= 1 ? props.max_buffer_frames : null;
const maximum = explicitMaximum ?? Math.max(3, Math.ceil(state.fps * .5));
if (state.queue.length > maximum) { state.queue.splice(0, state.queue.length - (explicitMaximum ?? Math.max(3, Math.ceil(state.fps * .2)))); state.accumulator = 0; }
if (!isLive() && (keyboard.held.size || joystick.pointerId !== null || gamepad.horizontal || gamepad.forward || gamepad.run)) releaseInputs(true);
updateKeyHUD();
}
function showPose(item) {
state.playedItem = item; state.pose = item.frame; state.displayedFrame = item.index; state.displayedMeta = item.meta;
if (typeof item.meta?.prompt === "string" && item.meta.prompt) actionLabel.textContent = item.meta.prompt;
let mesh = state.skeletonOnly ? null : item.mesh;
if (!state.skeletonOnly && !mesh && item.meshPose && state.body) {
const body = state.body;
mesh = {positions: body.deform(item.meshPose.rotations, item.meshPose.origin, body.positions), topology: body.topology};
}
view.pose(item.frame, item.meta, mesh);
view.controls(state.controls, state.displayedFrame, state.target, state.spring);
}
function tick(now) {
if (state.disposed) return;
if (!element.isConnected) { dispose(); return; }
const dt = state.lastTick ? Math.min(100, now - state.lastTick) : 0; state.lastTick = now;
pollGamepad(dt, now);
if (state.queue.length && state.status !== "error") {
state.accumulator += dt;
const interval = 1000 / state.fps;
let latest = null;
// Preserve every queue/timing advance, but deform only the pose that can
// actually reach this animation frame. Intermediate catch-up poses were
// previously deformed and assigned, then overwritten before render().
while (state.accumulator >= interval && state.queue.length) { latest = state.queue.shift(); state.accumulator -= interval; }
if (latest) { showPose(latest); state.count++; state.lastPlayedAt = now; }
if (!state.queue.length) state.accumulator = 0;
} else state.accumulator = 0;
if (now - state.windowAt >= 1000) { state.playback = state.count * 1000 / (now - state.windowAt); state.count = 0; state.windowAt = now; }
if (now - state.lastStats > 100) {
if (stat.runtime) stat.runtime.textContent = `${(state.elapsed + (isLive() ? (now - state.receivedAt) / 1000 : 0)).toFixed(1)} s`;
stat.generation.textContent = state.inference > 0 ? `${state.inference.toFixed(1)} fps` : "— fps";
stat.playback.textContent = `${state.playback.toFixed(0)} fps`;
if (stat.buffer) stat.buffer.textContent = `${(state.queue.length / state.fps).toFixed(2)} s`;
const latest = state.controls.at(-1)?.frame ?? -1;
const first = state.displayedFrame >= 0 ? state.displayedFrame : state.controls[0]?.frame ?? latest;
if (leadLabel) leadLabel.textContent = `Lead ${(Math.max(0, latest - first) / state.fps).toFixed(2)} s`;
state.lastStats = now;
}
// A healthy one-frame-at-a-time stream empties its queue after every pose.
// Show a stall only after actual playback has stopped for a sustained gap;
// metadata-only packets must not keep that gap hidden.
const buffering = !state.queue.length && state.lastPlayedAt !== null && now - state.lastPlayedAt >= 400;
const status = state.status === "error" ? "Error" : state.status === "paused" ? "Stopped" : state.status === "done" ? (state.queue.length ? "Finishing" : "Stopped") : isLive() ? (state.pose ? (buffering ? "Buffering" : "Live") : "Starting") : "Ready";
if (statusLabel.textContent !== status) statusLabel.textContent = status;
view.render(); state.raf = requestAnimationFrame(tick);
}
const resize = new ResizeObserver(() => view.resize()); resize.observe(stage);
function dispose() {
endPointer(); releaseInputs(true); state.disposed = true; cancelAnimationFrame(state.raf); clearInterval(heartbeat); resize.disconnect();
canvas.removeEventListener("pointerdown", onPointerDown); canvas.removeEventListener("pointermove", onPointerMove);
canvas.removeEventListener("pointerup", onPointerUp); canvas.removeEventListener("pointercancel", onPointerCancel);
canvas.removeEventListener("lostpointercapture", onLostCapture); canvas.removeEventListener("wheel", onWheel);
canvas.removeEventListener("dblclick", onResetView); canvas.removeEventListener("contextmenu", onContextMenu); canvas.removeEventListener("blur", onCanvasBlur);
joystickPad?.removeEventListener("pointerdown", onJoystickDown);
joystickPad?.removeEventListener("pointermove", onJoystickMove);
joystickPad?.removeEventListener("pointerup", onJoystickUp);
joystickPad?.removeEventListener("pointercancel", onJoystickUp);
joystickPad?.removeEventListener("lostpointercapture", onJoystickLostCapture);
joystickPad?.removeEventListener("contextmenu", onContextMenu);
slotButtons.forEach((button, slot) => button?.removeEventListener("click", slotClickHandlers[slot]));
window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keyup", onKeyUp); window.removeEventListener("blur", onBlur);
window.removeEventListener("gamepaddisconnected", onGamepadDisconnected);
document.removeEventListener("visibilitychange", onVisibility);
document.removeEventListener("focusin", onFocusIn);
bodyRetry.removeEventListener("click", loadBody);
state.queue = []; state.meshTopology = null; state.body = null; state.playedItem = null; view.dispose(); delete element.__motionViewer;
}
watch("value", () => receive(props.value)); receive(props.value); view.resize(); state.raf = requestAnimationFrame(tick);
function makeThree() {
const T = globalThis.THREE, renderer = new T.WebGLRenderer({antialias: true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setClearColor(0xf8fafb); stage.appendChild(renderer.domElement);
const scene = new T.Scene(), camera = new T.PerspectiveCamera(40, 1, .05, 150);
camera.up.set(0, 1, 0);
scene.add(new T.HemisphereLight(0xffffff, 0x8994a0, 1.2));
const light = new T.DirectionalLight(0xffffff, .65); light.position.set(3, 7, 5); scene.add(light);
const grid = new T.GridHelper(40, 80, 0xb8c7ce, 0xe0e7eb); grid.position.y = .002; scene.add(grid);
const group = new T.Group(); group.visible = false; scene.add(group);
const boneGeometry = new T.CylinderGeometry(.012, .012, 1, 8), jointGeometry = new T.SphereGeometry(.02, 10, 8);
const bodyMaterial = new T.MeshBasicMaterial({color: slotColors[0], transparent: true, opacity: .75, depthTest: false, depthWrite: false});
const bones = edges.map(() => { const m = new T.Mesh(boneGeometry, bodyMaterial); m.renderOrder = 20; group.add(m); return m; });
const joints = Array.from({length: 22}, () => { const m = new T.Mesh(jointGeometry, bodyMaterial); m.renderOrder = 21; group.add(m); return m; });
const meshMaterial = new T.MeshStandardMaterial({color: 0x9aaec2, roughness: .72, metalness: .02});
let surface = null, currentTopology = null;
function clearSurface() {
if (surface) { scene.remove(surface); surface.geometry.dispose(); }
surface = null; currentTopology = null;
}
function setTopology(topology) {
clearSurface(); currentTopology = topology;
const geometry = new T.BufferGeometry();
geometry.setIndex(new T.BufferAttribute(topology.faces, 1));
geometry.setAttribute("position", new T.BufferAttribute(new Float32Array(topology.vertexCount * 3), 3).setUsage(T.DynamicDrawUsage));
geometry.setAttribute("normal", new T.BufferAttribute(new Float32Array(topology.vertexCount * 3), 3).setUsage(T.DynamicDrawUsage));
surface = new T.Mesh(geometry, meshMaterial); surface.name = "motion-surface";
surface.visible = false; surface.frustumCulled = false; scene.add(surface);
}
function controlLine(pending, count = 2000) {
const geometry = new T.BufferGeometry();
geometry.setAttribute("position", new T.BufferAttribute(new Float32Array(count * 6), 3));
geometry.setAttribute("color", new T.BufferAttribute(new Float32Array(count * 6), 3)); geometry.setDrawRange(0, 0);
const options = {vertexColors: true, transparent: true, opacity: pending ? 1 : .55};
const material = pending ? new T.LineDashedMaterial({...options, dashSize: .12, gapSize: .075}) : new T.LineBasicMaterial(options);
const line = new T.LineSegments(geometry, material); line.frustumCulled = false; scene.add(line); return line;
}
const reached = controlLine(false), ahead = controlLine(true), connector = controlLine(true, 1);
const springMarker = new T.Mesh(new T.RingGeometry(.10, .145, 32), new T.MeshBasicMaterial({color: slotColors[0], side: T.DoubleSide}));
const targetMarker = new T.Mesh(new T.CircleGeometry(.115, 4), new T.MeshBasicMaterial({color: slotColors[0], side: T.DoubleSide}));
for (const marker of [springMarker, targetMarker]) { marker.rotation.x = -Math.PI / 2; marker.visible = false; scene.add(marker); }
const a = new T.Vector3(), b = new T.Vector3(), direction = new T.Vector3(), up = new T.Vector3(0, 1, 0), color = new T.Color();
function updateLine(line, count) { line.geometry.attributes.position.needsUpdate = true; line.geometry.attributes.color.needsUpdate = true; line.geometry.setDrawRange(0, count * 2); }
function updateMarker(marker, point, height) { marker.visible = !!point; if (point) { marker.position.set(point.x, height, point.z); marker.material.color.setHex(colorOf(point)); } }
return {
canvas: renderer.domElement,
supportsMesh: true,
clearMesh: clearSurface,
clear() { clearSurface(); group.visible = false; springMarker.visible = targetMarker.visible = false; [reached, ahead, connector].forEach(l => l.geometry.setDrawRange(0, 0)); },
pose(frame, meta, mesh) {
if (mesh?.topology && mesh.topology !== currentTopology) setTopology(mesh.topology);
const showMesh = !!(surface && mesh?.topology === currentTopology && mesh.positions.length === currentTopology.vertexCount * 3);
if (surface) surface.visible = showMesh;
if (showMesh) {
surface.geometry.attributes.position.array.set(mesh.positions);
surface.geometry.attributes.position.needsUpdate = true;
surface.geometry.computeVertexNormals();
}
bodyMaterial.opacity = showMesh ? .75 : 1;
group.visible = true; bodyMaterial.color.setHex(colorOf(meta));
frame.forEach((p, i) => joints[i].position.set(p[0], p[1], p[2]));
edges.forEach((edge, i) => { a.fromArray(frame[edge.a]); b.fromArray(frame[edge.b]); direction.subVectors(b, a); bones[i].position.copy(a).add(b).multiplyScalar(.5); bones[i].scale.set(1, direction.length(), 1); if (direction.lengthSq() > 1e-12) bones[i].quaternion.setFromUnitVectors(up, direction.normalize()); });
const root = frame[0]; grid.position.set(Math.round(root[0]), .002, Math.round(root[2]));
},
controls(points, displayed, target, spring) {
const counts = [0, 0], lines = [reached, ahead];
for (let i = 1; i < points.length; i++) {
const previous = points[i - 1], point = points[i], pending = point.frame > displayed ? 1 : 0, line = lines[pending], offset = counts[pending] * 6;
line.geometry.attributes.position.array.set([previous.x, .02, previous.z, point.x, .02, point.z], offset);
color.setHex(colorOf(point)); line.geometry.attributes.color.array.set([color.r, color.g, color.b, color.r, color.g, color.b], offset); counts[pending]++;
}
lines.forEach((line, i) => updateLine(line, counts[i])); ahead.computeLineDistances();
updateMarker(springMarker, spring, .024); updateMarker(targetMarker, target, .029);
if (target && spring) {
connector.geometry.attributes.position.array.set([spring.x, .022, spring.z, target.x, .022, target.z]);
connector.geometry.attributes.color.array.fill(.55); updateLine(connector, 1); connector.computeLineDistances();
} else connector.geometry.setDrawRange(0, 0);
},
resize() { const w = Math.max(1, stage.clientWidth), h = Math.max(1, stage.clientHeight); renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix(); },
render() { const basis = cameraFrame(); camera.position.fromArray(basis.position); camera.lookAt(...basis.target); renderer.render(scene, camera); },
dispose() { clearSurface(); [boneGeometry, jointGeometry, reached.geometry, ahead.geometry, connector.geometry, springMarker.geometry, targetMarker.geometry, grid.geometry].forEach(g => g.dispose()); [meshMaterial, bodyMaterial, reached.material, ahead.material, connector.material, springMarker.material, targetMarker.material, ...(Array.isArray(grid.material) ? grid.material : [grid.material])].forEach(m => m.dispose()); renderer.dispose(); }
};
}
function makeCanvas() {
const canvas = document.createElement("canvas"); stage.appendChild(canvas); const ctx = canvas.getContext("2d");
let width = 1, height = 1, pose = null, meta = null, points = [], displayed = -1, target = null, spring = null;
let projection = cameraFrame();
function project(p) {
const d = p.map((v, i) => v - projection.position[i]);
const depth = -d.reduce((v, x, i) => v + x * projection.front[i], 0);
if (depth <= .05) return null;
const scale = height / (2 * Math.tan(fovRadians / 2) * depth);
return [width / 2 + d.reduce((v, x, i) => v + x * projection.right[i], 0) * scale, height / 2 - d.reduce((v, x, i) => v + x * projection.up[i], 0) * scale];
}
function line(a, b, color, thickness) { a = project(a); b = project(b); if (!a || !b) return; ctx.strokeStyle = color; ctx.lineWidth = thickness; ctx.beginPath(); ctx.moveTo(...a); ctx.lineTo(...b); ctx.stroke(); }
return {
canvas,
supportsMesh: false,
clearMesh() {},
clear() { pose = null; points = []; target = spring = null; },
pose(p, m) { pose = p; meta = m; },
controls(p, f, t, s) { points = p; displayed = f; target = t; spring = s; },
resize() { width = Math.max(1, stage.clientWidth); height = Math.max(1, stage.clientHeight); const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = width * dpr; canvas.height = height * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); },
render() {
projection = cameraFrame();
ctx.fillStyle = "#f8fafb"; ctx.fillRect(0, 0, width, height); ctx.lineCap = "round"; ctx.setLineDash([]); ctx.globalAlpha = 1;
const root = pose ? pose[0] : [0, 0, 0], x = Math.round(root[0]), z = Math.round(root[2]);
for (let i = -12; i <= 12; i++) { line([x + i / 2, 0, z - 6], [x + i / 2, 0, z + 6], "#e0e7eb", 1); line([x - 6, 0, z + i / 2], [x + 6, 0, z + i / 2], "#e0e7eb", 1); }
let dashDistance = 0;
for (let i = 1; i < points.length; i++) {
const a = points[i - 1], b = points[i], pending = b.frame > displayed;
ctx.setLineDash(pending ? [7, 5] : []); ctx.lineDashOffset = -dashDistance; ctx.globalAlpha = pending ? 1 : .55;
line([a.x, .02, a.z], [b.x, .02, b.z], cssColor(colorOf(b)), pending ? 2.5 : 2);
const pa = project([a.x, .02, a.z]), pb = project([b.x, .02, b.z]); if (pa && pb) dashDistance += Math.hypot(pa[0] - pb[0], pa[1] - pb[1]);
}
ctx.globalAlpha = 1; ctx.lineDashOffset = 0;
if (target && spring) { ctx.setLineDash([4, 4]); line([target.x, .022, target.z], [spring.x, .022, spring.z], "#87949d", 1.5); }
ctx.setLineDash([]);
if (spring) for (let i = 0; i < 32; i++) { const a = i / 32 * Math.PI * 2, b = (i + 1) / 32 * Math.PI * 2; line([spring.x + Math.cos(a) * .13, .024, spring.z + Math.sin(a) * .13], [spring.x + Math.cos(b) * .13, .024, spring.z + Math.sin(b) * .13], cssColor(colorOf(spring)), 2); }
if (target) {
const corners = Array.from({length: 4}, (_, i) => { const angle = i / 4 * Math.PI * 2; return project([target.x + Math.cos(angle) * .115, .029, target.z + Math.sin(angle) * .115]); });
if (corners.every(Boolean)) { ctx.fillStyle = cssColor(colorOf(target)); ctx.beginPath(); corners.forEach((p, i) => i ? ctx.lineTo(...p) : ctx.moveTo(...p)); ctx.closePath(); ctx.fill(); }
}
if (!pose) return;
const body = cssColor(colorOf(meta));
[...edges].sort((a, b) => projection.front.reduce((sum, v, i) => sum + v * (pose[a.a][i] + pose[a.b][i] - pose[b.a][i] - pose[b.b][i]), 0)).forEach(edge => line(pose[edge.a], pose[edge.b], body, 4));
ctx.fillStyle = body; pose.forEach(p => { const q = project(p); if (q) { ctx.beginPath(); ctx.arc(q[0], q[1], 3, 0, 2 * Math.PI); ctx.fill(); } });
},
dispose() {}
};
}
})();