Reference movement HTML demo
Use this example to test reference movement tracking in a simple HTML page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PoseTracker Reference Movement Tracking β Demo </title>
<style>
* { box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
padding: 16px;
background: #1a1a1a;
color: #f8fafc;
margin: 0;
}
.intro {
background: #2a2a2a;
padding: 16px;
border-radius: 8px;
margin-bottom: 24px;
text-align: center;
}
/* Step container: only one step visible at a time. Step 3 uses full width. */
.step-container { max-width: 860px; margin: 0 auto; }
.step-container:has(#step3.active) { max-width: none; width: 100%; padding: 0 16px; }
.step-panel { display: none; }
.step-panel.active { display: block; }
.step-title {
font-size: 1.1rem;
color: #94a3b8;
margin-bottom: 16px;
font-weight: 600;
}
.step-panel h2 { margin: 0 0 20px 0; font-size: 1.35rem; }
/* Step 1: reference cards with looping video */
.ref-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.ref-card {
padding: 0;
border: 2px solid #444;
border-radius: 12px;
cursor: pointer;
background: #1a1a1a;
transition: border-color 0.2s, background 0.2s, transform 0.15s;
overflow: hidden;
}
.ref-card:hover { border-color: #666; background: #252525; transform: translateY(-2px); }
.ref-card.selected { border-color: #3b82f6; background: #1e3a5f; box-shadow: 0 0 0 1px #3b82f6; }
.ref-card .card-video-wrap {
position: relative;
width: 100%;
aspect-ratio: 16/9;
background: #000;
overflow: hidden;
}
.ref-card video {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.ref-card .card-title {
padding: 12px 14px;
font-weight: 600;
font-size: 1rem;
}
/* Step 2: chosen reference card */
.chosen-ref-card {
background: #2a2a2a;
border-radius: 12px;
padding: 14px;
margin-bottom: 24px;
border: 1px solid #374151;
justify-items: center;
}
.chosen-ref-card .chosen-ref-title {
font-size: 0.9rem;
color: #94a3b8;
margin-bottom: 8px;
}
.chosen-ref-card .chosen-ref-label {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 10px;
}
.chosen-ref-card .chosen-ref-video-wrap {
width: 100%;
max-width: 360px;
aspect-ratio: 16/9;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.chosen-ref-card .chosen-ref-video-wrap video {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
/* Step 2: mode cards */
.mode-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.mode-card {
padding: 24px;
border: 2px solid #3b82f6;
border-radius: 12px;
cursor: pointer;
background: #1e3a5f;
transition: border-color 0.2s, background 0.2s, transform 0.15s;
text-align: center;
}
.mode-card:hover { border-color: #2563eb; background: #2563eb; transform: translateY(-2px); }
.mode-card.selected { border-color: #3b82f6; background: #1e3a5f; box-shadow: 0 0 0 1px #3b82f6; }
.mode-card .mode-icon { font-size: 2.5rem; margin-bottom: 12px; }
.mode-card .mode-title { font-size: 1.1rem; font-weight: 600; margin-bottom: 6px; }
.mode-card .mode-desc { font-size: 0.9rem; color: #94a3b8; }
/* Navigation buttons */
.nav-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 28px;
gap: 12px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
border: 2px solid #3b82f6;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
background: #1e3a5f;
color: #fff;
}
.btn:hover { background: #2563eb; border-color: #2563eb; }
.btn-primary {
background: #1e3a5f;
color: #fff;
border: 2px solid #3b82f6;
}
.btn-primary:hover:not(:disabled) { background: #2563eb; border-color: #2563eb; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-secondary {
background: #1e3a5f;
color: #fff;
border: 2px solid #3b82f6;
}
.btn-secondary:hover { background: #2563eb; border-color: #2563eb; }
/* Step 3: demo layout β large: full width, video + iframe 50% each, row height 50vh max; small: stacked, 90% width each */
.demo-layout {
display: grid;
gap: 16px;
}
@media (min-width: 768px) {
.demo-layout {
grid-template-columns: 1fr 1fr;
grid-template-rows: minmax(0, 50vh) auto;
grid-template-areas: "ref-video iframe" "results results";
gap: 20px;
width: 100%;
}
.encart-ref { grid-area: ref-video; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.encart-iframe { grid-area: iframe; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.encart-results { grid-area: results; min-width: 0; }
.encart-ref .ref-video-container { flex: 1; min-height: 0; aspect-ratio: auto; max-height: 100%; }
.encart-ref .ref-video-container video { object-fit: contain; }
.encart-iframe iframe { flex: 1; min-height: 200px; max-height: 100%; }
}
@media (max-width: 767px) {
.demo-layout {
grid-template-columns: 1fr;
grid-template-areas: "ref-video" "iframe" "results";
grid-template-rows: auto auto auto;
}
.demo-layout .encart {
width: 90%;
max-width: 90%;
justify-self: center;
min-width: 0;
}
.encart-ref { grid-area: ref-video; }
.encart-iframe { grid-area: iframe; }
.encart-results { grid-area: results; }
}
.encart {
background: #2a2a2a;
border-radius: 10px;
padding: 14px;
min-height: 180px;
}
.encart-title {
font-size: 0.85rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.ref-video-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.ref-video-container video {
width: 100%;
height: 100%;
object-fit: contain;
}
.encart-iframe iframe {
width: 100%;
min-height: 320px;
border: 2px solid #3b82f6;
border-radius: 8px;
background: #000;
display: block;
}
@media (max-width: 767px) {
.encart-iframe iframe { min-height: 280px; }
}
.status-text { font-size: 0.9rem; color: #94a3b8; margin-bottom: 8px; }
.error-box {
background: #7f1d1d;
color: #fecaca;
padding: 10px;
border-radius: 6px;
font-size: 0.9rem;
margin-bottom: 8px;
}
.posture-hint { font-size: 0.85rem; color: #fbbf24; margin-bottom: 8px; }
.last-score { font-size: 1.5rem; font-weight: bold; color: #10b981; margin-bottom: 4px; }
.last-grade { font-size: 1.2rem; color: #94a3b8; margin-bottom: 8px; }
.last-sub-scores { font-size: 0.8rem; color: #94a3b8; margin-bottom: 12px; }
.history-list { max-height: 200px; overflow-y: auto; font-size: 0.85rem; }
.history-item {
padding: 6px 8px;
border-bottom: 1px solid #333;
display: flex;
justify-content: space-between;
gap: 8px;
}
.history-item .grade { font-weight: bold; }
.history-item .sub-scores { font-size: 0.75rem; color: #94a3b8; margin-top: 2px; }
.summary-box {
background: #1e3a5f;
padding: 10px;
border-radius: 6px;
font-size: 0.9rem;
margin-bottom: 8px;
}
.btn-new-session { margin-top: 10px; }
code { background: #1a1a1a; padding: 2px 6px; border-radius: 3px; color: #10b981; font-size: 0.9em; }
.demo-footer { margin-top: 16px; padding: 0 16px; text-align: center; font-size: 0.85rem; color: #94a3b8; }
.demo-footer:first-of-type { margin-top: 32px; }
.demo-footer code { background: #2a2a2a; padding: 2px 6px; border-radius: 4px; font-size: 0.8rem; }
.docs-link { color: #3b82f6; text-decoration: underline; font-size: 0.9rem; }
.docs-link:hover { color: #60a5fa; }
</style>
</head>
<body>
<div class="intro">
<h1>PoseTracker Reference Movement Tracking β Demo App</h1>
</div>
<div class="step-container">
<!-- Step 1: Select reference movement -->
<div class="step-panel active" id="step1">
<p class="step-title">Step 1 of 2</p>
<h2>Select reference movement</h2>
<div class="ref-cards" id="refCards"></div>
</div>
<!-- Step 2: Select capture mode -->
<div class="step-panel" id="step2">
<p class="step-title">Step 2 of 2</p>
<h2>How do you want to capture your movement?</h2>
<div class="chosen-ref-card" id="chosenRefCard">
<p class="chosen-ref-title">Reference selected</p>
<p class="chosen-ref-label" id="chosenRefLabel">β</p>
<div class="chosen-ref-video-wrap">
<video id="chosenRefVideo" muted loop playsinline autoplay preload="auto"></video>
</div>
</div>
<p class="status-text" style="margin-bottom: 16px;">Choose how to record your performance so it can be compared against the reference above.</p>
<div class="mode-cards">
<div class="mode-card" data-mode="realtime" id="modeCardRealtime">
<div class="mode-icon">π·</div>
<div class="mode-title">Real-time</div>
<div class="mode-desc">Use your device camera for live comparison.</div>
</div>
<div class="mode-card" data-mode="upload" id="modeCardUpload">
<div class="mode-icon">π</div>
<div class="mode-title">Upload from gallery</div>
<div class="mode-desc">Pick a video from your device to analyze.</div>
</div>
</div>
<div class="nav-row">
<button type="button" class="btn btn-secondary" id="backToStep1">Back</button>
</div>
</div>
<!-- Step 3: Demo view (ref | results | iframe) -->
<div class="step-panel" id="step3">
<p class="status-text" id="statusText">Loadingβ¦</p>
<div class="demo-layout">
<div class="encart encart-ref">
<div class="encart-title">Ref movement</div>
<div class="ref-video-container">
<video id="refVideo" muted loop playsinline preload="metadata"></video>
</div>
</div>
<div class="encart encart-results">
<div class="encart-title">Result / data PoseTracker</div>
<div id="errorBox" class="error-box" style="display: none;"></div>
<div id="postureHint" class="posture-hint" style="display: none;"></div>
<div class="last-score" id="lastScore">β</div>
<div class="last-grade" id="lastGrade">β</div>
<div class="last-sub-scores" id="lastSubScores"></div>
<div id="summaryBox" class="summary-box" style="display: none;"></div>
<div class="history-list" id="historyList"></div>
<button type="button" class="btn btn-secondary btn-new-session" id="newSessionBtn">Clear results</button>
</div>
<div class="encart encart-iframe">
<div class="encart-title">Iframe PoseTracker</div>
<iframe id="trackingFrame" src="about:blank" allow="camera *;" title="PoseTracker"></iframe>
</div>
</div>
<div class="nav-row" style="margin-top: 20px;">
<button type="button" class="btn btn-secondary" id="backToStep2">Back</button>
<span></span>
</div>
</div>
</div>
<p class="demo-footer">
<a href="#" id="docsLink" class="docs-link">Find the codebase demo source code and documentation for reference movement processing here.</a>
</p>
<script>
const CONFIG = {
baseUrl: window.location.origin,
token: REPLACE_WITH_YOUR_POSETRACKER_TOKEN,
references: [
{ uuid: 'eccd157f-fbd0-42fc-a6ae-fd1ab62cbf70', videoUrl: 'https://posetracker.s3.eu-west-3.amazonaws.com/Taekwondo_kick_skeleton_6b66fd74dc.webm', label: 'Roundhouse Taekwondo Kick' },
{ uuid: '6341bc97-8996-4865-88e4-28c4dc5cd646', videoUrl: 'https://posetracker.s3.eu-west-3.amazonaws.com/basket_cross_skeleton_48a25de218.webm', label: 'Basket - Cross' },
{ uuid: '2c60cd0d-426e-4713-bb57-8a0f0a5656ef', videoUrl: 'https://posetracker.s3.eu-west-3.amazonaws.com/foot_passement_skeleton_0efd280769.webm', label: 'Foot - leg pass' }
]
};
let currentStep = 1;
let selectedRef = null;
let mode = 'realtime';
let repHistory = [];
let exerciseSummary = null;
function showStep(step) {
currentStep = step;
document.querySelectorAll('.step-panel').forEach((el) => el.classList.remove('active'));
const panel = document.getElementById('step' + step);
if (panel) panel.classList.add('active');
}
function buildIframeUrl(widthPx, heightPx) {
if (!selectedRef) return null;
const base = CONFIG.baseUrl;
const token = encodeURIComponent(CONFIG.token);
const ref = encodeURIComponent(selectedRef.uuid);
const width = widthPx != null && widthPx > 0 ? Number(widthPx) : null;
const height = heightPx != null && heightPx > 0 ? Number(heightPx) : null;
const sizeParams = (width != null && height != null) ? `&width=${encodeURIComponent(width)}&height=${encodeURIComponent(height)}` : '';
if (mode === 'realtime') {
return `${base}/pose_tracker/tracking?token=${token}&reference=${ref}&skeleton=true${sizeParams}`;
}
return `${base}/pose_tracker/upload_tracking?token=${token}&reference=${ref}&skeleton=true&source=video${sizeParams}`;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function renderRefCards() {
const container = document.getElementById('refCards');
container.innerHTML = '';
CONFIG.references.forEach((ref) => {
const card = document.createElement('div');
card.className = 'ref-card' + (selectedRef && selectedRef.uuid === ref.uuid ? ' selected' : '');
const videoHtml = ref.videoUrl
? '<video src="' + escapeHtml(ref.videoUrl) + '" muted loop playsinline autoplay preload="auto"></video>'
: '<div style="aspect-ratio:16/9;background:#111;display:flex;align-items:center;justify-content:center;color:#666;">No video</div>';
card.innerHTML = '<div class="card-video-wrap">' + videoHtml + '</div><div class="card-title">' + escapeHtml(ref.label) + '</div>';
card.onclick = () => {
selectedRef = ref;
renderRefCards();
goToStep2();
};
container.appendChild(card);
var v = card.querySelector('video');
if (v) {
v.controls = false;
v.play().catch(function() {});
}
});
}
function goToStep2() {
if (!selectedRef) return;
document.getElementById('chosenRefLabel').textContent = selectedRef.label;
var chosenVideo = document.getElementById('chosenRefVideo');
chosenVideo.src = selectedRef.videoUrl || '';
chosenVideo.controls = false;
chosenVideo.load();
chosenVideo.play().catch(function() {});
initModeCards();
showStep(2);
}
function initModeCards() {
document.querySelectorAll('.mode-card').forEach((el) => {
el.classList.toggle('selected', el.getAttribute('data-mode') === mode);
el.onclick = () => {
mode = el.getAttribute('data-mode');
document.querySelectorAll('.mode-card').forEach((c) => c.classList.toggle('selected', c.getAttribute('data-mode') === mode));
showStep(3);
startDemo();
};
});
}
function startDemo() {
const refVideo = document.getElementById('refVideo');
refVideo.src = selectedRef.videoUrl || '';
refVideo.loop = true;
refVideo.muted = true;
refVideo.playsInline = true;
refVideo.load();
refVideo.play().catch(() => {});
document.getElementById('statusText').textContent = mode === 'realtime' ? 'Demo loaded. Allow camera if prompted.' : 'Demo loaded. Upload a video in the iframe.';
document.getElementById('errorBox').style.display = 'none';
document.getElementById('errorBox').textContent = '';
const frameEl = document.getElementById('trackingFrame');
if (!frameEl) return;
function setIframeSrcWithSize() {
var w = frameEl.offsetWidth || frameEl.clientWidth || 0;
var h = frameEl.offsetHeight || frameEl.clientHeight || 0;
if (w <= 0 || h <= 0) {
w = 640;
h = 480;
}
const url = buildIframeUrl(w, h);
if (url) frameEl.src = url;
}
requestAnimationFrame(setIframeSrcWithSize);
}
function formatScorePct(score) {
return score != null && typeof score === 'number' && !Number.isNaN(score)
? Math.round(score * 100) + '%' : 'β';
}
function formatSubScoresLine(r) {
const p = formatScorePct(r.poseScore);
const t = formatScorePct(r.timingScore);
const m = formatScorePct(r.movementScore);
return 'Pose ' + p + ' Β· Timing ' + t + ' Β· Movement ' + m;
}
function renderResults() {
const lastScoreEl = document.getElementById('lastScore');
const lastGradeEl = document.getElementById('lastGrade');
const lastSubScoresEl = document.getElementById('lastSubScores');
const historyEl = document.getElementById('historyList');
if (repHistory.length === 0) {
lastScoreEl.textContent = 'β';
lastGradeEl.textContent = 'β';
if (lastSubScoresEl) lastSubScoresEl.textContent = '';
} else {
const last = repHistory[repHistory.length - 1];
const pct = last.overallScore != null ? Math.round(last.overallScore * 100) : 'β';
lastScoreEl.textContent = 'Score: ' + pct + '%';
lastGradeEl.textContent = 'Grade: ' + (last.grade || 'β');
if (lastSubScoresEl) lastSubScoresEl.textContent = formatSubScoresLine(last);
}
historyEl.innerHTML = repHistory.map((r, i) => {
const subLine = (r.poseScore != null || r.timingScore != null || r.movementScore != null)
? '<div class="sub-scores">' + formatSubScoresLine(r) + '</div>' : '';
return '<div class="history-item">' +
'<span>Rep ' + (i + 1) + '</span>' +
'<span>' + (r.overallScore != null ? formatScorePct(r.overallScore) : 'β') + '</span>' +
'<span class="grade">' + (r.grade || 'β') + '</span>' +
subLine + '</div>';
}).join('');
const summaryBox = document.getElementById('summaryBox');
if (exerciseSummary) {
summaryBox.style.display = 'block';
summaryBox.innerHTML = 'Total reps: ' + (exerciseSummary.counter ?? 0) +
(exerciseSummary.avg_rep_score != null ? ' Β· Avg score: ' + Math.round(exerciseSummary.avg_rep_score * 100) + '%' : '') +
(exerciseSummary.avg_similarity != null ? ' Β· Avg similarity: ' + Math.round(exerciseSummary.avg_similarity * 100) + '%' : '');
} else {
summaryBox.style.display = 'none';
}
}
window.addEventListener('message', function (event) {
try {
const allowedOrigin = new URL(CONFIG.baseUrl).origin;
if (event.origin !== allowedOrigin && event.origin !== window.location.origin) return;
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (!data || !data.type) return;
const statusEl = document.getElementById('statusText');
const errorBox = document.getElementById('errorBox');
const postureHint = document.getElementById('postureHint');
if (statusEl && data.type === 'initialization' && data.message) statusEl.textContent = data.message;
if (errorBox && data.type === 'error') {
errorBox.style.display = 'block';
errorBox.textContent = data.message || data.error || 'Error';
}
if (postureHint && data.type === 'posture') {
if (data.ready === false && data.message) {
postureHint.style.display = 'block';
postureHint.textContent = data.message + (data.direction ? ' (' + data.direction + ')' : '');
} else {
postureHint.style.display = 'none';
}
}
if (data.type === 'counter' && data.reference_score) {
repHistory.push({
repIndex: data.current_count,
...data.reference_score,
timestamp: Date.now()
});
renderResults();
}
if (data.type === 'exercise_summary' && data.reference_scores) {
exerciseSummary = {
counter: data.counter,
avg_similarity: data.avg_similarity,
avg_rep_score: data.avg_rep_score,
total_frames_compared: data.total_frames_compared
};
renderResults();
}
} catch (e) {}
});
function newSession() {
repHistory = [];
exerciseSummary = null;
renderResults();
const errorBox = document.getElementById('errorBox');
const postureHint = document.getElementById('postureHint');
if (errorBox) { errorBox.style.display = 'none'; errorBox.textContent = ''; }
if (postureHint) postureHint.style.display = 'none';
}
document.getElementById('backToStep1').onclick = () => showStep(1);
document.getElementById('backToStep2').onclick = () => showStep(2);
document.getElementById('newSessionBtn').onclick = newSession;
renderRefCards();
window.CONFIG = CONFIG;
window.reloadDemo = startDemo;
window.newSession = newSession;
</script>
</body>
</html>
Last updated