alphamotion-visor / player_shell.html
sajio's picture
Publish existing Viser review player with clip selector, timeline and contacts
f920668 verified
Raw History Blame Contribute Delete
6.78 kB
<!doctype html><html lang="zh"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>AlphaMotion · Visor</title>
<style>
:root{--bg:#101318;--panel:#171c23;--line:#303744;--text:#eef2f6;--muted:#a1adbd;--orange:#ff913d}*{box-sizing:border-box}html,body{margin:0;height:100%;font:14px system-ui;background:var(--bg);color:var(--text)}#app{height:100%;display:grid;grid-template-columns:245px 1fr;grid-template-rows:48px 1fr 102px}header{grid-column:1/3;display:flex;gap:24px;align-items:center;padding:0 18px;border-bottom:1px solid var(--line)}header b{letter-spacing:.1em}#status{color:var(--muted);font-size:12px}aside{grid-row:2/4;background:var(--panel);padding:14px;display:flex;flex-direction:column;gap:12px;border-right:1px solid var(--line);min-height:0}input[type=search]{width:100%;padding:10px;background:var(--bg);border:1px solid var(--line);border-radius:5px;color:var(--text)}#clips{overflow:auto;flex:1}button{cursor:pointer;border:1px solid var(--line);background:var(--panel);color:var(--text);padding:9px;border-radius:4px}#clips button{display:block;width:100%;text-align:left;margin-bottom:7px;overflow-wrap:anywhere;font-size:12px}#clips button.selected{border-color:var(--orange);background:#362b22}.legend{font-size:12px;line-height:1.8;color:var(--muted)}.green{color:#42ce83}.red{color:#ff665c}main{overflow:hidden;position:relative;background:#ede9df;min-width:0}iframe{border:0;width:calc(100% + 320px);height:100%;display:block}#loading{position:absolute;inset:0;display:grid;place-items:center;background:#ede9df;color:#333;pointer-events:none}footer{background:var(--panel);padding:12px 18px;border-top:1px solid var(--line);min-width:0}.transport{display:flex;gap:16px;align-items:center;margin-bottom:12px}#play{background:var(--orange);color:#211407;border:0;width:36px}#title{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px}#clock{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--muted)}#speed{background:var(--bg);color:var(--text);border:1px solid var(--line);padding:6px}.timeline-wrap{display:flex;align-items:center;gap:12px}#timeline{flex:1;min-width:20px;accent-color:var(--orange)}#frame{font-size:12px;color:var(--muted);min-width:70px;text-align:right}@media(max-width:650px){#app{grid-template-columns:150px 1fr}aside{padding:8px}.transport{gap:8px}#title{display:none}header{gap:10px;font-size:12px}#clock{font-size:11px}}
</style></head><body><div id="app"><header><b>ALPHAMOTION · VISOR</b><span id="status">连接中…</span></header><aside><b>动作片段</b><input id="search" type="search" placeholder="搜索片段"><div id="clips"></div><div class="legend"><span class="green">■ 绿色:贴地表面</span><br><span class="red">■ 红色:穿地表面</span><br>半透明区域;并非接触力。<br><span id="prediction"></span></div></aside><main><iframe src="/scene" title="现有 Viser 3D 播放器"></iframe><div id="loading">正在连接 3D 场景…</div></main><footer><div class="transport"><button id="play" aria-label="播放或暂停">▶</button><span id="clock">0.00 / 0.00 秒</span><span id="title"></span><select id="speed" aria-label="播放速度"><option value="0.25">0.25×</option><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option></select></div><div class="timeline-wrap"><input id="timeline" type="range" min="0" max="1" step="1" value="0" aria-label="拖动播放进度"><span id="frame"></span></div></footer></div>
<script>
const overlay=document.createElement('canvas');overlay.style.cssText='position:absolute;inset:0;pointer-events:none;width:100%;height:100%';document.querySelector('main').append(overlay);
const $=id=>document.getElementById(id);let clips=[],state={},dragging=false,pendingFrame=null,sendingFrame=false;
async function api(path,patch){let response=await fetch('/api/'+path,patch===undefined?{}:{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(patch)});if(!response.ok){let e=await response.json();throw Error(e.detail||response.status)}return response.json()}
function show(s){if(state.revision!==undefined&&s.revision<state.revision)return;state=s;$('timeline').max=s.frames-1;if(!dragging)$('timeline').value=s.frame;$('frame').textContent=(s.frame+1)+' / '+s.frames;$('clock').textContent=(s.frame/s.fps).toFixed(2)+' / '+(s.frames/s.fps).toFixed(2)+' 秒';$('title').textContent=s.label;$('play').textContent=s.playing?'❚❚':'▶';$('status').textContent=s.error|| (s.demo?'程序生成的显示测试;选择本地数据请下载运行':'已连接 · 原始 q / root 回放');$('prediction').textContent=s.demo?'':('预测足部标签:左 '+(s.predicted_contact[0]?'接触':'离地')+' · 右 '+(s.predicted_contact[1]?'接触':'离地'));document.querySelectorAll('#clips button').forEach(b=>b.classList.toggle('selected',Number(b.dataset.id)===s.clip))}
async function update(patch){try{show(await api('state',patch))}catch(e){$('status').textContent=e.message}}
function list(){let text=$('search').value.toLowerCase();$('clips').replaceChildren();for(let c of clips){if(!c.label.toLowerCase().includes(text))continue;let b=document.createElement('button');b.textContent=c.label;b.dataset.id=c.id;b.classList.toggle('selected',c.id===state.clip);b.onclick=()=>update({clip:c.id});$('clips').append(b)}}
$('search').oninput=list;$('play').onclick=()=>update({playing:!state.playing});$('speed').onchange=e=>update({speed:Number(e.target.value)});
async function seek(){if(sendingFrame)return;sendingFrame=true;while(pendingFrame!==null){let frame=pendingFrame;pendingFrame=null;await update({frame,playing:false})}sendingFrame=false}
$('timeline').addEventListener('pointerdown',()=>{dragging=true});$('timeline').oninput=e=>{pendingFrame=Number(e.target.value);seek()};window.addEventListener('pointerup',()=>{dragging=false});
async function init(){try{clips=(await api('clips')).clips;show(await api('state'));list();$('loading').style.display='none'}catch(e){$('loading').textContent=e.message}}
init();setInterval(async()=>{try{show(await api('state'))}catch(e){$('status').textContent='连接断开:'+e.message}},250);
let painting=false;setInterval(async()=>{if(painting)return;painting=true;try{let regions=(await api('overlay')).regions;overlay.width=overlay.clientWidth;overlay.height=overlay.clientHeight;let ctx=overlay.getContext('2d');ctx.globalAlpha=.45;for(let r of regions){ctx.beginPath();r.points.forEach(([x,y],i)=>{if(i)ctx.lineTo(x*overlay.width,y*overlay.height);else ctx.moveTo(x*overlay.width,y*overlay.height)});ctx.closePath();ctx.fillStyle='rgb('+r.color.join(',')+')';ctx.fill()}}catch(e){}finally{painting=false}},100);
</script></body></html>