/* Page logic for the pick-and-place sample viewers: camera grid, synchronised playback with * autoplay + loop, the state readout, the joint-effort strip and the charts. * The instruction header, step timeline and current-step panel come from steps-ui.js, which * the older sample pages use as well. All page data lives in the JSON blocks of the page. */ (function () { const J = id => { const e = document.getElementById(id); return e ? JSON.parse(e.textContent) : null; }; const S = J('series'), STEPS = J('steps') || [], CAMS = J('cams') || [], FILES = J('files') || [], TASK = J('task') || {}; const has = k => S[k] !== undefined; const DUR = S.dur != null ? S.dur : (S.t ? S.t[S.n - 1] : (S.n - 1) / S.fps); const END_A = S.side === 'L' ? S.end_l : S.end_r; // active arm, derived const cmdDelta = i => { // |command - state|, derived let m = 0; for (const c of S.joint_idx) m = Math.max(m, Math.abs(S.act_joints[i][c] - S.joints[i][c])); return m; }; const frameAt = t => Math.max(0, Math.min(S.n - 1, Math.round(t * S.fps))); const SIDE = S.side, SIDEL = SIDE === 'L' ? 'left' : 'right'; if (window.self !== window.top) { // embedded in the gallery shell const b = document.getElementById('backlink'); if (b) b.style.display = 'none'; } // ---- file list const fm = document.getElementById('fileManifest'); FILES.forEach(f => { const d = document.createElement('span'); d.className = 'file-chip'; d.innerHTML = f.name + '' + f.size + ''; fm.appendChild(d); }); // ---- cameras const camgrid = document.getElementById('camgrid'), videos = []; CAMS.forEach(c => { const d = document.createElement('div'); d.className = 'cam'; d.innerHTML = `
${c.label}
` + ``; camgrid.appendChild(d); videos.push(d.querySelector('video')); }); const master = videos[Math.max(0, CAMS.findIndex(c => c.key === 'head_color'))]; // ---- playback: autoplay once every tile can render, then loop const playBtn = document.getElementById('play'), seek = document.getElementById('seek'), tlabel = document.getElementById('tlabel'); let playing = false; const seekTo = t => videos.forEach(v => { try { v.currentTime = t; } catch (e) {} }); function setPlaying(p) { playing = p; playBtn.textContent = p ? '❚❚' : '▶︎'; if (p) { videos.forEach(v => v.play().catch(() => {})); requestAnimationFrame(loop); } else videos.forEach(v => v.pause()); } playBtn.onclick = () => setPlaying(!playing); let started = false, iv = setInterval(tryStart, 400); function tryStart() { if (started || playing || !videos.length || !videos.every(v => v.readyState >= 2)) return; started = true; clearInterval(iv); videos.forEach(v => v.muted = true); setPlaying(true); } videos.forEach(v => { v.addEventListener('canplay', tryStart); v.addEventListener('loadeddata', tryStart); }); window.addEventListener('load', tryStart); setTimeout(() => clearInterval(iv), 90000); setInterval(() => { if (playing && master.currentTime >= DUR - 0.08) { seekTo(0); videos.forEach(v => v.play().catch(() => {})); } }, 200); seek.oninput = () => { const t = seek.value / 1000 * DUR; seekTo(t); update(); }; function resync() { const t = master.currentTime; videos.forEach(v => { if (v !== master && Math.abs(v.currentTime - t) > 0.2) { try { v.currentTime = t; } catch (e) {} } }); } master.addEventListener('seeked', resync); setInterval(() => { if (!master.paused) resync(); }, 1000); function loop() { update(); if (playing) requestAnimationFrame(loop); } // ---- shared step UI: instruction header, timeline, current-step panel, step list const list = document.getElementById('stepList'); const items = STEPS.map(() => { const d = document.createElement('div'); d.className = 'step-item'; list.appendChild(d); return d; }); const steps = StepsUI.mount({ series: S, steps: STEPS, task: TASK, colorBy: 'token', listItems: items, seek: t => { seekTo(t); update(); }, hosts: { instr: document.getElementById('suInstr'), timeline: document.getElementById('suTl'), panel: document.getElementById('suPanel'), }, }); // ---- chart labels that depend on which arm moved document.getElementById('lblJoints').textContent = SIDEL + '-arm joint positions (rad) · 7 DoF (j1 shoulder → j7 wrist)' + (has('act_joints') ? ' · solid = state, dashed = teleop command' : ''); document.getElementById('lblEnd').innerHTML = SIDEL + '-arm end-effector position (m) — x ' + 'y z'; if (has('act_grip_l')) document.getElementById('lblGrip').innerHTML += ' · dashed = command'; // ---- joint-effort strip function heat(v) { v = Math.max(0, Math.min(1, v)); const s = [[30, 40, 120], [40, 140, 200], [60, 200, 120], [230, 210, 60], [220, 60, 50]]; const x = v * (s.length - 1), i = Math.floor(x), f = x - i, a = s[i], b = s[Math.min(i + 1, s.length - 1)]; return `rgb(${Math.round(a[0] + (b[0] - a[0]) * f)},${Math.round(a[1] + (b[1] - a[1]) * f)},` + `${Math.round(a[2] + (b[2] - a[2]) * f)})`; } const cEff = document.getElementById('cEff'); if (has('eff')) { document.getElementById('effInfo').textContent = '(7 joints/arm · both arms shown · peak ' + S.eff_max + ' Nm)'; document.getElementById('effPeak').textContent = S.eff_max; } function drawEffort(i) { if (!has('eff')) return; const ctx = cEff.getContext('2d'), dpr = devicePixelRatio || 1, w = cEff.clientWidth, h = cEff.clientHeight || 46; cEff.width = w * dpr; cEff.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); const vals = S.eff[i] || [], K = vals.length, gap = 18, cw = (w - gap - 4) / K; for (let k = 0; k < K; k++) { const x = 2 + k * cw + (k >= 7 ? gap : 0), v = Math.abs(vals[k]) / Math.max(1e-6, S.eff_max); ctx.fillStyle = v > 0.02 ? heat(v) : '#1b1f2a'; ctx.fillRect(x, 4, cw - 1.5, h - 16); ctx.strokeStyle = '#0b0d12'; ctx.lineWidth = 1; ctx.strokeRect(x, 4, cw - 1.5, h - 16); } ctx.font = '10px ui-monospace'; ctx.fillStyle = SIDE === 'L' ? '#41c76b' : '#6b7686'; ctx.fillText('ARM L', 2, h - 3); ctx.fillStyle = SIDE === 'R' ? '#e0616f' : '#6b7686'; ctx.fillText('ARM R', 2 + 7 * cw + gap, h - 3); } // ---- charts const fmt = v => (Math.abs(v) >= 100 ? v.toFixed(0) : Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2)); function mkDraw(canvas, series, cols, yMin, yMax, opt) { opt = opt || {}; const fill = opt.fill, dash = opt.dash || []; let off = null, ow = 0, oh = 0; // the curves never change: draw them once function paint(w, h, dpr) { off = document.createElement('canvas'); off.width = w * dpr; off.height = h * dpr; const ctx = off.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); STEPS.forEach((st, i) => { ctx.fillStyle = steps.colorOf(st, i) + '18'; ctx.fillRect(st.start_s / DUR * w, 0, (st.end_s - st.start_s) / DUR * w, h); }); ctx.strokeStyle = '#232733'; ctx.lineWidth = 1; for (let g = 0; g <= 4; g++) { const y = (h - 14) * g / 4 + 7; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } const X = k => k / (S.n - 1) * w, Y = v => (h - 14) - ((v - yMin) / Math.max(1e-9, yMax - yMin)) * (h - 14) + 7; series.forEach((c, ci) => { ctx.strokeStyle = cols[ci % cols.length]; ctx.lineWidth = dash[ci] ? 1 : 1.4; ctx.setLineDash(dash[ci] ? [3, 3] : []); ctx.beginPath(); c.forEach((v, k) => { const x = X(k), y = Y(v); k ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }); ctx.stroke(); ctx.setLineDash([]); if (fill) { ctx.lineTo(X(c.length - 1), Y(yMin)); ctx.lineTo(X(0), Y(yMin)); ctx.closePath(); ctx.fillStyle = cols[ci % cols.length] + '33'; ctx.fill(); } }); ctx.font = '9px ui-monospace'; ctx.fillStyle = '#6b7686'; ctx.textAlign = 'right'; ctx.fillText(fmt(yMax), w - 3, 10); ctx.fillText(fmt(yMin), w - 3, h - 3); } return function (t) { const dpr = devicePixelRatio || 1; const w = canvas.clientWidth, h = canvas.clientHeight || 90; if (!off || ow !== w || oh !== h) { paint(w, h, dpr); ow = w; oh = h; } canvas.width = w * dpr; canvas.height = h * dpr; const ctx = canvas.getContext('2d'); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.drawImage(off, 0, 0); // cached curves ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const px = (t / DUR) * w; // only the cursor is drawn per frame ctx.strokeStyle = '#ffffffaa'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, h); ctx.stroke(); }; } const rng = ss => { let a = Infinity, b = -Infinity; ss.forEach(c => c.forEach(v => { a = Math.min(a, v); b = Math.max(b, v); })); return [a, b]; }; const draws = []; const jcols = ['#e0616f', '#e0955f', '#e0d15f', '#7fe05f', '#5fe0c0', '#5f9fe0', '#b46fe0']; let jser = S.joint_idx.map(c => S.joints.map(r => r[c])), jdash = jser.map(() => false); if (has('act_joints')) S.joint_idx.forEach(c => { jser.push(S.act_joints.map(r => r[c])); jdash.push(true); }); draws.push(mkDraw(document.getElementById('cJoints'), jser, jcols, ...rng(jser), { dash: jdash })); if (has('end_l')) { const er = [0, 1, 2].map(c => END_A.map(r => r[c])); draws.push(mkDraw(document.getElementById('cEnd'), er, ['#e0616f', '#41c76b', '#4f8ff7'], ...rng(er))); } if (has('grip_l')) { let gs = [S.grip_l, S.grip_r], gd = [false, false]; if (has('act_grip_l')) { gs.push(S.act_grip_l, S.act_grip_r); gd.push(true, true); } draws.push(mkDraw(document.getElementById('cGrip'), gs, ['#41c76b', '#e0616f'], 0, 1, { dash: gd })); } if (has('fmag_l')) { const [, fx] = rng([S.fmag_l, S.fmag_r]); draws.push(mkDraw(document.getElementById('cForce'), [S.fmag_l, S.fmag_r], ['#41c76b', '#e0616f'], 0, fx || 1)); } if (has('src')) draws.push(mkDraw(document.getElementById('cSrc'), [S.src], ['#e0a53e'], 0, 1.05, { fill: true })); ['cForce', 'cJoints', 'cEnd', 'cGrip', 'cSrc'].forEach(id => { const c = document.getElementById(id); if (!c) return; c.addEventListener('click', e => { const r = c.getBoundingClientRect(); seekTo((e.clientX - r.left) / r.width * DUR); update(); }); }); // ---- per-frame readout function update() { const t = master.currentTime, i = frameAt(t); const si = steps.update(t), st = STEPS[si]; const set = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; }; set('roTime', t.toFixed(2) + 's'); set('roFrame', i + ' / ' + (S.n - 1)); set('roStep', st.id + '. ' + st.token); set('roGrip', has('grip_l') ? S.grip_l[i].toFixed(2) + ' / ' + S.grip_r[i].toFixed(2) : '–'); set('roEndL', has('end_l') ? S.end_l[i].map(x => x.toFixed(3)).join(' / ') : '–'); set('roEndR', has('end_l') ? S.end_r[i].map(x => x.toFixed(3)).join(' / ') : '–'); set('roF', has('fmag_l') ? S.fmag_l[i].toFixed(1) + ' / ' + S.fmag_r[i].toFixed(1) : '–'); set('roSrc', has('src') ? (S.src[i] === 1 ? 'OPERATOR' : 'idle / other') : '–'); set('roSide', SIDEL + ' arm'); set('roCmd', has('act_joints') ? cmdDelta(i).toFixed(3) : '–'); if (has('eff')) { const e = S.eff[i]; let bk = 0; for (let k = 0; k < e.length; k++) if (Math.abs(e[k]) > Math.abs(e[bk])) bk = k; set('roTau', S.joint_names[bk] + ' ' + e[bk].toFixed(1) + ' Nm'); } set('roProg', Math.round((t - st.start_s) / Math.max(1e-6, st.end_s - st.start_s) * 100) + '% of step ' + st.id); seek.value = Math.round(t / DUR * 1000); tlabel.textContent = t.toFixed(2) + ' / ' + DUR.toFixed(2) + 's'; drawEffort(i); draws.forEach(d => d(t)); } window.addEventListener('resize', update); master.addEventListener('loadedmetadata', update); master.addEventListener('seeked', update); window.addEventListener('load', update); update(); })();