An animated route editor for maps and images. Drop in a background, click to place waypoints, tweak styles and timing, then export as MP4, WebM, or a self-contained HTML file.
- Load a background image (drag-and-drop, upload, or pick a built-in example).
- Click the canvas to add major waypoints (full features) or Cmd/Ctrl+Click for minor waypoints (path shaping only).
- A Catmull-Rom spline connects them into a smooth animated path.
- Configure per-waypoint: marker style, colour, beacon effect, text label, wait time, segment speed, camera zoom, and area highlight.
- Configure globally: path visibility mode, waypoint visibility mode, background reveal mode (spotlight, angle-of-view), tint, trail, and graphics scale.
- Toggle between Edit and Preview modes in the header.
- Export to MP4 (H.264 via WebCodecs), WebM (VP8), or standalone HTML with interactive playback.
- Projects auto-save to localStorage and can be saved/loaded as ZIP files.
git clone https://github.com/djDAOjones/router-plotter-02.git
cd router-plotter-02
npm install
npm run dev # Dev server with watch → http://localhost:3000npm run build # Production bundle → docs/
npm run build:deploy # Build + copy dist → docs/ (GitHub Pages)
npm test # Vitest (jsdom)
npm run push # Build, commit docs/, push to origin/mainindex.html Single-page app shell (sidebar + canvas + controls)
build.js esbuild bundler, version management, dev server
version.json Auto-incremented build number
push.js GitHub Pages deploy helper
src/
main.js RoutePlotter class — app entry point and orchestrator
config/
constants.js All tuneable values (animation, rendering, path, etc.)
keybindings.js Mouse + keyboard bindings (customisable via localStorage)
helpContent.js Welcome modal and inline help HTML generators
tooltips.js Tooltip definitions
components/
SwatchPicker.js Okabe-Ito colour-blind safe palette picker
Dropdown.js Accessible dropdown menus
Tooltip.js Tooltip attachment
ParamTooltip.js Click-label parameter tooltips (Carbon pattern)
controllers/
UIController.js Sidebar controls, waypoint list, slider sync
SectionController.js Collapsible settings sections
core/
EventBus.js Pub-sub event system
handlers/
InteractionHandler.js Mouse, keyboard, touch, and drag-and-drop input
models/
Waypoint.js Waypoint data model (position, style, camera, area, etc.)
AnimationState.js Playback state (progress, timing, pause tracking)
ImageAsset.js Custom image references (marker, path head)
services/
AnimationEngine.js Playback loop, timing, segment speed, pause markers
PathCalculator.js Catmull-Rom spline, reparameterisation, curvature
PathCalculatorWithWorker.js Web Worker wrapper (falls back to main thread)
RenderingService.js Canvas drawing — path, markers, labels, overlays
BeaconRenderer.js Animated waypoint effects (ripple, glow, pop, grow, pulse)
TextLabelService.js Text label layout, fade, auto-positioning
MotionVisibilityService.js Path/waypoint/background visibility calculations
CameraService.js Per-waypoint zoom with continuous interpolation
CoordinateTransform.js Image ↔ canvas coordinate conversion
VideoExporter.js MP4/WebM export (WebCodecs primary, MediaRecorder fallback)
HTMLExportService.js Self-contained HTML export with embedded player
ImageAssetService.js Custom image management and deduplication
StorageService.js localStorage with debounce and change detection
UndoService.js 150-step undo/redo history
AreaDrawingService.js Polygon area drawing mode
AreaEditService.js Area highlight repositioning and vertex editing
AreaHighlightRenderer.js Per-waypoint area highlight rendering
utils/
CatmullRom.js Catmull-Rom spline interpolation
Easing.js Easing functions (linear, quad, cubic, etc.)
focusTrap.js Modal focus trapping for accessibility
workers/
pathWorker.js Web Worker for off-thread path calculation
styles/
tokens.css Design tokens — UoN palette, semantic colours, spacing
main.css Core layout, sidebar, canvas, controls, modals
swatch-picker.css Swatch picker grid (5×2, 44px AAA touch targets)
dropdown.css Dropdown component styles
tooltip.css Tooltip styles
tests/
example.test.js Unit tests (Waypoint, AnimationState, Path, EventBus, etc.)
setup.js Vitest jsdom setup
docs/ Build output served by GitHub Pages
RoutePlotter (in main.js) is the single orchestrator. It owns all services, handles EventBus events, manages application state, and drives the render loop.
There is no framework. The app is pure JavaScript with Canvas 2D rendering and vanilla DOM for the sidebar UI.
Components talk through EventBus (pub-sub), not direct method calls:
User clicks canvas → InteractionHandler emits event
→ main.js handles event, updates Waypoint model
→ main.js calls queueRender()
→ RenderingService draws the frame
User moves slider → UIController emits event
→ main.js handles event, updates state
→ main.js recalculates timing / path
→ main.js calls queueRender()
| Event prefix | Source | Purpose |
|---|---|---|
waypoint:* |
InteractionHandler, UIController | Add, delete, select, move, restyle waypoints |
animation:* |
AnimationEngine, UIController | Play, pause, reset, speed, seek |
ui:* |
UIController | Slider sync, mode changes, export triggers |
video:* |
VideoExporter | Export lifecycle (started, progress, complete, error) |
area:* |
AreaDrawingService, AreaEditService | Area highlight draw/edit |
undo:* |
UndoService | State snapshot/restore |
queueRender()batches requests viarequestAnimationFrame(one frame per tick).render()builds arenderStateobject from current waypoints, animation progress, motion settings, camera, and preview mode.RenderingServicedraws layers in order: background → tint overlay → area highlights → path → waypoints → labels → path head → beacons.MotionVisibilityServicecomputes per-frame visibility/opacity for path, waypoints, and background based on animation progress and the active visibility mode.CameraServiceapplies zoom/pan transforms from per-waypoint camera keyframes.
Waypoints are stored in normalised image coordinates (0–1). The CoordinateTransform service converts between:
- Image coords (
imgX,imgY) — storage and serialisation. - Canvas coords (
x,y) — rendering and hit-testing.
Zoom, pan, and fit/fill mode are handled inside the transform. Path points are recalculated when the canvas resizes.
State is debounce-saved to routePlotter_autosave on every change. Loaded on startup. Includes waypoints, styles, motion settings, background reference, animation state, and export settings.
Other localStorage keys: routePlotter_preferences, routePlotter_splashShown, routePlotter_customKeybindings.
Save Project packages all state (including the background image) into a .zip file. Open Project restores from a .zip.
- MP4: H.264 via WebCodecs + mediabunny muxer. Hardware-accelerated, immune to background-tab throttling. Requires even dimensions (auto-rounded).
- WebM: VP8 via WebCodecs + mediabunny. Falls back to MediaRecorder on browsers without WebCodecs (Firefox, older Safari).
- Configurable resolution (up to 7680×4320), frame rate (10–60 fps), aspect ratio presets, and path-only (transparent) mode.
Self-contained HTML file with embedded base64 background image and a full JavaScript player. Supports all visibility modes, beacons, camera, labels, and interactive scrubbing. 80–95% smaller than equivalent video.
Format: major.minor.build (e.g. 3.1.530).
- major.minor — set manually in
package.json. - build — auto-incremented in
version.jsononce per dev-server start or production build.
The combined string is injected at build time via esbuild's define as APP_VERSION.
| Change | Version bumps? |
|---|---|
Edit JS in src/ |
Build increments on next npm run dev restart or npm run build |
| Edit CSS/HTML only | No (static files are copied, not rebuilt) |
| Force bump after CSS | Restart dev server, or touch src/main.js |
All shortcuts live in src/config/keybindings.js. User overrides are stored in routePlotter_customKeybindings localStorage key and merged at load time.
Each binding specifies: key, modifiers (meta/alt/shift), action (EventBus event name), description, and category.
meta maps to Cmd on macOS, Ctrl on Windows/Linux.
The in-app help panel (press ?) renders all bindings dynamically from this config.
All tuneable values are in src/config/constants.js, grouped by concern:
| Group | Key values |
|---|---|
ANIMATION |
DEFAULT_SPEED 200 px/s, DEFAULT_DURATION 10 000 ms, DEFAULT_WAIT_TIME 1 500 ms, TARGET_FPS 60 |
VIDEO_EXPORT |
DEFAULT_FRAME_RATE 25 fps, DEFAULT_BITRATE 20 Mbps, START_BUFFER_MS 2 000 ms |
RENDERING |
DEFAULT_PATH_COLOR #D55E00, DEFAULT_DOT_SIZE 8 px, MINOR_DOT_SIZE 4 px, CONTROLS_HEIGHT 80 px |
PATH |
POINTS_PER_SEGMENT 100, DEFAULT_TENSION 0.1, TARGET_SPACING 2 px, MIN_CORNER_SPEED 0.2 |
MOTION |
Trail default 20%, spotlight 10% canvas, AoV 60°/25%/50%, timeline handles 2 s + 3 s |
INTERACTION |
Hit radius 15 px, drag threshold 3 px, double-click 300 ms |
TEXT_LABEL |
16–48 px font, 15% width, 0.85 bg opacity, 500 ms fade, 8-direction auto-position |
AREA_HIGHLIGHT |
Circle/rectangle/polygon, Okabe-Ito fill, configurable border, fade in/out, same visibility modes as waypoints |
STORAGE |
Autosave debounce 1 000 ms |
| Enum | Values |
|---|---|
PATH_VISIBILITY |
always-show, show-on-progression, hide-on-progression, instantaneous (comet), always-hide |
WAYPOINT_VISIBILITY |
always-show, hide-before, hide-after, hide-before-and-after, always-hide |
BACKGROUND_VISIBILITY |
always-show, spotlight, spotlight-reveal, angle-of-view, angle-of-view-reveal, always-hide |
TEXT_VISIBILITY |
off, on, fade-up, fade-up-down |
AREA_VISIBILITY |
Same five modes as WAYPOINT_VISIBILITY |
- Add the property with a default in
Waypoint.jsconstructor. - Include it in
toJSON()and handle it infromJSON(). - Add a UI control in the appropriate
index.htmlsettings section. - Wire the control in
UIController.jsto emit an EventBus event. - Handle the event in
main.js(update waypoint, callqueueRender()).
- Add the value to the relevant state object in
RoutePlotterconstructor (motionSettings,exportSettings,styles). - Add a constant/default in
constants.js. - Add UI control in
index.html, wire inUIController.js. - Handle in
main.js, persist in auto-save and project save/load.
Edit RenderingService.js. Drawing methods follow the naming pattern render*(). The rendering order is defined in the LAYERS constants.
- Console: The app intercepts
console.log/warn/errorinto a 500-entry ring buffer. Use Export → Download Debug Log or Copy Debug Log to capture it as markdown. - Browser DevTools: Check the Console tab and Network tab.
- Version: Shown in the header tooltip and page title.
- Don't edit
docs/— it is generated by the build. Edit source files insrc/,styles/, orindex.html. - Imports at top only — esbuild bundles from
src/main.js. Never import mid-file. - Coordinate transform — always use
CoordinateTransform.canvasToImage()/imageToCanvas()when converting between screen and storage positions. - Autosave can get stuck — if the app enters a bad state, clear
routePlotter_autosavein browser DevTools → Application → Local Storage. - Slider feedback loops — programmatic slider updates must go through
ui:slider:update-speedto avoid re-triggering input event handlers. CheckisUpdatingSliderflag inUIController. - H.264 even dimensions — MP4 export requires even width and height. The exporter auto-rounds, but custom resolution inputs can produce odd values.
- Web Worker fallback —
PathCalculatorWithWorkerinitialises a Web Worker for off-thread path calculation. If it fails (e.g. CORS), it falls back to the main-threadPathCalculatorsilently. - mediabunny is the only runtime dependency — it provides the MP4/WebM mux layer. Everything else is vanilla JS.
Precise terms used across the codebase.
- Route — Full journey from first to last waypoint.
- Path — Interpolated Catmull-Rom spline connecting waypoints.
- Path points — Dense array of
{x, y}coordinates defining the path. - Major waypoint — Full-featured: labels, pauses, beacons, area highlights, larger marker.
- Minor waypoint — Path shaping only: smaller marker, no pause/label/beacon.
- Marker — Visual dot, square, flag, custom image, or none.
- Path head — Leading indicator at current animation position (arrow, dot, custom image, or none).
- Beacon — Animated effect at waypoints: ripple, glow, pop, grow, pulse.
- Label — Text attached to a waypoint with fade/visibility modes.
- Area highlight — Per-waypoint overlay region (circle, rectangle, or drawn polygon) with visibility timing.
- Tint — Background overlay from −100 (black) to +100 (white).
- Spotlight — Circular background reveal around the path head.
- Angle of view — Cone-shaped background reveal from the path head.
- Trail — In comet mode, the visible path segment behind the head.
- Segment speed — Per-segment speed multiplier (0.1x–10x).
- Progress — Animation position, 0.0–1.0.
- Image coordinates — Normalised 0–1 position on the background image. Used for storage.
- Canvas coordinates — Screen-pixel position. Used for rendering and hit-testing.
- Graphics scale — Global multiplier (0.25×–4×) applied to all vector element sizes.
MIT
Joe Bell — University of Nottingham