TrainersEye Gold
Clinical Biomechanical Analysis
Ready
Shoulder
Waist
Knee
Centre
Camera Off
Tap Start below to activate MediaPipe Pose V2, enable the centre-grid framing guides, and begin biomechanical capture.
MediaPipe Pose V2
Zhang Calibration
CFI · MMSx-SCAN
Clinical Export
FPS --
Proc --ms
Quality Idle
Calib Pending
Position Guide
1
Stand 2.0–2.5 m from the camera.
2
Align your body centre to the vertical dotted line.
3
Match shoulder / waist / knee to the 3 horizontal markers.
4
Ensure even lighting — no strong backlight.
Front View — Centred
Align body to the centre vertical line and 3 horizontal markers. Tap the shutter when framed.
Tap to capture
CFI Score
--
Composite Flaw Index
Movement Grade
--
Clinical classification
Injury Risk
--
Predictive (BPIT™)
L/R Symmetry
--
Bilateral index
Live Traces
CFI Timeline
--
Left Knee (°)
--
Symmetry Index (%)
--
Captured Views
Front View
Front · Centred
Side View
Side · Centred
TrainersEye · Clinical Report
!
Initialising
Loading clinical engine…
`; }, /* ─── SVG plot builder (single series) ─── */ svgPlot(data, opts){ if (!data || !data.length) return ''; const W = 720, H = (opts.tall ? 130 : 100); const PAD_L = 34, PAD_R = 10, PAD_T = 10, PAD_B = 20; const pw = W - PAD_L - PAD_R; const ph = H - PAD_T - PAD_B; const valid = data.filter(v => typeof v === 'number' && isFinite(v)); if (!valid.length) return ''; let minY = opts.minY !== undefined ? opts.minY : Math.min(...valid); let maxY = opts.maxY !== undefined ? opts.maxY : Math.max(...valid); if (opts.centreLine) { const m = Math.max(Math.abs(minY), Math.abs(maxY)); minY = -m; maxY = m; } const range = (maxY - minY) || 1; const step = pw / (data.length - 1 || 1); const pts = data.map((v,i)=>{ const x = PAD_L + i * step; const y = PAD_T + ph - ((v - minY)/range) * ph; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); // Area (fill) const areaPts = `${PAD_L},${PAD_T+ph} ${pts} ${PAD_L+pw},${PAD_T+ph}`; // Gridlines const gridLines = opts.gridLines || []; let gridSVG = ''; gridLines.forEach(g=>{ if (g < minY || g > maxY) return; const y = PAD_T + ph - ((g - minY)/range) * ph; gridSVG += ``; gridSVG += `${g}`; }); // Reference line if (opts.refLine !== undefined) { const y = PAD_T + ph - ((opts.refLine - minY)/range) * ph; gridSVG += ``; gridSVG += `ref ${opts.refLine}`; } // Centre axis if (opts.centreLine) { const y = PAD_T + ph/2; gridSVG += ``; } // Y-axis min/max labels gridSVG += `${maxY.toFixed(0)}`; gridSVG += `${minY.toFixed(0)}`; // Event markers let eventSVG = ''; if (opts.eventMarkers) { opts.eventMarkers.forEach(fn=>{ if (fn >= data.length) return; const x = PAD_L + fn * step; eventSVG += ``; eventSVG += ``; }); } // Frame count axis const frameLbl = `${data.length} frames`; const fill = opts.fill || 'rgba(0,0,0,0.08)'; const colour = opts.colour || '#0f2341'; return ` ${gridSVG} ${eventSVG} ${frameLbl} `; }, /* ─── multi-series overlay plot ─── */ svgPlotMulti(series, opts){ if (!series || !series.length || !series[0].data.length) return ''; const W = 720, H = 100; const PAD_L = 34, PAD_R = 10, PAD_T = 10, PAD_B = 20; const pw = W - PAD_L - PAD_R; const ph = H - PAD_T - PAD_B; // Global y range const all = series.flatMap(s => s.data).filter(v => typeof v === 'number' && isFinite(v)); let minY = opts.minY !== undefined ? opts.minY : Math.min(...all); let maxY = opts.maxY !== undefined ? opts.maxY : Math.max(...all); const range = (maxY - minY) || 1; const len = series[0].data.length; const step = pw / (len - 1 || 1); const plots = series.map(s=>{ const pts = s.data.map((v,i)=>{ const x = PAD_L + i*step; const y = PAD_T + ph - ((v - minY)/range)*ph; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); return ``; }).join(''); return ` ${maxY.toFixed(0)} ${minY.toFixed(0)} ${plots} `; }, sleep(ms){ return new Promise(r => setTimeout(r, ms)); } }; document.addEventListener('DOMContentLoaded', ()=> TE.init());