remotion-claude-agent-demo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -0
- package/apps/web/README.md +36 -0
- package/apps/web/env.example +20 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package-lock.json +10348 -0
- package/apps/web/package.json +35 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
- package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
- package/apps/web/src/app/events/stream/route.ts +58 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +174 -0
- package/apps/web/src/app/layout.tsx +34 -0
- package/apps/web/src/app/messages/answer/route.ts +57 -0
- package/apps/web/src/app/messages/stream/route.ts +381 -0
- package/apps/web/src/app/page.tsx +358 -0
- package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
- package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
- package/apps/web/src/app/tasks/route.ts +13 -0
- package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
- package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
- package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
- package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
- package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
- package/apps/web/src/lib/a2a/activity-store.ts +150 -0
- package/apps/web/src/lib/a2a/client.ts +357 -0
- package/apps/web/src/lib/a2a/sse.ts +19 -0
- package/apps/web/src/lib/a2a/task-store.ts +111 -0
- package/apps/web/src/lib/a2a/types.ts +216 -0
- package/apps/web/src/lib/agent/answer-store.ts +109 -0
- package/apps/web/src/lib/agent/background-delivery.ts +343 -0
- package/apps/web/src/lib/agent/background-tool.ts +78 -0
- package/apps/web/src/lib/agent/background.ts +452 -0
- package/apps/web/src/lib/agent/chat.ts +543 -0
- package/apps/web/src/lib/agent/session-store.ts +26 -0
- package/apps/web/src/lib/chat/types.ts +44 -0
- package/apps/web/src/lib/env.ts +31 -0
- package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
- package/apps/web/src/lib/state/chat-atoms.ts +52 -0
- package/apps/web/src/lib/workspace.ts +9 -0
- package/apps/web/tsconfig.json +35 -0
- package/bin/remotion-agent.js +451 -0
- package/package.json +34 -0
- package/templates/.claude/CLAUDE.md +95 -0
- package/templates/.claude/README.md +129 -0
- package/templates/.claude/agents/composer-agent.md +188 -0
- package/templates/.claude/agents/crafter.md +181 -0
- package/templates/.claude/agents/creator.md +134 -0
- package/templates/.claude/agents/perceiver.md +92 -0
- package/templates/.claude/settings.json +36 -0
- package/templates/.claude/settings.local.json +39 -0
- package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
- package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
- package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
- package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
- package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
- package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
- package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
- package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
- package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
- package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
- package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
- package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
- package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
- package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
- package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
- package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
- package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
- package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
- package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
- package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
- package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
- package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
- package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
- package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
- package/templates/.claude/skills/render-config/SKILL.md +290 -0
- package/templates/.claude/skills/script-writer/SKILL.md +59 -0
- package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
- package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
- package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
- package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
- package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
- package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
- package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
- package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
- package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
- package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
- package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
- package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
- package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
- package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
- package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
- package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: maps
|
|
3
|
+
description: Make map animations with Mapbox
|
|
4
|
+
metadata:
|
|
5
|
+
tags: map, map animation, mapbox
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Maps can be added to a Remotion video with Mapbox.
|
|
9
|
+
The [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/api/) has the API reference.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Mapbox and `@turf/turf` need to be installed.
|
|
14
|
+
|
|
15
|
+
Search the project for lockfiles and run the correct command depending on the package manager:
|
|
16
|
+
|
|
17
|
+
If `package-lock.json` is found, use the following command:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i mapbox-gl @turf/turf @types/mapbox-gl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If `bun.lock` is found, use the following command:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun i mapbox-gl @turf/turf @types/mapbox-gl
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If `yarn.lock` is found, use the following command:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
yarn add mapbox-gl @turf/turf @types/mapbox-gl
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If `pnpm-lock.yaml` is found, use the following command:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm i mapbox-gl @turf/turf @types/mapbox-gl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.
|
|
42
|
+
|
|
43
|
+
The mapbox token needs to be added to the `.env` file:
|
|
44
|
+
|
|
45
|
+
```txt title=".env"
|
|
46
|
+
REMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Adding a map
|
|
50
|
+
|
|
51
|
+
Here is a basic example of a map in Remotion.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import {useEffect, useMemo, useRef, useState} from 'react';
|
|
55
|
+
import {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';
|
|
56
|
+
import mapboxgl, {Map} from 'mapbox-gl';
|
|
57
|
+
|
|
58
|
+
export const lineCoordinates = [
|
|
59
|
+
[6.56158447265625, 46.059891147620725],
|
|
60
|
+
[6.5691375732421875, 46.05679376154153],
|
|
61
|
+
[6.5842437744140625, 46.05059898938315],
|
|
62
|
+
[6.594886779785156, 46.04702502069337],
|
|
63
|
+
[6.601066589355469, 46.0460718554722],
|
|
64
|
+
[6.6089630126953125, 46.0365370783104],
|
|
65
|
+
[6.6185760498046875, 46.018420689207964],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
mapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;
|
|
69
|
+
|
|
70
|
+
export const MyComposition = () => {
|
|
71
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
72
|
+
const {delayRender, continueRender} = useDelayRender();
|
|
73
|
+
|
|
74
|
+
const {width, height} = useVideoConfig();
|
|
75
|
+
const [handle] = useState(() => delayRender('Loading map...'));
|
|
76
|
+
const [map, setMap] = useState<Map | null>(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const _map = new Map({
|
|
80
|
+
container: ref.current!,
|
|
81
|
+
zoom: 11.53,
|
|
82
|
+
center: [6.5615, 46.0598],
|
|
83
|
+
pitch: 65,
|
|
84
|
+
bearing: 0,
|
|
85
|
+
style: 'mapbox://styles/mapbox/standard',
|
|
86
|
+
interactive: false,
|
|
87
|
+
fadeDuration: 0,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
_map.on('style.load', () => {
|
|
91
|
+
// Hide all features from the Mapbox Standard style
|
|
92
|
+
const hideFeatures = [
|
|
93
|
+
'showRoadsAndTransit',
|
|
94
|
+
'showRoads',
|
|
95
|
+
'showTransit',
|
|
96
|
+
'showPedestrianRoads',
|
|
97
|
+
'showRoadLabels',
|
|
98
|
+
'showTransitLabels',
|
|
99
|
+
'showPlaceLabels',
|
|
100
|
+
'showPointOfInterestLabels',
|
|
101
|
+
'showPointsOfInterest',
|
|
102
|
+
'showAdminBoundaries',
|
|
103
|
+
'showLandmarkIcons',
|
|
104
|
+
'showLandmarkIconLabels',
|
|
105
|
+
'show3dObjects',
|
|
106
|
+
'show3dBuildings',
|
|
107
|
+
'show3dTrees',
|
|
108
|
+
'show3dLandmarks',
|
|
109
|
+
'show3dFacades',
|
|
110
|
+
];
|
|
111
|
+
for (const feature of hideFeatures) {
|
|
112
|
+
_map.setConfigProperty('basemap', feature, false);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_map.setConfigProperty('basemap', 'colorMotorways', 'rgba(0, 0, 0, 0)');
|
|
116
|
+
_map.setConfigProperty('basemap', 'colorRoads', 'rgba(0, 0, 0, 0)');
|
|
117
|
+
_map.setConfigProperty('basemap', 'colorTrunks', 'rgba(0, 0, 0, 0)');
|
|
118
|
+
|
|
119
|
+
_map.addSource('trace', {
|
|
120
|
+
type: 'geojson',
|
|
121
|
+
data: {
|
|
122
|
+
type: 'Feature',
|
|
123
|
+
properties: {},
|
|
124
|
+
geometry: {
|
|
125
|
+
type: 'LineString',
|
|
126
|
+
coordinates: lineCoordinates,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
_map.addLayer({
|
|
131
|
+
type: 'line',
|
|
132
|
+
source: 'trace',
|
|
133
|
+
id: 'line',
|
|
134
|
+
paint: {
|
|
135
|
+
'line-color': 'black',
|
|
136
|
+
'line-width': 5,
|
|
137
|
+
},
|
|
138
|
+
layout: {
|
|
139
|
+
'line-cap': 'round',
|
|
140
|
+
'line-join': 'round',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
_map.on('load', () => {
|
|
146
|
+
continueRender(handle);
|
|
147
|
+
setMap(_map);
|
|
148
|
+
});
|
|
149
|
+
}, [handle, lineCoordinates]);
|
|
150
|
+
|
|
151
|
+
const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);
|
|
152
|
+
|
|
153
|
+
return <AbsoluteFill ref={ref} style={style} />;
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The following is important in Remotion:
|
|
158
|
+
|
|
159
|
+
- Animations must be driven by `useCurrentFrame()` and animations that Mapbox brings itself should be disabled. For example, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.
|
|
160
|
+
- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.
|
|
161
|
+
- The element containing the ref MUST have an explicit width and height and `position: "absolute"`.
|
|
162
|
+
- Do not add a `_map.remove();` cleanup function.
|
|
163
|
+
|
|
164
|
+
## Drawing lines
|
|
165
|
+
|
|
166
|
+
Unless I request it, do not add a glow effect to the lines.
|
|
167
|
+
Unless I request it, do not add additional points to the lines.
|
|
168
|
+
|
|
169
|
+
## Map style
|
|
170
|
+
|
|
171
|
+
By default, use the `mapbox://styles/mapbox/standard` style.
|
|
172
|
+
Hide the labels from the base map style.
|
|
173
|
+
|
|
174
|
+
Unless I request otherwise, remove all features from the Mapbox Standard style.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
// Hide all features from the Mapbox Standard style
|
|
178
|
+
const hideFeatures = [
|
|
179
|
+
'showRoadsAndTransit',
|
|
180
|
+
'showRoads',
|
|
181
|
+
'showTransit',
|
|
182
|
+
'showPedestrianRoads',
|
|
183
|
+
'showRoadLabels',
|
|
184
|
+
'showTransitLabels',
|
|
185
|
+
'showPlaceLabels',
|
|
186
|
+
'showPointOfInterestLabels',
|
|
187
|
+
'showPointsOfInterest',
|
|
188
|
+
'showAdminBoundaries',
|
|
189
|
+
'showLandmarkIcons',
|
|
190
|
+
'showLandmarkIconLabels',
|
|
191
|
+
'show3dObjects',
|
|
192
|
+
'show3dBuildings',
|
|
193
|
+
'show3dTrees',
|
|
194
|
+
'show3dLandmarks',
|
|
195
|
+
'show3dFacades',
|
|
196
|
+
];
|
|
197
|
+
for (const feature of hideFeatures) {
|
|
198
|
+
_map.setConfigProperty('basemap', feature, false);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_map.setConfigProperty('basemap', 'colorMotorways', 'transparent');
|
|
202
|
+
_map.setConfigProperty('basemap', 'colorRoads', 'transparent');
|
|
203
|
+
_map.setConfigProperty('basemap', 'colorTrunks', 'transparent');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Animating the camera
|
|
207
|
+
|
|
208
|
+
You can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.
|
|
209
|
+
|
|
210
|
+
Unless I ask for it, do not jump between camera angles.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import * as turf from '@turf/turf';
|
|
214
|
+
import {interpolate} from 'remotion';
|
|
215
|
+
import {Easing} from 'remotion';
|
|
216
|
+
import {useCurrentFrame, useVideoConfig, useDelayRender} from 'remotion';
|
|
217
|
+
|
|
218
|
+
const animationDuration = 20;
|
|
219
|
+
const cameraAltitude = 4000;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
const frame = useCurrentFrame();
|
|
224
|
+
const {fps} = useVideoConfig();
|
|
225
|
+
const {delayRender, continueRender} = useDelayRender();
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!map) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const handle = delayRender('Moving point...');
|
|
232
|
+
|
|
233
|
+
const routeDistance = turf.length(turf.lineString(lineCoordinates));
|
|
234
|
+
|
|
235
|
+
const progress = interpolate(frame / fps, [0.00001, animationDuration], [0, 1], {
|
|
236
|
+
easing: Easing.inOut(Easing.sin),
|
|
237
|
+
extrapolateLeft: 'clamp',
|
|
238
|
+
extrapolateRight: 'clamp',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const camera = map.getFreeCameraOptions();
|
|
242
|
+
|
|
243
|
+
const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;
|
|
244
|
+
|
|
245
|
+
camera.lookAtPoint({
|
|
246
|
+
lng: alongRoute[0],
|
|
247
|
+
lat: alongRoute[1],
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
map.setFreeCameraOptions(camera);
|
|
251
|
+
map.once('idle', () => continueRender(handle));
|
|
252
|
+
}, [lineCoordinates, fps, frame, handle, map]);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Notes:
|
|
256
|
+
|
|
257
|
+
IMPORTANT: Keep the camera by default so north is up.
|
|
258
|
+
IMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.
|
|
259
|
+
|
|
260
|
+
- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors
|
|
261
|
+
- See [Timing](./timing.md) for more options for timing.
|
|
262
|
+
- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.
|
|
263
|
+
|
|
264
|
+
## Animating lines
|
|
265
|
+
|
|
266
|
+
### Straight lines (linear interpolation)
|
|
267
|
+
|
|
268
|
+
To animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's `lineSliceAlong` or `along` functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
const frame = useCurrentFrame();
|
|
272
|
+
const {durationInFrames} = useVideoConfig();
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (!map) return;
|
|
276
|
+
|
|
277
|
+
const animationHandle = delayRender('Animating line...');
|
|
278
|
+
|
|
279
|
+
const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {
|
|
280
|
+
extrapolateLeft: 'clamp',
|
|
281
|
+
extrapolateRight: 'clamp',
|
|
282
|
+
easing: Easing.inOut(Easing.cubic),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Linear interpolation for a straight line on the map
|
|
286
|
+
const start = lineCoordinates[0];
|
|
287
|
+
const end = lineCoordinates[1];
|
|
288
|
+
const currentLng = start[0] + (end[0] - start[0]) * progress;
|
|
289
|
+
const currentLat = start[1] + (end[1] - start[1]) * progress;
|
|
290
|
+
|
|
291
|
+
const lineData: GeoJSON.Feature<GeoJSON.LineString> = {
|
|
292
|
+
type: 'Feature',
|
|
293
|
+
properties: {},
|
|
294
|
+
geometry: {
|
|
295
|
+
type: 'LineString',
|
|
296
|
+
coordinates: [start, [currentLng, currentLat]],
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const source = map.getSource('trace') as mapboxgl.GeoJSONSource;
|
|
301
|
+
if (source) {
|
|
302
|
+
source.setData(lineData);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
map.once('idle', () => continueRender(animationHandle));
|
|
306
|
+
}, [frame, map, durationInFrames]);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Curved lines (geodesic/great circle)
|
|
310
|
+
|
|
311
|
+
To animate a line that follows the geodesic (great circle) path between two points, use turf's `lineSliceAlong`. This is useful for showing flight paths or the actual shortest distance on Earth.
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
import * as turf from '@turf/turf';
|
|
315
|
+
|
|
316
|
+
const routeLine = turf.lineString(lineCoordinates);
|
|
317
|
+
const routeDistance = turf.length(routeLine);
|
|
318
|
+
|
|
319
|
+
const currentDistance = Math.max(0.001, routeDistance * progress);
|
|
320
|
+
const slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);
|
|
321
|
+
|
|
322
|
+
const source = map.getSource('route') as mapboxgl.GeoJSONSource;
|
|
323
|
+
if (source) {
|
|
324
|
+
source.setData(slicedLine);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Markers
|
|
329
|
+
|
|
330
|
+
Add labels, and markers where appropriate.
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
_map.addSource('markers', {
|
|
334
|
+
type: 'geojson',
|
|
335
|
+
data: {
|
|
336
|
+
type: 'FeatureCollection',
|
|
337
|
+
features: [
|
|
338
|
+
{
|
|
339
|
+
type: 'Feature',
|
|
340
|
+
properties: {name: 'Point 1'},
|
|
341
|
+
geometry: {type: 'Point', coordinates: [-118.2437, 34.0522]},
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
_map.addLayer({
|
|
348
|
+
id: 'city-markers',
|
|
349
|
+
type: 'circle',
|
|
350
|
+
source: 'markers',
|
|
351
|
+
paint: {
|
|
352
|
+
'circle-radius': 40,
|
|
353
|
+
'circle-color': '#FF4444',
|
|
354
|
+
'circle-stroke-width': 4,
|
|
355
|
+
'circle-stroke-color': '#FFFFFF',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
_map.addLayer({
|
|
360
|
+
id: 'labels',
|
|
361
|
+
type: 'symbol',
|
|
362
|
+
source: 'markers',
|
|
363
|
+
layout: {
|
|
364
|
+
'text-field': ['get', 'name'],
|
|
365
|
+
'text-font': ['DIN Pro Bold', 'Arial Unicode MS Bold'],
|
|
366
|
+
'text-size': 50,
|
|
367
|
+
'text-offset': [0, 0.5],
|
|
368
|
+
'text-anchor': 'top',
|
|
369
|
+
},
|
|
370
|
+
paint: {
|
|
371
|
+
'text-color': '#FFFFFF',
|
|
372
|
+
'text-halo-color': '#000000',
|
|
373
|
+
'text-halo-width': 2,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Make sure they are big enough. Check the composition dimensions and scale the labels accordingly.
|
|
379
|
+
For a composition size of 1920x1080, the label font size should be at least 40px.
|
|
380
|
+
|
|
381
|
+
IMPORTANT: Keep the `text-offset` small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
"text-offset": [0, 0.5],
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## 3D buildings
|
|
388
|
+
|
|
389
|
+
To enable 3D buildings, use the following code:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
_map.setConfigProperty('basemap', 'show3dObjects', true);
|
|
393
|
+
_map.setConfigProperty('basemap', 'show3dLandmarks', true);
|
|
394
|
+
_map.setConfigProperty('basemap', 'show3dBuildings', true);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Rendering
|
|
398
|
+
|
|
399
|
+
When rendering a map animation, make sure to render with the following flags:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
npx remotion render --gl=angle --concurrency=1
|
|
403
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: measuring-dom-nodes
|
|
3
|
+
description: Measuring DOM element dimensions in Remotion
|
|
4
|
+
metadata:
|
|
5
|
+
tags: measure, layout, dimensions, getBoundingClientRect, scale
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Measuring DOM nodes in Remotion
|
|
9
|
+
|
|
10
|
+
Remotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.
|
|
11
|
+
|
|
12
|
+
## Measuring element dimensions
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { useCurrentScale } from "remotion";
|
|
16
|
+
import { useRef, useEffect, useState } from "react";
|
|
17
|
+
|
|
18
|
+
export const MyComponent = () => {
|
|
19
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
20
|
+
const scale = useCurrentScale();
|
|
21
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!ref.current) return;
|
|
25
|
+
const rect = ref.current.getBoundingClientRect();
|
|
26
|
+
setDimensions({
|
|
27
|
+
width: rect.width / scale,
|
|
28
|
+
height: rect.height / scale,
|
|
29
|
+
});
|
|
30
|
+
}, [scale]);
|
|
31
|
+
|
|
32
|
+
return <div ref={ref}>Content to measure</div>;
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: measuring-text
|
|
3
|
+
description: Measuring text dimensions, fitting text to containers, and checking overflow
|
|
4
|
+
metadata:
|
|
5
|
+
tags: measure, text, layout, dimensions, fitText, fillTextBox
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Measuring text in Remotion
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
Install @remotion/layout-utils if it is not already installed:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx remotion add @remotion/layout-utils # If project uses npm
|
|
16
|
+
bunx remotion add @remotion/layout-utils # If project uses bun
|
|
17
|
+
yarn remotion add @remotion/layout-utils # If project uses yarn
|
|
18
|
+
pnpm exec remotion add @remotion/layout-utils # If project uses pnpm
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Measuring text dimensions
|
|
22
|
+
|
|
23
|
+
Use `measureText()` to calculate the width and height of text:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { measureText } from "@remotion/layout-utils";
|
|
27
|
+
|
|
28
|
+
const { width, height } = measureText({
|
|
29
|
+
text: "Hello World",
|
|
30
|
+
fontFamily: "Arial",
|
|
31
|
+
fontSize: 32,
|
|
32
|
+
fontWeight: "bold",
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Results are cached - duplicate calls return the cached result.
|
|
37
|
+
|
|
38
|
+
## Fitting text to a width
|
|
39
|
+
|
|
40
|
+
Use `fitText()` to find the optimal font size for a container:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { fitText } from "@remotion/layout-utils";
|
|
44
|
+
|
|
45
|
+
const { fontSize } = fitText({
|
|
46
|
+
text: "Hello World",
|
|
47
|
+
withinWidth: 600,
|
|
48
|
+
fontFamily: "Inter",
|
|
49
|
+
fontWeight: "bold",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
fontSize: Math.min(fontSize, 80), // Cap at 80px
|
|
56
|
+
fontFamily: "Inter",
|
|
57
|
+
fontWeight: "bold",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
Hello World
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Checking text overflow
|
|
66
|
+
|
|
67
|
+
Use `fillTextBox()` to check if text exceeds a box:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { fillTextBox } from "@remotion/layout-utils";
|
|
71
|
+
|
|
72
|
+
const box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });
|
|
73
|
+
|
|
74
|
+
const words = ["Hello", "World", "This", "is", "a", "test"];
|
|
75
|
+
for (const word of words) {
|
|
76
|
+
const { exceedsBox } = box.add({
|
|
77
|
+
text: word + " ",
|
|
78
|
+
fontFamily: "Arial",
|
|
79
|
+
fontSize: 24,
|
|
80
|
+
});
|
|
81
|
+
if (exceedsBox) {
|
|
82
|
+
// Text would overflow, handle accordingly
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Best practices
|
|
89
|
+
|
|
90
|
+
**Load fonts first:** Only call measurement functions after fonts are loaded.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { loadFont } from "@remotion/google-fonts/Inter";
|
|
94
|
+
|
|
95
|
+
const { fontFamily, waitUntilDone } = loadFont("normal", {
|
|
96
|
+
weights: ["400"],
|
|
97
|
+
subsets: ["latin"],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
waitUntilDone().then(() => {
|
|
101
|
+
// Now safe to measure
|
|
102
|
+
const { width } = measureText({
|
|
103
|
+
text: "Hello",
|
|
104
|
+
fontFamily,
|
|
105
|
+
fontSize: 32,
|
|
106
|
+
});
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Use validateFontIsLoaded:** Catch font loading issues early:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
measureText({
|
|
114
|
+
text: "Hello",
|
|
115
|
+
fontFamily: "MyCustomFont",
|
|
116
|
+
fontSize: 32,
|
|
117
|
+
validateFontIsLoaded: true, // Throws if font not loaded
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Match font properties:** Use the same properties for measurement and rendering:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
const fontStyle = {
|
|
125
|
+
fontFamily: "Inter",
|
|
126
|
+
fontSize: 32,
|
|
127
|
+
fontWeight: "bold" as const,
|
|
128
|
+
letterSpacing: "0.5px",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const { width } = measureText({
|
|
132
|
+
text: "Hello",
|
|
133
|
+
...fontStyle,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return <div style={fontStyle}>Hello</div>;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<div style={{ outline: "2px solid red" }}>Text</div>
|
|
143
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: parameters
|
|
3
|
+
description: Make a video parametrizable by adding a Zod schema
|
|
4
|
+
metadata:
|
|
5
|
+
tags: parameters, zod, schema
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
To make a video parametrizable, a Zod schema can be added to a composition.
|
|
9
|
+
|
|
10
|
+
First, `zod` must be installed - it must be exactly version `3.22.3`.
|
|
11
|
+
|
|
12
|
+
Search the project for lockfiles and run the correct command depending on the package manager:
|
|
13
|
+
|
|
14
|
+
If `package-lock.json` is found, use the following command:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i zod@3.22.3
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If `bun.lockb` is found, use the following command:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun i zod@3.22.3
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If `yarn.lock` is found, use the following command:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
yarn add zod@3.22.3
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If `pnpm-lock.yaml` is found, use the following command:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm i zod@3.22.3
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then, a Zod schema can be defined alongside the component:
|
|
39
|
+
|
|
40
|
+
```tsx title="src/MyComposition.tsx"
|
|
41
|
+
import {z} from 'zod';
|
|
42
|
+
|
|
43
|
+
export const MyCompositionSchema = z.object({
|
|
44
|
+
title: z.string(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = () => {
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<h1>{props.title}</h1>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In the root file, the schema can be passed to the composition:
|
|
57
|
+
|
|
58
|
+
```tsx title="src/Root.tsx"
|
|
59
|
+
import {Composition} from 'remotion';
|
|
60
|
+
import {MycComponent, MyCompositionSchema} from './MyComposition';
|
|
61
|
+
|
|
62
|
+
export const RemotionRoot = () => {
|
|
63
|
+
return <Composition id="MyComposition" component={MyComponent} durationInFrames={100} fps={30} width={1080} height={1080} defaultProps={{title: 'Hello World'}} schema={MyCompositionSchema} />;
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Now, the user can edit the parameter visually in the sidebar.
|
|
68
|
+
|
|
69
|
+
All schemas that are supported by Zod are supported by Remotion.
|
|
70
|
+
|
|
71
|
+
Remotion requires that the top-level type is a z.object(), because the collection of props of a React component is always an object.
|
|
72
|
+
|
|
73
|
+
## Color picker
|
|
74
|
+
|
|
75
|
+
For adding a color picker, use `zColor()` from `@remotion/zod-types`.
|
|
76
|
+
|
|
77
|
+
If it is not installed, use the following command:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx remotion add @remotion/zod-types # If project uses npm
|
|
81
|
+
bunx remotion add @remotion/zod-types # If project uses bun
|
|
82
|
+
yarn remotion add @remotion/zod-types # If project uses yarn
|
|
83
|
+
pnpm exec remotion add @remotion/zod-types # If project uses pnpm
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then import `zColor` from `@remotion/zod-types`:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import {zColor} from '@remotion/zod-types';
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then use it in the schema:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
export const MyCompositionSchema = z.object({
|
|
96
|
+
color: zColor(),
|
|
97
|
+
});
|
|
98
|
+
```
|