Spaces:
Runtime error
Runtime error
| // RealWonder Interactive Demo - Frontend JavaScript | |
| (function() { | |
| "use strict"; | |
| const socket = io(); | |
| const canvas = document.getElementById("videoCanvas"); | |
| const ctx = canvas.getContext("2d"); | |
| const loadingPanel = document.getElementById("loadingPanel"); | |
| const controlsPanel = document.getElementById("controlsPanel"); | |
| const caseSelect = document.getElementById("caseSelect"); | |
| const caseSelectorGroup = document.getElementById("caseSelectorGroup"); | |
| const caseDivider = document.getElementById("caseDivider"); | |
| const promptInput = document.getElementById("promptInput"); | |
| const startBtn = document.getElementById("startBtn"); | |
| const stopBtn = document.getElementById("stopBtn"); | |
| const resetBtn = document.getElementById("resetBtn"); | |
| const statusBar = document.getElementById("statusBar"); | |
| const progressBar = document.getElementById("progressBar"); | |
| const progressText = document.getElementById("progressText"); | |
| const frameCounter = document.getElementById("frameCounter"); | |
| const forceConfigContainer = document.getElementById("forceConfigContainer"); | |
| const objectSelect = document.getElementById("objectSelect"); | |
| const forceViewerWrap = document.getElementById("forceViewerWrap"); | |
| const videoPanel = document.getElementById("videoPanel"); | |
| let frameCount = 0; | |
| let isGenerating = false; | |
| let allowChangeForce = false; | |
| // Per-object state: { forceX, forceY, forceZ, maxStrength } | |
| // forceX = world left(-)/right(+) | |
| // forceY = world out(-)/in(+) (depth) | |
| // forceZ = world down(-)/up(+) | |
| let objectForces = []; | |
| let objectConfigs = []; | |
| let objectCentroids = []; // [{x, y}] in canvas pixel coords | |
| let maskImages = []; | |
| let previewImage = null; | |
| let selectedObjectIdx = -1; | |
| // ---- Three.js 3D viewer ---- | |
| var renderer3d = null; | |
| var scene3d = null; | |
| var camera3d = null; | |
| var forceArrow3d = null; | |
| var viewer3dReady = false; | |
| // Axis config: [worldKey, label, negLabel, posLabel, dotColor, threeAxis] | |
| const AXES = [ | |
| { key: "forceX", name: "Left / Right", neg: "← Left", pos: "Right →", color: "#d4756b" }, | |
| { key: "forceZ", name: "Down / Up", neg: "↓ Down", pos: "Up ↑", color: "#6baa7a" }, | |
| { key: "forceY", name: "Out / In", neg: "◄ Out", pos: "In ►", color: "#6b8aed" }, | |
| ]; | |
| // ---- SocketIO ---- | |
| socket.on("connect", function() { setStatus("Connected \u2014 waiting for server data..."); }); | |
| socket.on("disconnect", function() { setStatus("Disconnected from server"); }); | |
| // "init" is emitted once on connect; it carries the list of all cases | |
| // plus the active case data. We build the case dropdown, then apply | |
| // the active case data as if it were a "ready" event. | |
| socket.on("init", function(data) { | |
| var caseList = data.cases || []; | |
| // Build (or rebuild) the case dropdown | |
| caseSelect.innerHTML = ""; | |
| for (var i = 0; i < caseList.length; i++) { | |
| var opt = document.createElement("option"); | |
| opt.value = caseList[i].name; | |
| opt.textContent = caseList[i].display_name || caseList[i].name; | |
| if (caseList[i].name === data.active_case) opt.selected = true; | |
| caseSelect.appendChild(opt); | |
| } | |
| // Show the scene selector only when there are multiple cases | |
| if (caseList.length > 1) { | |
| caseSelectorGroup.style.display = ""; | |
| caseDivider.style.display = ""; | |
| } | |
| // Treat the active case data exactly like a "ready" event | |
| handleReadyData(data); | |
| }); | |
| socket.on("ready", function(data) { | |
| handleReadyData(data); | |
| }); | |
| // Shared handler for both "init" (active case portion) and "ready" (case switch) | |
| function handleReadyData(data) { | |
| setStatus("Ready. Set force direction with the sliders, then click Start."); | |
| // Update case selector value if case changed server-side | |
| if (data.active_case && caseSelect) { | |
| caseSelect.value = data.active_case; | |
| } else if (data.case_name && caseSelect) { | |
| caseSelect.value = data.case_name; | |
| } | |
| if (data.preview) { | |
| previewImage = new Image(); | |
| previewImage.onload = function() { redrawCanvas(); }; | |
| previewImage.src = "data:image/jpeg;base64," + data.preview; | |
| } | |
| if (data.prompt) promptInput.value = data.prompt; | |
| if (data.ui_config) { | |
| allowChangeForce = !!data.ui_config.allow_change_force; | |
| buildForceControls(data.ui_config); | |
| } | |
| loadingPanel.style.display = "none"; | |
| controlsPanel.style.display = "block"; | |
| enableControls(true); | |
| } | |
| // ---- Case selector ---- | |
| caseSelect.addEventListener("change", function() { | |
| if (isGenerating) return; | |
| // Clear canvas + reset progress for the new case | |
| frameCount = 0; | |
| frameCounter.textContent = "Frame: 0"; | |
| progressBar.style.width = "0%"; | |
| progressText.textContent = ""; | |
| previewImage = null; | |
| redrawCanvas(); | |
| socket.emit("select_case", { case_name: caseSelect.value }); | |
| }); | |
| socket.on("frame", function(data) { | |
| frameCount++; | |
| frameCounter.textContent = "Frame: " + frameCount; | |
| var img = new Image(); | |
| img.onload = function() { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); }; | |
| img.src = "data:image/jpeg;base64," + data.data; | |
| }); | |
| socket.on("status", function(data) { | |
| setStatus(data.message); | |
| if (data.block !== undefined && data.total_blocks !== undefined) { | |
| var pct = ((data.block + 1) / data.total_blocks * 100).toFixed(0); | |
| progressBar.style.width = pct + "%"; | |
| progressText.textContent = "Block " + (data.block + 1) + "/" + data.total_blocks; | |
| } | |
| }); | |
| socket.on("error", function(data) { setStatus("Error: " + data.message); }); | |
| socket.on("generation_complete", function() { | |
| setStatus("Generation complete. Click Reset to run again."); | |
| progressBar.style.width = "100%"; | |
| isGenerating = false; | |
| enableControls(true); | |
| showForceViewer(true); | |
| }); | |
| // ---- Centroid from mask ---- | |
| function computeMaskCentroid(maskImg) { | |
| var off = document.createElement("canvas"); | |
| off.width = canvas.width; off.height = canvas.height; | |
| var offCtx = off.getContext("2d"); | |
| offCtx.drawImage(maskImg, 0, 0, canvas.width, canvas.height); | |
| var data = offCtx.getImageData(0, 0, canvas.width, canvas.height).data; | |
| var sx = 0, sy = 0, n = 0; | |
| for (var i = 0; i < data.length; i += 4) { | |
| if (data[i] > 128) { | |
| sx += (i / 4) % canvas.width; | |
| sy += Math.floor((i / 4) / canvas.width); | |
| n++; | |
| } | |
| } | |
| return n === 0 | |
| ? { x: canvas.width / 2, y: canvas.height / 2 } | |
| : { x: sx / n, y: sy / n }; | |
| } | |
| // ---- Build controls (once per ready) ---- | |
| function buildForceControls(uiConfig) { | |
| objectConfigs = uiConfig.objects || []; | |
| objectForces = []; | |
| objectCentroids = []; | |
| maskImages = []; | |
| objectSelect.innerHTML = ""; | |
| for (var i = 0; i < objectConfigs.length; i++) { | |
| var obj = objectConfigs[i]; | |
| objectForces.push({ forceX: 0, forceY: 0, forceZ: 0, maxStrength: obj.max_strength || 2.0 }); | |
| objectCentroids.push({ x: canvas.width / 2, y: canvas.height / 2 }); | |
| var maskImg = null; | |
| if (obj.mask_b64) { | |
| maskImg = new Image(); | |
| (function(idx, img) { | |
| img.onload = function() { | |
| objectCentroids[idx] = computeMaskCentroid(img); | |
| if (idx === selectedObjectIdx) { | |
| positionForceViewer(idx); | |
| } | |
| redrawCanvas(); // always redraw — harmless if not selected | |
| }; | |
| img.src = "data:image/png;base64," + obj.mask_b64; | |
| })(i, maskImg); | |
| } | |
| maskImages.push(maskImg); | |
| var opt = document.createElement("option"); | |
| opt.value = i; opt.textContent = obj.label || ("Object " + obj.idx); | |
| objectSelect.appendChild(opt); | |
| } | |
| objectSelect.onchange = function() { | |
| showObjectControls(parseInt(objectSelect.value)); | |
| }; | |
| setup3DViewer(); | |
| if (objectConfigs.length > 0) { | |
| selectedObjectIdx = 0; | |
| objectSelect.value = "0"; | |
| showObjectControls(0); | |
| } | |
| } | |
| // ---- Per-object controls ---- | |
| function showObjectControls(idx) { | |
| selectedObjectIdx = idx; | |
| redrawCanvas(); | |
| positionForceViewer(idx); | |
| forceConfigContainer.innerHTML = ""; | |
| if (idx < 0 || idx >= objectConfigs.length) return; | |
| var obj = objectConfigs[idx]; | |
| var force = objectForces[idx]; | |
| var maxStr = force.maxStrength; | |
| var group = document.createElement("div"); | |
| group.className = "object-force-group"; | |
| var titleEl = document.createElement("div"); | |
| titleEl.className = "object-force-label"; | |
| titleEl.textContent = obj.label || ("Object " + obj.idx); | |
| group.appendChild(titleEl); | |
| // Force summary | |
| var summary = document.createElement("div"); | |
| summary.id = "forceSummary_" + idx; | |
| summary.className = "force-summary force-none"; | |
| summary.textContent = "No force set"; | |
| group.appendChild(summary); | |
| // Three axis sliders | |
| AXES.forEach(function(ax) { | |
| group.appendChild(makeAxisSlider(idx, ax, maxStr, force[ax.key])); | |
| }); | |
| // Reset force button | |
| var resetForceBtn = document.createElement("button"); | |
| resetForceBtn.className = "reset-force-btn"; | |
| resetForceBtn.textContent = "Reset force to zero"; | |
| resetForceBtn.addEventListener("click", function() { | |
| objectForces[idx].forceX = 0; | |
| objectForces[idx].forceY = 0; | |
| objectForces[idx].forceZ = 0; | |
| AXES.forEach(function(ax) { | |
| var sl = document.getElementById("axisSlider_" + idx + "_" + ax.key); | |
| var vl = document.getElementById("axisVal_" + idx + "_" + ax.key); | |
| if (sl) sl.value = "0"; | |
| if (vl) vl.textContent = "0.0"; | |
| }); | |
| update3DArrow(0, 0, 0); | |
| updateForceSummary(idx); | |
| if (isGenerating && allowChangeForce) emitForceUpdate(); | |
| }); | |
| group.appendChild(resetForceBtn); | |
| forceConfigContainer.appendChild(group); | |
| updateForceSummary(idx); | |
| update3DArrow(force.forceX, force.forceY, force.forceZ); | |
| } | |
| function makeAxisSlider(objIdx, ax, maxStr, initialValue) { | |
| var group = document.createElement("div"); | |
| group.className = "axis-slider-group"; | |
| // Header row: dot + name + value | |
| var header = document.createElement("div"); | |
| header.className = "axis-slider-header"; | |
| var dot = document.createElement("span"); | |
| dot.className = "axis-dot"; | |
| dot.style.background = ax.color; | |
| header.appendChild(dot); | |
| var name = document.createElement("span"); | |
| name.className = "axis-name"; | |
| name.textContent = ax.name; | |
| header.appendChild(name); | |
| var valEl = document.createElement("span"); | |
| valEl.className = "axis-value"; | |
| valEl.id = "axisVal_" + objIdx + "_" + ax.key; | |
| valEl.textContent = (initialValue || 0).toFixed(1); | |
| header.appendChild(valEl); | |
| group.appendChild(header); | |
| // Endpoint labels | |
| var endLabels = document.createElement("div"); | |
| endLabels.className = "axis-end-labels"; | |
| endLabels.innerHTML = "<span>" + ax.neg + "</span><span>" + ax.pos + "</span>"; | |
| group.appendChild(endLabels); | |
| // Slider | |
| var slider = document.createElement("input"); | |
| slider.type = "range"; | |
| slider.min = String(-maxStr); | |
| slider.max = String(maxStr); | |
| slider.step = "0.1"; | |
| slider.value = String(initialValue || 0); | |
| slider.id = "axisSlider_" + objIdx + "_" + ax.key; | |
| slider.addEventListener("input", function() { | |
| var val = parseFloat(slider.value); | |
| objectForces[objIdx][ax.key] = val; | |
| valEl.textContent = val.toFixed(1); | |
| var f = objectForces[objIdx]; | |
| update3DArrow(f.forceX, f.forceY, f.forceZ); | |
| updateForceSummary(objIdx); | |
| if (isGenerating && allowChangeForce) emitForceUpdate(); | |
| }); | |
| group.appendChild(slider); | |
| return group; | |
| } | |
| // ---- Force summary ---- | |
| function updateForceSummary(idx) { | |
| var el = document.getElementById("forceSummary_" + idx); | |
| if (!el) return; | |
| var f = objectForces[idx]; | |
| var str = getTotalStrength(f); | |
| var dir = get3DDirection(f); | |
| if (str < 0.01) { | |
| el.textContent = "No force set"; | |
| el.className = "force-summary force-none"; | |
| } else { | |
| el.innerHTML = | |
| "<span class='force-strength'>" + str.toFixed(1) + " units</span>" + | |
| "<span class='force-vec'>[" + | |
| dir[0].toFixed(2) + ", " + dir[1].toFixed(2) + ", " + dir[2].toFixed(2) + "]</span>"; | |
| el.className = "force-summary force-active"; | |
| } | |
| } | |
| // ---- 3D force helpers ---- | |
| function get3DDirection(force) { | |
| var len = Math.sqrt(force.forceX*force.forceX + force.forceY*force.forceY + force.forceZ*force.forceZ); | |
| if (len < 1e-6) return [0, 0, 0]; | |
| return [force.forceX / len, force.forceY / len, force.forceZ / len]; | |
| } | |
| function getTotalStrength(force) { | |
| var raw = Math.sqrt(force.forceX*force.forceX + force.forceY*force.forceY + force.forceZ*force.forceZ); | |
| return Math.min(raw, force.maxStrength || 2.0); | |
| } | |
| // ---- Three.js 3D viewer ---- | |
| function setup3DViewer() { | |
| if (typeof THREE === "undefined") { | |
| console.warn("Three.js not loaded — 3D viewer disabled."); | |
| return; | |
| } | |
| var viewerCanvas = document.getElementById("forceViewer3d"); | |
| if (!viewerCanvas) return; | |
| renderer3d = new THREE.WebGLRenderer({ canvas: viewerCanvas, antialias: true, alpha: true }); | |
| renderer3d.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer3d.setSize(160, 160); | |
| scene3d = new THREE.Scene(); | |
| camera3d = new THREE.PerspectiveCamera(42, 1.0, 0.1, 50); | |
| camera3d.position.set(1.6, 1.2, 1.9); | |
| camera3d.lookAt(0, 0, 0); | |
| // Origin sphere | |
| var originMesh = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.07, 16, 16), | |
| new THREE.MeshBasicMaterial({ color: 0x6b8aed }) | |
| ); | |
| scene3d.add(originMesh); | |
| // Reference axis arrows — darker tones for light background | |
| // World X (left-right) → Three.js +X (red) | |
| // World Z (up-down) → Three.js +Y (green) | |
| // World Y (in-out) → Three.js -Z (blue) | |
| var axLen = 0.65, hLen = 0.14, hW = 0.07; | |
| scene3d.add(new THREE.ArrowHelper(new THREE.Vector3(1,0,0), new THREE.Vector3(), axLen, 0xcc3333, hLen, hW)); | |
| scene3d.add(new THREE.ArrowHelper(new THREE.Vector3(0,1,0), new THREE.Vector3(), axLen, 0x33994a, hLen, hW)); | |
| scene3d.add(new THREE.ArrowHelper(new THREE.Vector3(0,0,-1), new THREE.Vector3(), axLen, 0x3355cc, hLen, hW)); | |
| // Force arrow (vivid orange, stands out on light background, initially hidden) | |
| forceArrow3d = new THREE.ArrowHelper( | |
| new THREE.Vector3(1, 0, 0), new THREE.Vector3(), 0.5, 0xe06010, 0.22, 0.10 | |
| ); | |
| forceArrow3d.visible = false; | |
| scene3d.add(forceArrow3d); | |
| scene3d.add(new THREE.AmbientLight(0xffffff, 1.0)); | |
| viewer3dReady = true; | |
| render3D(); | |
| } | |
| // forceX = world X, forceY = world Y (depth), forceZ = world Z (up) | |
| function update3DArrow(forceX, forceY, forceZ) { | |
| if (!viewer3dReady || !forceArrow3d) return; | |
| // Map world → Three.js: X→X, Z→Y(up), Y→-Z(into screen) | |
| var tx = forceX, ty = forceZ, tz = -forceY; | |
| var len = Math.sqrt(tx*tx + ty*ty + tz*tz); | |
| if (len < 0.01) { | |
| forceArrow3d.visible = false; | |
| } else { | |
| forceArrow3d.visible = true; | |
| forceArrow3d.setDirection(new THREE.Vector3(tx/len, ty/len, tz/len)); | |
| // Arrow length: scales 0.25–0.85 with force magnitude | |
| var maxStr = (selectedObjectIdx >= 0 && objectForces[selectedObjectIdx]) | |
| ? objectForces[selectedObjectIdx].maxStrength : 2.0; | |
| forceArrow3d.setLength(0.25 + 0.60 * Math.min(len / maxStr, 1.0), 0.20, 0.09); | |
| } | |
| render3D(); | |
| } | |
| function render3D() { | |
| if (renderer3d && scene3d && camera3d) { | |
| renderer3d.render(scene3d, camera3d); | |
| } | |
| } | |
| // Position the viewer wrap over the selected object's centroid on screen | |
| function positionForceViewer(idx) { | |
| if (!forceViewerWrap || !videoPanel) return; | |
| var centroid = objectCentroids[idx] || { x: canvas.width / 2, y: canvas.height / 2 }; | |
| var canvasRect = canvas.getBoundingClientRect(); | |
| var panelRect = videoPanel.getBoundingClientRect(); | |
| var scaleX = canvasRect.width / canvas.width; | |
| var scaleY = canvasRect.height / canvas.height; | |
| var cssCx = (canvasRect.left - panelRect.left) + centroid.x * scaleX; | |
| var cssCy = (canvasRect.top - panelRect.top) + centroid.y * scaleY; | |
| var viewerW = 160, viewerH = 185; // canvas + label | |
| var left = cssCx - viewerW / 2; | |
| var top = cssCy - viewerH - 12; // float above centroid | |
| // Clamp inside panel | |
| left = Math.max(4, Math.min(left, panelRect.width - viewerW - 4)); | |
| top = Math.max(4, Math.min(top, panelRect.height - viewerH - 4)); | |
| forceViewerWrap.style.left = left + "px"; | |
| forceViewerWrap.style.top = top + "px"; | |
| forceViewerWrap.style.display = "flex"; | |
| } | |
| function showForceViewer(visible) { | |
| if (!forceViewerWrap) return; | |
| if (visible) { | |
| positionForceViewer(selectedObjectIdx); | |
| } else { | |
| forceViewerWrap.style.display = "none"; | |
| } | |
| } | |
| // ---- Canvas drawing (preview + mask overlay only — no force arrow on canvas) ---- | |
| function redrawCanvas() { | |
| if (isGenerating) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| if (previewImage && previewImage.complete) { | |
| ctx.drawImage(previewImage, 0, 0, canvas.width, canvas.height); | |
| } else { | |
| ctx.fillStyle = "#e8ecf2"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| var idx = selectedObjectIdx; | |
| if (idx >= 0 && maskImages[idx] && maskImages[idx].complete) { | |
| drawMaskOverlay(maskImages[idx]); | |
| } | |
| } | |
| function drawMaskOverlay(maskImg) { | |
| var off = document.createElement("canvas"); | |
| off.width = canvas.width; off.height = canvas.height; | |
| var offCtx = off.getContext("2d"); | |
| offCtx.drawImage(maskImg, 0, 0, canvas.width, canvas.height); | |
| var d = offCtx.getImageData(0, 0, canvas.width, canvas.height); | |
| for (var i = 0; i < d.data.length; i += 4) { | |
| if (d.data[i] > 128) { | |
| d.data[i] = 80; d.data[i+1] = 140; d.data[i+2] = 237; d.data[i+3] = 100; | |
| } else { | |
| d.data[i+3] = 0; | |
| } | |
| } | |
| offCtx.putImageData(d, 0, 0); | |
| ctx.drawImage(off, 0, 0); | |
| } | |
| // ---- Collect and emit ---- | |
| function collectForces() { | |
| return objectForces.map(function(f, i) { | |
| return { | |
| obj_idx: i, | |
| direction: get3DDirection(f), | |
| strength: getTotalStrength(f), | |
| }; | |
| }); | |
| } | |
| function emitForceUpdate() { | |
| socket.emit("update_forces", { forces: collectForces() }); | |
| } | |
| // ---- Start / Stop / Reset ---- | |
| startBtn.addEventListener("click", function() { | |
| frameCount = 0; | |
| frameCounter.textContent = "Frame: 0"; | |
| progressBar.style.width = "0%"; | |
| progressText.textContent = ""; | |
| isGenerating = true; | |
| showForceViewer(false); | |
| enableControls(false); | |
| socket.emit("start_generation", { | |
| forces: collectForces(), | |
| prompt: promptInput.value || "A video of physical simulation", | |
| }); | |
| }); | |
| stopBtn.addEventListener("click", function() { socket.emit("stop_generation"); }); | |
| resetBtn.addEventListener("click", function() { | |
| frameCount = 0; | |
| frameCounter.textContent = "Frame: 0"; | |
| progressBar.style.width = "0%"; | |
| progressText.textContent = ""; | |
| isGenerating = false; | |
| socket.emit("reset"); | |
| }); | |
| document.addEventListener("keydown", function(e) { | |
| if (e.target.tagName === "TEXTAREA" || e.target.tagName === "INPUT") return; | |
| if (e.target.tagName === "SELECT") return; | |
| if (e.key === "Enter") startBtn.click(); | |
| }); | |
| // ---- Helpers ---- | |
| function enableControls(enabled) { | |
| startBtn.disabled = !enabled; | |
| promptInput.disabled = !enabled; | |
| if (caseSelect) caseSelect.disabled = !enabled; | |
| var forceEnabled = enabled || (isGenerating && allowChangeForce); | |
| objectSelect.disabled = !forceEnabled; | |
| var sliders = forceConfigContainer.querySelectorAll("input[type='range']"); | |
| for (var j = 0; j < sliders.length; j++) sliders[j].disabled = !forceEnabled; | |
| var resetBtns = forceConfigContainer.querySelectorAll(".reset-force-btn"); | |
| for (var k = 0; k < resetBtns.length; k++) resetBtns[k].disabled = !forceEnabled; | |
| } | |
| function setStatus(msg) { statusBar.textContent = "Status: " + msg; } | |
| // Placeholder | |
| ctx.fillStyle = "#e8ecf2"; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = "#9ca3b0"; | |
| ctx.font = "500 16px Inter, sans-serif"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText("Initializing...", canvas.width / 2, canvas.height / 2); | |
| })(); | |