# Reference movement HTML demo

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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://posetracker.gitbook.io/posetracker-api/use-posetracker-on-real-time-camera-webcam/integration-tutorials/reference-movement-html-demo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
