(() => { "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() {} }; } })();