Metrics Engine
Files:
frontend/src/components/dashboard/useSprintMetrics.ts— React hookfrontend/src/components/dashboard/sprintMath.ts— pure math functions
Design
The metrics engine is split into two layers:
sprintMath.ts — pure TypeScript functions. No React, no DOM, no imports beyond TypeScript types. This layer can be imported into unit tests, scripts, or any other context without any framework setup.
useSprintMetrics — a React hook that calls into sprintMath.ts and wraps everything in a useMemo. It handles the React lifecycle (memoisation, dependency tracking) while delegating all actual computation to the pure layer.
This separation means the math is independently testable. See Testing.
sprintMath.ts
Exported functions
| Function | Signature | Purpose |
|---|---|---|
angleDeg | (a, b, c, ar, fw, fh) → number | Interior angle at vertex B |
segAngleDeg | (p1, p2, ar, fw, fh) → number | Signed angle from downward vertical |
segInclineDeg | (p1, p2, ar, fw, fh) → number | Unsigned inclination from horizontal |
smooth | (arr, w) → number[] | Box filter |
derivative | (arr, fps) → number[] | Central-difference derivative + smooth |
buildSeries | (raw, fps) → JointTimeSeries | Null-fill → smooth → differentiate |
See the Math Reference for the equations behind each function.
P2 type
type P2 = { x: number; y: number };Used throughout the math layer. All points are in inference-frame pixel coordinates before aspect-ratio correction is applied inside each function.
JointTimeSeries
interface JointTimeSeries {
frames: number[]; // identity: [0, 1, 2, ..., N-1]
angle: number[]; // degrees, smoothed
velocity: number[]; // deg/s
accel: number[]; // deg/s²
}useSprintMetrics Hook
Signature
function useSprintMetrics(
getKeypoints: (frame: number) => Keypoint[],
totalFrames: number,
fps: number,
calibration: CalibrationData | null,
frameWidth: number,
frameHeight: number,
flipH?: boolean,
): SprintMetrics | nullReturns null if:
totalFrames < 2orfps <= 0calibrationisnull(distances would be meaningless)
Computation sequence
Build point arrays — for each of the 17 relevant landmark indices,
col(idx)maps across all frames, applying thescore ≥ 0.35threshold and returningP2 | nullper frameCoM — midpoint of left and right hip arrays
Scale operations — if calibration + frame dimensions are available, creates a
ScaleOpsobject withh,hSigned, andxyconverters from pixels to metresGround contacts —
detectContactsis called separately for left and right foot, then merged and sorted by frameAngular helpers — three inner lambdas are defined over the closed-over
ar,fw,fh:jA(a, b, c)— callsangleDegper frame (joint angles)sA(from, to)— callssegAngleDegper frame (thigh)iA(from, to)— callssegInclineDegper frame (torso, shin)
buildSeriescalls — every joint and segment gets aJointTimeSeriesby callingbuildSerieson the per-frame angle arrayCoM series — horizontal position, speed, acceleration, and cumulative distance computed inline
detectContacts
export function detectContacts(
heelPts: (P2 | null)[],
toePts: (P2 | null)[],
fps: number,
foot: 'left' | 'right',
comPts: (P2 | null)[],
prev: GroundContactEvent[],
scaleOps: ScaleOps,
flipH?: boolean,
): GroundContactEvent[]The prev parameter is the already-detected contacts of the other foot. It is used to compute step length and step frequency, which are defined relative to the previous contact of either foot.
The flipH flag inverts the sign of the CoM distance for videos where the athlete runs right-to-left.
See Math Reference — Ground Contact Detection for the full algorithm.
Confidence Threshold
const SCORE_MIN = 0.35;Keypoints with score < SCORE_MIN are returned as null. This threshold was chosen empirically: lower values include noisy detections that corrupt angle calculations; higher values cause too many frames to be treated as missing data.