This is a visual race replay system with JavaScript frontend and Python data generation scripts.
./
βββ js/ # Frontend JavaScript (ES6 modules)
β βββ app.js # Main application logic
β βββ commentary-engine.js # Audio commentary orchestration
β βββ data.js # Race data and constants
β βββ utils.js # Track geometry, timing utilities
βββ data/ # Race and commentary data files
βββ generate_*.py # Python scripts for audio/data generation
βββ index.html # Main HTML entry point
- Open
index.htmlin a browser to test the frontend - Use a local server:
python -m http.server 8000then visithttp://localhost:8000
- Run any script directly:
python generate_commentary.py - Python linting:
python -m py_compile <file>to check syntax
- No build step required (vanilla ES6 modules)
- Use browser console for debugging
- No formal test framework exists
- Manual testing: Open browser console, test functions directly
- Example:
import { getDistanceAtTime, getTrackCoordinates } from './js/utils.js'
- Use ES6 modules with explicit imports/exports
- Use
constby default,letwhen mutation needed - Arrow functions for callbacks, function declarations for methods
- Template literals for string interpolation
- Object destructuring for imports:
import { foo, bar } from './module'
- snake_case for functions and variables
- UPPER_SNAKE_CASE for constants
- Double quotes for strings
- Files:
snake_case.py,kebab-case.js - Functions:
camelCase(JS),snake_case(Python) - Constants:
UPPER_SNAKE_CASE - Classes:
PascalCase
- Use JSDoc comments for complex functions
- Example:
/** * @param {number[]} splits - Array of split times in seconds * @param {number} currentTime - Current race time * @returns {number} Distance in meters */
- Use try/catch for async operations
- Log errors to console with context
- Handle audio playback failures gracefully (see
app.js:136-139)
- 2-space indentation
- Trailing commas in arrays/objects
- One import per line
- Group imports: external, then internal
js/data.js: Runner data, audio clips, checkpoint definitionsjs/commentary-engine.js: Event timing logic (the core algorithm)js/utils.js: Track geometry, time/distance calculationsdata/race_data.json: Runner splits and metadata
- Add runner to
data/race_data.json - Add runner checkpoints to
js/data.js(RUNNER_CHECKPOINTS_TEMPLATE) - Regenerate audio commentary if needed
- Add clip to AUDIO_CLIPS array in
js/data.js - Add checkpoint trigger in appropriate runner's checkpoint list or GLOBAL_EVENTS
Track coordinates are in js/utils.js (getTrackCoordinates function). The track is a 200m banked indoor oval.
- Race data (splits, runner info) is loaded from
data/race_data.json - Commentary timing is defined in
js/data.js(RUNNER_CHECKPOINTS_TEMPLATE, GLOBAL_EVENTS_TEMPLATE) - Audio clips are stored in
commentary_audio/or similar directories - The CommentaryEngine (
commentary-engine.js) evaluates triggers each frame - Audio is synced to runner positions using distance interpolation
- 200m banked indoor oval track
- 4 laps = 800m race distance
- Coordinate system: pixels calculated from meters using
PIXELS_PER_METERconstant - Track rendering uses HTML/CSS positioning (not Canvas)
The core algorithm in commentary-engine.js:32-97 works as follows:
- Each animation frame, check if audio is currently playing
- Collect "due" global events (time-based or distance-based triggers)
- Collect "due" runner checkpoint events (distance thresholds)
- Sort all due events by dueTime, pick the earliest
- Mark event as played and return audio clip info
- Use a single state object at module level or in a class
- Avoid global variables; encapsulate in modules
- Example from
app.js:11-19:const state = { raceTime: 0, speed: 1, isRunning: false, // ... };
- Cache DOM element references at init time
- Use dataset for storing runner-specific data
- Update styles directly; avoid re-rendering entire sections
- Preload all audio elements at startup
- Handle play() failures gracefully (user interaction required)
- Reset currentTime to 0 before replaying
- Only one audio plays at a time - check
isAudioPlayingflag
- Use requestAnimationFrame for smooth 60fps updates
- Calculate delta time between frames for consistent speed
- Stop animation when all runners finish (distance >= 800m)
- Each generate_*.py script is independent
- Use argparse for command-line arguments if needed
- Output files to appropriate directories (data/, commentary_audio/, etc.)
- JSON for data interchange (read/write race data)
- Use json module for parsing and serialization
- Validate data structure before processing
- Scripts may generate audio using external APIs
- Always create manifest.json for audio directories
- Include timing metadata in manifests
- Open browser console, verify no errors on load
- Test play/pause/resume functionality
- Verify audio syncs with runner positions
- Test reset returns to initial state
- Check all runners finish correctly
- Most browsers block autoplay; requires user interaction first
- Check audio file paths exist and are correct
- Check splits array format: [0, t200, t400, t600, t800]
- Verify requestAnimationFrame is being called
- Verify checkpoint distances match actual runner positions
- Ensure isAudioPlaying prevents overlapping clips