A 2D interactive soccer drill builder. Place players, cones, goals and zones on a floodlit pitch, draw passes / runs / dribbles / shots as paths, play the drill back as a smooth animation, save drills locally, and export an animated GIF.
100% client-side — no backend, no account. Drills persist in IndexedDB.
- Next.js 15 (App Router, TypeScript strict) — the editor is client-only
- react-konva + konva — 2D canvas rendering
- Zustand + zundo — state with undo/redo (undo tracks the document only)
- Dexie — IndexedDB persistence (drills carry PNG thumbnails)
- gifenc in a Web Worker — GIF encoding off the main thread
npm install
npm run dev # http://localhost:3000
npm run build # production build
npm run typecheck # tsc --noEmit
npm run validate # headless engine invariants + a real GIF encode (no browser)Playback is a pure function of time. compileDrill() precomputes arc-length
parameterised Catmull-Rom splines and per-action timing; positionsAt(t) maps a
time to entity positions. Both the live rAF player and the GIF exporter call
positionsAt, so what you preview is exactly what you export.
- Logical pitch space is
1050 × 680; the stage scales to fit. - Steps run sequentially (0.25s gap); actions within a step run in parallel.
- Positions are written imperatively to Konva nodes inside one rAF loop — React re-renders only the scrubber. This is the smoothness requirement.
| Path | Responsibility |
|---|---|
lib/types.ts |
Data model (Entity, Action, Step, Drill) |
lib/geometry.ts |
Splines, arc-length sampling, easing |
lib/playback.ts |
compileDrill / positionsAt |
lib/notation.ts |
Coaching line styles (squiggle / zigzag / arrowheads) |
lib/store.ts |
Zustand + zundo editor store |
lib/db.ts |
Dexie persistence |
lib/templates.ts |
The four built-in drills |
components/ |
Konva stage, entities, timeline, GIF export |
scripts/validate.ts |
Headless engine regression guard |