Inspiration

I run the 400 and the 800 at Skyline, in Sammamish. The split I get after a race is when a parent on the rail taps a screen. That tap is late. Sometimes it is early. It is never the body on the line. I wanted the time when I hit the mark. One phone on a tripod can already see one plane. FinishLynx trucks do not show up at a Tuesday dual. So I built a finish camera that sits on one mark and tells you which mark that is. The geometry of a 400m oval is the constraint, not a footnote. The 200 and the finish are opposite sides. One lens cannot see both. Split does not pretend it can.

What it does

Split is a finish-line camera for any runner. Live camera or an uploaded clip. You draw a yellow plane on the video. ARM takes a one-second empty-line baseline. After that, a body crossing the plane stamps the split. The clock is huge. ARM is one fat control. Manual MARK exists and stays small on purpose. Default stock preset: 800 at FINISH, empty goal. First crossing is the 400. Second is the finish. Type your own goal if you want even-split math. Anyone can add events, marks, split names, planned crossings, calibrate window, and debounce. Share the setup as a URL or as JSON. Times are $M{:}SS.xx$. There is no built-in coach. Connect AI is bring-your-own: a webhook that POSTs each crossing, a session JSON download, or an optional OpenAI-compatible endpoint. Keys stay in this browser.

How I built it

Vanilla JavaScript. No npm. No frameworks. Static files. Serve with python3 -m http.server or GitHub Pages. getUserMedia feeds the stage. A click places a vertical or horizontal plane. Each frame copies the video into an offscreen canvas, maps the on-screen line through object-fit: cover into video pixels, and reads 64 luma samples along that strip: $$ L = 0.299R + 0.587G + 0.114B $$ ARM calibrates an empty-line baseline $B$ for one second. Energy is mean absolute error against that profile: $$ E = \frac{1}{64}\sum_{i=1}^{64} \lvert L_i - B_i \rvert $$ A crossing fires when $E \ge \tau$ for 2 consecutive frames (default $\tau = 18$), then the line ignores new hits for $800\,\mathrm{ms}$. Quiet frames update the baseline with a slow EMA ($\alpha = 0.08$) so a cloud does not become an athlete. The plane flashes white when it stamps. If you type a goal $G$ with $N$ planned crossings and $k$ already down, $$ \mathrm{even} = \frac{G}{N},\qquad \mathrm{remaining} = G - t,\qquad \mathrm{next} = \frac{G - t}{N - k}. $$ Optional MediaPipe Pose (CDN) draws stick figures. A hip that crosses the plane can also stamp. If the CDN is blocked, luma still works.

Challenges I ran into

file:// blocks the camera. The page has to say so and print the server command. A full-height average washes out a small body, so detection is a 64-point profile, not one pixel. ARM has to calibrate before the gun. If calibrate eats the first second of the race, the first split is a lie. object-fit: cover means the visible line is not the video line. Mapping that wrong double-stamps or misses. Tick marks on the plane were the only way the line read as a mark instead of a UI decoration. Opposite sides of a 400m track cannot share one lens. I almost designed a two-mark story. Runners would have known it was fake.

Accomplishments that I'm proud of

The crossing is the product. The clock is the size of a meet board. The UI says which mark the camera is on. Stock events are editable. The demo works with a hand waved through the line, no meet, no account, no API key.

What I learned

A finish camera is a start watch plus a line. Honesty about what one phone can see matters more than a longer feature list. If you lie about seeing the 200 and the finish, the people who run those races will know.

What's next

A second phone on the other side, talking over a local link. Not in this submission.

Built With

Share this project:

Updates