copyrightReference movement HTML demo

Use this example to test reference movement tracking in a simple HTML page.

This demo shows how to:

  • select a reference movement

  • launch real-time or uploaded video tracking

  • pass reference=REFERENCE_UUID

  • receive reference_score and exercise_summary from PoseTracker

Before using it, replace:

  • REPLACE_WITH_YOUR_POSETRACKER_TOKEN

  • the reference UUIDs in CONFIG.references

<!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>
  • Replace REPLACE_WITH_YOUR_POSETRACKER_TOKEN with your API token.

  • Replace the demo reference UUIDs with your own references created in the dashboard.

Last updated