Equilibriumpress

RouteVideo Remotion Skill

- Route still uses GeoJSON `[lon, lat]` coordinates.

Equilibriumpress 0 Updated 2w ago

Resources

8
GitHub

Install

npx skillscat add equilibriumpress/routevideo

Install via the SkillsCat registry.

SKILL.md

RouteVideo Remotion Skill

Use this file as the project knowledge base for ChatGPT, Claude, Copilot or another coding agent working on this repository.

Mission

Build and improve RouteVideo: a focused route animation app made with React, Vite, MapLibre GL JS, Turf.js and Remotion.

The product flow is simple:

  1. User enters an origin and destination.
  2. The app geocodes both locations.
  3. The app creates either a road route or a flight route.
  4. The user previews the route animation in a Remotion Player.
  5. The user exports an MP4 through a Remotion render server.

Do not turn this into a general video editor. Keep the MVP focused on fast route videos.

Source of truth

Use the official Remotion documentation as the first source:

When changing Remotion code, check the official docs before guessing.

Current stack

  • React
  • Vite
  • TypeScript
  • Remotion
  • @remotion/player
  • @remotion/renderer
  • @remotion/bundler
  • MapLibre GL JS
  • Turf.js
  • OSRM for car routes
  • Nominatim for geocoding
  • GitHub Pages for static preview hosting
  • Local Node/Express server for MP4 rendering

Important architecture

The app has two layers.

1. Editor layer

Files:

  • src/App.tsx
  • src/lib/routing.ts
  • src/defaultProject.ts
  • src/styles.css

The editor owns route input, geocoding, routing, settings and Player preview.

Use <Player> from @remotion/player for interactive preview.

Use a changing key when project settings change, because a crashed or stale Player can require remounting.

2. Remotion composition layer

Files:

  • src/remotion/index.ts
  • src/remotion/Root.tsx
  • src/remotion/RouteVideo.tsx
  • src/lib/routeMath.ts
  • src/lib/camera.ts
  • src/lib/mapStyles.ts

The composition must be deterministic from props. Do not read editor state directly. All render data must come through inputProps.

The shared project object contains:

  • transportMode
  • originName
  • destinationName
  • originCoordinate
  • destinationCoordinate
  • routeGeoJSON
  • durationSeconds
  • speedMultiplier
  • cameraMode
  • mapStyle
  • labelsEnabled
  • aspectRatio

speedMultiplier is kept for backwards compatibility. The UI no longer exposes speed. Video progress should run from 0% to 100% over the chosen duration.

Current camera modes

  • cinematic: default. Starts closer to the origin, moves wider through the middle, ends closer to the destination.
  • overview: stable whole-route view.
  • follow: follows the moving marker and rotates with route bearing.

Keep camera logic in src/lib/camera.ts.

Keep Turf route math in src/lib/routeMath.ts.

Do not put camera logic back into RouteVideo.tsx unless it is presentation-only.

Remotion map rules

Use the official Remotion map animation pattern.

For Remotion renders, MapLibre must be initialized with:

  • interactive: false
  • attributionControl: false
  • fadeDuration: 0
  • canvasContextAttributes: { preserveDrawingBuffer: true }

The map container must have explicit width and height from useVideoConfig() and position: 'absolute'.

Use useDelayRender() and continueRender() so Remotion waits until the map and the current frame are ready.

Recommended pattern:

  • create map once inside useEffect()
  • add GeoJSON sources after load
  • continue initial render after idle
  • on each frame, update sources with setData()
  • move camera with jumpTo() or calculateCameraOptionsFromTo()
  • call triggerRepaint() after camera/source updates
  • continue frame render after idle

Do not rely on MapLibre's own animation methods such as easeTo() for rendered video. Remotion must control animation per frame.

Route animation rules

Flight route

Use Turf great-circle for flight mode.

Rules:

  • create a route with turf.greatCircle()
  • use enough points, usually 100 to 200
  • if Turf returns a MultiLineString, choose the longest segment
  • slice the route with turf.lineSliceAlong()
  • keep the slice non-empty at progress 0 by using a tiny minimum distance such as 0.001

Car route

Use OSRM for the road polyline.

Rules:

  • request overview=full
  • request geometries=geojson
  • if routing fails, fall back to a straight line or show a clear error
  • keep the RouteVideo component agnostic about where the route came from

Marker motion

Move the marker with Turf:

  • compute total route length
  • compute current distance = route length times progress
  • use turf.along() to get the current coordinate
  • compute bearing from two close progress points
  • rotate the marker using the bearing

The current marker is an SVG overlay, not a MapLibre text symbol. Project it with map.project() after camera updates.

Line reveal

Use a full route layer as a faint base route.

Use a second progress route layer on top. Update the second source each frame.

Never animate line reveal with CSS width. The route is geographic data and must be sliced as GeoJSON.

Rendering rules

Use @remotion/renderer for server-side MP4 export.

Preferred API:

  • bundle the Remotion entry with @remotion/bundler
  • select the composition with selectComposition()
  • render with renderMedia()

Prefer renderMedia() over manual renderFrames() plus stitching unless a special workflow needs individual frames.

Use one render process at a time for the MVP, because MapLibre plus Chromium is resource-heavy.

GitHub Pages rules

GitHub Pages only hosts the static app.

It cannot run the MP4 render server.

On GitHub Pages, keep export disabled and explain that MP4 export needs the local or hosted render server.

Geocoding rules

The current geocoder is Nominatim.

Use it carefully:

  • do explicit search after user action
  • avoid autocomplete against the public endpoint
  • use limit=1 or a small result count
  • keep cache and pending-request deduplication
  • keep at least roughly one second between public Nominatim requests
  • make geocoder endpoint configurable
  • for production, prefer a proxy or paid geocoder

Do not hardcode a secret key into frontend code.

Routing rules

The current car routing service is OSRM.

Use:

  • route/v1/driving
  • coordinates as lon,lat
  • overview=full
  • geometries=geojson

Always remember GeoJSON uses [longitude, latitude], not [latitude, longitude].

Styling rules

Keep the UI premium and focused.

Editor:

  • large video preview
  • compact controls
  • no duplicate map previews unless they add clear value
  • route input and settings stay readable

Video output:

  • route should fill the viewport well
  • labels must not obscure the route
  • overlays should be useful and minimal
  • route line needs contrast against the basemap
  • vehicle marker should be readable but not toy-like

Common mistakes to avoid

  • Do not use MapLibre animations like flyTo() during render.
  • Do not use zero-length Turf slices.
  • Do not use latitude/longitude order by mistake.
  • Do not add random values inside a composition unless seeded and deterministic.
  • Do not make network requests inside the Remotion composition during render if the data can be passed through props.
  • Do not depend on GitHub Pages for MP4 rendering.
  • Do not leave a second static map in the UI without a clear job.
  • Do not create many settings before the core route animation feels strong.

Useful commands

Install:

npm install

Run editor:

npm run dev

Run render server:

npm run server

Open Remotion Studio:

npm run remotion:studio

Render sample:

npm run render:sample

Build static site:

npm run build

Format:

npm run format

Acceptance checklist for future changes

Before saying a change is done, check:

  • TypeScript builds.
  • Vite builds.
  • Player preview still mounts.
  • Route input still geocodes.
  • Car mode still produces an OSRM route or graceful fallback.
  • Plane mode still produces a curved route.
  • GitHub Pages does not depend on server-only code.
  • MP4 export remains server-side.
  • Video composition reads only from props.
  • Route still uses GeoJSON [lon, lat] coordinates.