freemotion 0.0.0-stage β 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/LICENSE +21 -0
- package/README.md +364 -2
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/node/args.d.ts +33 -0
- package/dist/node/args.js +229 -0
- package/dist/node/browser.d.ts +9 -0
- package/dist/node/browser.js +31 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +54 -0
- package/dist/node/ffmpeg.d.ts +20 -0
- package/dist/node/ffmpeg.js +81 -0
- package/dist/node/init.d.ts +15 -0
- package/dist/node/init.js +40 -0
- package/dist/node/logger.d.ts +12 -0
- package/dist/node/logger.js +34 -0
- package/dist/node/ordered-frames.d.ts +16 -0
- package/dist/node/ordered-frames.js +41 -0
- package/dist/node/paths.d.ts +8 -0
- package/dist/node/paths.js +15 -0
- package/dist/node/project.d.ts +4 -0
- package/dist/node/project.js +26 -0
- package/dist/node/render.d.ts +48 -0
- package/dist/node/render.js +185 -0
- package/dist/node/studio.d.ts +13 -0
- package/dist/node/studio.js +14 -0
- package/dist/node/vite-config.d.ts +22 -0
- package/dist/node/vite-config.js +116 -0
- package/dist/runtime/absolute-fill.d.ts +4 -0
- package/dist/runtime/absolute-fill.js +11 -0
- package/dist/runtime/composition.d.ts +26 -0
- package/dist/runtime/composition.js +36 -0
- package/dist/runtime/delay-render.d.ts +16 -0
- package/dist/runtime/delay-render.js +57 -0
- package/dist/runtime/easing.d.ts +33 -0
- package/dist/runtime/easing.js +100 -0
- package/dist/runtime/frame.d.ts +22 -0
- package/dist/runtime/frame.js +22 -0
- package/dist/runtime/img.d.ts +6 -0
- package/dist/runtime/img.js +24 -0
- package/dist/runtime/interpolate.d.ts +16 -0
- package/dist/runtime/interpolate.js +45 -0
- package/dist/runtime/player.d.ts +24 -0
- package/dist/runtime/player.js +53 -0
- package/dist/runtime/random.d.ts +6 -0
- package/dist/runtime/random.js +20 -0
- package/dist/runtime/scroll.d.ts +60 -0
- package/dist/runtime/scroll.js +107 -0
- package/dist/runtime/sequence.d.ts +30 -0
- package/dist/runtime/sequence.js +48 -0
- package/dist/runtime/spring.d.ts +25 -0
- package/dist/runtime/spring.js +37 -0
- package/dist/runtime/stage.d.ts +9 -0
- package/dist/runtime/stage.js +7 -0
- package/dist/runtime/static-file.d.ts +5 -0
- package/dist/runtime/static-file.js +8 -0
- package/dist/shared/render-api.d.ts +20 -0
- package/dist/shared/render-api.js +2 -0
- package/dist/studio/main.d.ts +1 -0
- package/dist/studio/main.js +13 -0
- package/dist/studio/render-page.d.ts +1 -0
- package/dist/studio/render-page.js +62 -0
- package/dist/studio/studio.d.ts +8 -0
- package/dist/studio/studio.js +152 -0
- package/package.json +93 -4
- package/templates/default/README.md +30 -0
- package/templates/default/_gitignore +5 -0
- package/templates/default/node_modules/.vitest-cache/CACHEDIR.TAG +3 -0
- package/templates/default/node_modules/.vitest-cache/_metadata.json +3 -0
- package/templates/default/package.json +35 -0
- package/templates/default/page/index.html +12 -0
- package/templates/default/page/main.tsx +55 -0
- package/templates/default/page/style.css +75 -0
- package/templates/default/page/vite.config.ts +10 -0
- package/templates/default/src/HelloWorld.tsx +88 -0
- package/templates/default/src/Story.tsx +99 -0
- package/templates/default/src/compositions.tsx +34 -0
- package/templates/default/src/env.d.ts +2 -0
- package/templates/default/tsconfig.json +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Douglas Nassif Roma Junior
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,365 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/assets/banner.png" alt="freemotion: make videos with React. Preview live, render anywhere. An ease-in-out curve with a ball drawn at evenly spaced frames." width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
Write videos as React components. Every frame is a function of time, so you can <b>preview</b> them live in the studio, <b>render</b> them to MP4, WebM, GIF or PNG, and <b>play</b> them inside any web page, even driven by the scroll.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/douglasjunior/freemotion?style=flat-square&color=FF4F7B" alt="MIT License"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/freemotion"><img src="https://img.shields.io/npm/v/freemotion?style=flat-square&color=FFB547" alt="npm version"></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/freemotion"><img src="https://img.shields.io/npm/dt/freemotion?style=flat-square" alt="npm downloads"></a>
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20.19-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >= 20.19">
|
|
14
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript strict">
|
|
15
|
+
<img src="https://img.shields.io/badge/built_on-Vite-646CFF?style=flat-square&logo=vite&logoColor=white" alt="Built on Vite">
|
|
16
|
+
<a href="https://github.com/douglasjunior/freemotion/pulls"><img src="https://img.shields.io/badge/PRs-welcome-22C55E?style=flat-square" alt="PRs welcome"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="#-quick-start"><b>Quick Start</b></a> Β·
|
|
21
|
+
<a href="#-why-freemotion">Why</a> Β·
|
|
22
|
+
<a href="#-how-it-works">How It Works</a> Β·
|
|
23
|
+
<a href="#-studio">Studio</a> Β·
|
|
24
|
+
<a href="#-rendering">Rendering</a> Β·
|
|
25
|
+
<a href="#-player">Player</a> Β·
|
|
26
|
+
<a href="#-api">API</a> Β·
|
|
27
|
+
<a href="#-coming-from-remotion">Coming from Remotion</a> Β·
|
|
28
|
+
<a href="#-examples">Examples</a>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src=".github/assets/showcase.gif" alt="A freemotion video: the logo springs in, code is typed in an editor while a preview plays the animation it describes, and a terminal renders the video to MP4." width="800">
|
|
35
|
+
<br>
|
|
36
|
+
<sub>This video is a freemotion composition. <a href="https://github.com/douglasjunior/freemotion-examples/tree/main/src/showcase">See its source</a>.</sub>
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
## π Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx freemotion init my-video
|
|
43
|
+
cd my-video
|
|
44
|
+
npm install
|
|
45
|
+
npm run studio # preview with hot reload
|
|
46
|
+
npm run render # out/HelloWorld.mp4
|
|
47
|
+
npm run page # a page that plays the Story video as you scroll
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The new project has two compositions to start from: `HelloWorld` (springs, `interpolate()`, a `<Sequence>`) and `Story` (steps chained with `<Series>`), plus a small Vite page in `page/` that plays `Story` with [`<ScrollPlayer>`](#scroll-driven-video).
|
|
51
|
+
|
|
52
|
+
You only need **Node.js 20.19+** (or 22.12+). New projects come with [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static), which downloads an ffmpeg binary into the project, and the headless Chromium used for rendering is downloaded automatically on the first render.
|
|
53
|
+
|
|
54
|
+
<details>
|
|
55
|
+
<summary><b>About ffmpeg and its license</b></summary>
|
|
56
|
+
|
|
57
|
+
freemotion encodes videos by running ffmpeg as a separate program. It looks for it in this order: `--ffmpeg <path>`, the `FREEMOTION_FFMPEG` environment variable, the project's `ffmpeg-static` package, and finally `ffmpeg` on your `PATH`. Prefer a system install? Remove `ffmpeg-static` and run `brew install ffmpeg`, `sudo apt install ffmpeg` or `winget install ffmpeg`.
|
|
58
|
+
|
|
59
|
+
The ffmpeg binaries that `ffmpeg-static` downloads are GPL-licensed. They live in your project as a regular dependency: freemotion doesn't ship, bundle or link them, so freemotion itself stays MIT, and the videos you render are yours under any license you like. The GPL only matters if you redistribute the ffmpeg binary itself, for example inside an app or a Docker image you publish.
|
|
60
|
+
|
|
61
|
+
</details>
|
|
62
|
+
|
|
63
|
+
## π‘ Why freemotion?
|
|
64
|
+
|
|
65
|
+
- βοΈ **It's just React.** Components, props, hooks, CSS, SVG, fonts and any npm package. If it renders in a browser, it renders in your video.
|
|
66
|
+
- ποΈ **Frame-perfect by design.** Everything derives from `useCurrentFrame()`. Any frame can be drawn on its own, in any order, so renders are deterministic and run in parallel.
|
|
67
|
+
- β‘ **Fast.** Several Chromium tabs capture frames at once and stream them straight into ffmpeg. The 10-second, 720p video above renders in about 5 seconds on a laptop.
|
|
68
|
+
- π₯ **A live studio.** Scrub the timeline, step frame by frame, switch compositions and see every edit instantly with hot reload.
|
|
69
|
+
- π **Plays on the web, too.** `<Player>` runs any composition inside a React page, and `<ScrollPlayer>` turns the page scroll into the timeline: scroll down to play, up to rewind.
|
|
70
|
+
- π§© **Built on Vite.** Your `vite.config`, aliases, CSS tooling and plugins keep working. `public/` files are served as they are.
|
|
71
|
+
- π **MIT, all of it.** The studio, the renderer and the player are free for any use, at any company size. No tiers, no seats, no render fees.
|
|
72
|
+
- π **A familiar API.** Coming from Remotion? `useCurrentFrame`, `interpolate`, `spring`, `Sequence` and friends have the same names. [See the differences](#-coming-from-remotion).
|
|
73
|
+
|
|
74
|
+
## π§ How It Works
|
|
75
|
+
|
|
76
|
+
A **composition** is a React component plus its size, frame rate and duration. Your project exports a list of them:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// src/compositions.tsx
|
|
80
|
+
import { defineComposition } from 'freemotion';
|
|
81
|
+
|
|
82
|
+
import { Intro } from './Intro';
|
|
83
|
+
|
|
84
|
+
export default [
|
|
85
|
+
defineComposition({
|
|
86
|
+
id: 'Intro',
|
|
87
|
+
component: Intro,
|
|
88
|
+
defaultProps: { title: 'Hello' },
|
|
89
|
+
fps: 30,
|
|
90
|
+
width: 1920,
|
|
91
|
+
height: 1080,
|
|
92
|
+
durationInFrames: 150, // 5 seconds
|
|
93
|
+
}),
|
|
94
|
+
];
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Inside the component, read the current frame and compute everything from it:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// src/Intro.tsx
|
|
101
|
+
import { AbsoluteFill, interpolate, Sequence, spring, useCurrentFrame, useVideoConfig } from 'freemotion';
|
|
102
|
+
|
|
103
|
+
export const Intro = ({ title }: { title: string }) => {
|
|
104
|
+
const frame = useCurrentFrame();
|
|
105
|
+
const { fps } = useVideoConfig();
|
|
106
|
+
|
|
107
|
+
const scale = spring({ frame, fps, config: { damping: 12 } });
|
|
108
|
+
const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<AbsoluteFill style={{ background: '#0b0b12', alignItems: 'center', justifyContent: 'center' }}>
|
|
112
|
+
<h1 style={{ color: 'white', fontSize: 140, opacity, transform: `scale(${scale})` }}>{title}</h1>
|
|
113
|
+
<Sequence from={45}>
|
|
114
|
+
{/* Inside a Sequence, useCurrentFrame() starts at 0 on frame 45. */}
|
|
115
|
+
<Subtitle />
|
|
116
|
+
</Sequence>
|
|
117
|
+
</AbsoluteFill>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
No timers, no `useEffect` animations, no CSS transitions: the frame **is** the time. That's what lets the studio jump to any frame, the renderer draw frames in parallel, and a web page scrub the video with the scroll.
|
|
123
|
+
|
|
124
|
+
Under the hood:
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
|---|---|
|
|
128
|
+
| **`freemotion studio`** | Starts a Vite dev server whose home page is the studio, with your compositions loaded and hot reload on. |
|
|
129
|
+
| **`freemotion render`** | Bundles the project with Vite, opens it in several headless Chromium tabs and, for each frame, draws it, waits for fonts, images and [`delayRender()`](#async-data-delayrender) holds, captures a PNG and pipes it to ffmpeg, in order. |
|
|
130
|
+
| **`<Player>`** | Draws a composition inside your own React app, scaled to fit its container. |
|
|
131
|
+
|
|
132
|
+
## π¬ Studio
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx freemotion studio
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
<p align="center">
|
|
139
|
+
<img src=".github/assets/studio.png" alt="The freemotion studio: a composition picker in the header, the video in the middle and a timeline with play and loop buttons at the bottom." width="800">
|
|
140
|
+
</p>
|
|
141
|
+
|
|
142
|
+
| Key | Action |
|
|
143
|
+
|---|---|
|
|
144
|
+
| <kbd>Space</kbd> | Play / pause |
|
|
145
|
+
| <kbd>β</kbd> <kbd>β</kbd> | Previous / next frame |
|
|
146
|
+
| <kbd>Shift</kbd> + <kbd>β</kbd> <kbd>β</kbd> | One second back / forward |
|
|
147
|
+
| <kbd>Home</kbd> <kbd>End</kbd> | First / last frame |
|
|
148
|
+
| <kbd>L</kbd> | Toggle loop |
|
|
149
|
+
|
|
150
|
+
The URL keeps the composition and the frame (`?composition=Intro&frame=42`), so reloading or sharing a link lands on the same picture. Compositions with a `folder` are grouped in the picker.
|
|
151
|
+
|
|
152
|
+
## π¦ Rendering
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx freemotion render Intro # out/Intro.mp4
|
|
156
|
+
npx freemotion render Intro -o promo.webm # VP9 WebM
|
|
157
|
+
npx freemotion render Intro -o promo.gif # animated GIF
|
|
158
|
+
npx freemotion render Intro -o promo.gif --scale 0.5 --every-nth-frame 2 # half size, 15 fps
|
|
159
|
+
npx freemotion render Intro --frame 60 # out/Intro.png, a still
|
|
160
|
+
npx freemotion render Intro --frames 0-89 # only the first 3 seconds
|
|
161
|
+
npx freemotion render Intro --scale 2 # twice the width and height
|
|
162
|
+
npx freemotion render Intro --props '{"title":"Launch day"}'
|
|
163
|
+
npx freemotion render --all # every composition, to out/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Format | Encoder | Notes |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `.mp4` | H.264, yuv420p, CRF 18 | Tagged BT.709, `+faststart` for the web. Width and height must be even. |
|
|
169
|
+
| `.webm` | VP9, CRF 32 | Needs an ffmpeg built with `libvpx`. |
|
|
170
|
+
| `.gif` | Palette per video | For lighter files, shrink it with `--scale 0.5` and drop frames with `--every-nth-frame 2`. |
|
|
171
|
+
| `.png` | Lossless | A single frame (`--frame`). ffmpeg is not needed. |
|
|
172
|
+
|
|
173
|
+
<details>
|
|
174
|
+
<summary><b>All render options</b></summary>
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
-o, --output <file> Output file, for a single composition (.mp4, .webm, .gif or .png)
|
|
178
|
+
--out-dir <dir> Folder for <id>.<format> files (default: out)
|
|
179
|
+
--all Render every composition
|
|
180
|
+
--format <format> mp4, webm, gif or png (default: from --output, else mp4; png with --frame)
|
|
181
|
+
--frame <n> Render a still image of frame n
|
|
182
|
+
--frames <a-b> Render only frames a to b (inclusive)
|
|
183
|
+
--every-nth-frame <n> Keep one frame out of every n, at 1/n of the frame rate (lighter GIFs)
|
|
184
|
+
--props <json|file> Props merged over defaultProps, as JSON or a path to a .json file
|
|
185
|
+
--scale <n> Pixel density: 2 renders at twice the width and height (default: 1)
|
|
186
|
+
--crf <n> Quality for mp4/webm, lower is better (default: 18 mp4, 32 webm)
|
|
187
|
+
--concurrency <n> Browser tabs rendering in parallel (default: CPU cores - 2, up to 8)
|
|
188
|
+
--entry <file> File that exports the compositions (default: src/compositions.tsx)
|
|
189
|
+
--config <file> Vite config to merge (default: vite.config.* in the project)
|
|
190
|
+
--no-config Ignore the project's Vite config
|
|
191
|
+
--ffmpeg <path> ffmpeg binary (default: $FREEMOTION_FFMPEG, the project's ffmpeg-static, or ffmpeg on PATH)
|
|
192
|
+
--browser-executable <path> Chromium/Chrome to use instead of the bundled headless shell
|
|
193
|
+
--quiet Only print errors
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
</details>
|
|
197
|
+
|
|
198
|
+
### Tips for identical renders everywhere
|
|
199
|
+
|
|
200
|
+
- **Bundle your fonts** with a package such as [`@fontsource`](https://fontsource.org) instead of relying on fonts installed on the machine. The renderer waits for every font face to load.
|
|
201
|
+
- **Derive everything from the frame.** CSS animations, transitions and timers run on the wall clock and won't match between tabs. Use `interpolate()` and `spring()` instead.
|
|
202
|
+
- **Use `random(seed)`** instead of `Math.random()`: the same seed gives the same number in every tab and on every machine.
|
|
203
|
+
- **Wait for async data** with [`delayRender()`](#async-data-delayrender).
|
|
204
|
+
|
|
205
|
+
## π Player
|
|
206
|
+
|
|
207
|
+
Play a composition inside any React app. It fits the width of its container and keeps the aspect ratio.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { Player } from 'freemotion';
|
|
211
|
+
|
|
212
|
+
import { intro } from './compositions';
|
|
213
|
+
|
|
214
|
+
// Plays by itself, in a loop:
|
|
215
|
+
<Player composition={intro} autoPlay />
|
|
216
|
+
|
|
217
|
+
// Or controlled: you choose the frame (from a slider, a timer, the scrollβ¦):
|
|
218
|
+
<Player composition={intro} frame={frame} inputProps={{ title: 'Hi' }} />
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| Prop | Type | Default | Description |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| `composition` | `Composition` | | What to play, from `defineComposition()`. |
|
|
224
|
+
| `inputProps` | `Partial<Props>` | | Merged over the composition's `defaultProps`. |
|
|
225
|
+
| `frame` | `number` | | Controlled mode: the frame to draw. Rounded and clamped. |
|
|
226
|
+
| `autoPlay` | `boolean` | `false` | Uncontrolled mode: start playing on mount. |
|
|
227
|
+
| `loop` | `boolean` | `true` | Uncontrolled mode: start over at the end. |
|
|
228
|
+
| `playbackRate` | `number` | `1` | Uncontrolled mode: speed multiplier. |
|
|
229
|
+
| `onFrameChange` | `(frame) => void` | | Uncontrolled mode: called on every new frame. |
|
|
230
|
+
| `style`, `className` | | | Applied to the container. |
|
|
231
|
+
|
|
232
|
+
### Scroll-driven video
|
|
233
|
+
|
|
234
|
+
`<ScrollPlayer>` renders a tall section with the player stuck to the viewport. While the section scrolls by, the scroll position picks the frame; when it ends, the video leaves with the page. Wheel steps are eased so the motion glides, and `prefers-reduced-motion` turns the easing off.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import { ScrollPlayer } from 'freemotion';
|
|
238
|
+
|
|
239
|
+
<ScrollPlayer composition={intro} scrollLength="500vh" />
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Prop | Type | Default | Description |
|
|
243
|
+
|---|---|---|---|
|
|
244
|
+
| `composition`, `inputProps` | | | As in `<Player>`. |
|
|
245
|
+
| `scrollLength` | CSS height | `'400vh'` | Height of the section: the longer, the slower the video advances. |
|
|
246
|
+
| `smoothing` | `number` (ms) | `120` | How long the shown frame takes to catch up with the scroll. `0` follows it exactly. |
|
|
247
|
+
| `width` | CSS width | fits the viewport | Width of the player. |
|
|
248
|
+
| `onFrameChange` | `(frame) => void` | | Called with the frame shown, e.g. to sync captions outside the video. |
|
|
249
|
+
| `stickyStyle`, `playerStyle`, `className` | | | Styles of the sticky box, the player and the section. |
|
|
250
|
+
| `children` | `ReactNode` | | Rendered in the sticky box, below the player. |
|
|
251
|
+
|
|
252
|
+
For your own layout, `useScrollFrame()` returns the frame and you place the `<Player>`:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
const target = useRef<HTMLDivElement>(null);
|
|
256
|
+
const frame = useScrollFrame(target, intro.durationInFrames, {
|
|
257
|
+
range: 'visible', // 'sticky' (default): while a tall section scrolls by
|
|
258
|
+
// 'visible': from entering the viewport to leaving it
|
|
259
|
+
container: scrollBox, // an element that scrolls, instead of the page
|
|
260
|
+
axis: 'x', // horizontal scrolling
|
|
261
|
+
smoothing: 0,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
<div ref={target}><Player composition={intro} frame={frame} /></div>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The [examples repository](https://github.com/douglasjunior/freemotion-examples/tree/main/web) has a complete page.
|
|
268
|
+
|
|
269
|
+
## π API
|
|
270
|
+
|
|
271
|
+
Everything is exported from `freemotion`.
|
|
272
|
+
|
|
273
|
+
| Export | What it does |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `defineComposition(definition)` | Declares a composition: `id`, `component`, `defaultProps`, `fps`, `width`, `height`, `durationInFrames` and an optional `folder`. |
|
|
276
|
+
| `useCurrentFrame()` | The frame being drawn. Relative to the start of the enclosing `<Sequence>`. |
|
|
277
|
+
| `useVideoConfig()` | `{ fps, width, height, durationInFrames }` of the composition. |
|
|
278
|
+
| `interpolate(input, inputRange, outputRange, options?)` | Maps a value between ranges, segment by segment, with `easing` and `extrapolateLeft`/`extrapolateRight` (`'extend'`, `'clamp'`, `'identity'` or `'wrap'`). |
|
|
279
|
+
| `spring({ frame, fps, config?, from?, to?, delay? })` | A physical spring from `from` to `to`. `config`: `damping` (10), `mass` (1), `stiffness` (100), `overshootClamping` (false). |
|
|
280
|
+
| `Easing` | `linear`, `ease`, `quad`, `cubic`, `poly(n)`, `sin`, `circle`, `exp`, `elastic(b)`, `back(s)`, `bounce`, `bezier(x1, y1, x2, y2)` and the modifiers `in`, `out`, `inOut`. |
|
|
281
|
+
| `<AbsoluteFill>` | A full-size, absolutely positioned flex column. The basic layer. |
|
|
282
|
+
| `<Sequence from durationInFrames layout>` | Shifts time for its children and mounts them only during its window. |
|
|
283
|
+
| `<Series>` / `<Series.Sequence durationInFrames offset>` | Plays sequences one after the other. A negative `offset` overlaps them. |
|
|
284
|
+
| `<Img>` | An `<img>` that holds the render until it has loaded. |
|
|
285
|
+
| `staticFile(path)` | URL of a file in `public/`, respecting Vite's `base`. |
|
|
286
|
+
| `delayRender(label?)` / `continueRender(handle)` / `cancelRender(error)` | Hold the render while something asynchronous loads. |
|
|
287
|
+
| `random(seed)` | A deterministic number in `[0, 1)` for a seed. |
|
|
288
|
+
| `<Player>` | Plays a composition in a web page. [See above](#-player). |
|
|
289
|
+
| `<ScrollPlayer>` / `useScrollFrame(target, durationInFrames, options?)` | Drive a composition with the page scroll or any scrolling element. [See above](#scroll-driven-video). |
|
|
290
|
+
| `scrollProgress(start, size, viewport, range?)` | The scroll math behind them, as a pure function, for custom integrations. |
|
|
291
|
+
| `<Stage config frame>` | Draws one frame at native size. The building block of the player, the studio and the renderer. |
|
|
292
|
+
|
|
293
|
+
### Async data: `delayRender()`
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
const [data, setData] = useState<Data | null>(null);
|
|
297
|
+
const [handle] = useState(() => delayRender('Loading stats'));
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
fetch(staticFile('stats.json'))
|
|
301
|
+
.then(response => response.json())
|
|
302
|
+
.then((json) => {
|
|
303
|
+
setData(json);
|
|
304
|
+
continueRender(handle);
|
|
305
|
+
})
|
|
306
|
+
.catch(cancelRender);
|
|
307
|
+
}, [handle]);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The renderer waits until every hold is released before capturing a frame. A hold that isn't released within 30 seconds fails the render with its label (configure it with `delayRender(label, { timeoutInMilliseconds })`).
|
|
311
|
+
|
|
312
|
+
## π Coming from Remotion?
|
|
313
|
+
|
|
314
|
+
freemotion keeps the names you already know, so most components move over by changing the import:
|
|
315
|
+
|
|
316
|
+
```diff
|
|
317
|
+
- import { AbsoluteFill, interpolate, spring, useCurrentFrame } from 'remotion';
|
|
318
|
+
+ import { AbsoluteFill, interpolate, spring, useCurrentFrame } from 'freemotion';
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Same names: `useCurrentFrame`, `useVideoConfig`, `interpolate`, `spring`, `Easing`, `AbsoluteFill`, `Sequence`, `Series`, `Img`, `staticFile`, `delayRender`, `continueRender`, `cancelRender` and `random`.
|
|
322
|
+
|
|
323
|
+
What's different:
|
|
324
|
+
|
|
325
|
+
- **Registering compositions.** Instead of `<Composition>` elements under `registerRoot()`, `src/compositions.tsx` default-exports an array built with `defineComposition()`.
|
|
326
|
+
- **The `<Player>`** takes the composition object (`composition={intro}`) and is controlled through its `frame` prop.
|
|
327
|
+
- **Not there yet:** audio, `<Video>`/`<OffthreadVideo>`, transparent video, `calculateMetadata()` and cloud rendering.
|
|
328
|
+
|
|
329
|
+
freemotion is an independent project written from scratch, and it is not affiliated with Remotion.
|
|
330
|
+
|
|
331
|
+
## π§ͺ Examples
|
|
332
|
+
|
|
333
|
+
**[douglasjunior/freemotion-examples](https://github.com/douglasjunior/freemotion-examples)** has the video at the top of this page, the banner, a bar chart that loads its data with `delayRender()`, and a page where the video plays as you scroll.
|
|
334
|
+
|
|
335
|
+
## πΊοΈ Roadmap
|
|
336
|
+
|
|
337
|
+
- [ ] Audio tracks and `<Audio>`
|
|
338
|
+
- [ ] `<Video>` with frame-accurate seeking
|
|
339
|
+
- [ ] Transparent output (ProRes 4444, VP9 with alpha)
|
|
340
|
+
- [ ] A render button and a props editor in the studio
|
|
341
|
+
- [ ] More `freemotion init` templates
|
|
342
|
+
|
|
343
|
+
Ideas and pull requests are welcome: [open an issue](https://github.com/douglasjunior/freemotion/issues).
|
|
344
|
+
|
|
345
|
+
## π€ Contributing
|
|
346
|
+
|
|
347
|
+
1. Read [`AGENTS.md`](AGENTS.md) for the architecture and conventions.
|
|
348
|
+
2. Fork the repository and create a topic branch.
|
|
349
|
+
3. Make sure `npm run ts`, `npm run lint`, `npm test` and `npm run test:e2e` pass. The e2e suite renders real videos, so it needs ffmpeg on your `PATH`.
|
|
350
|
+
4. Write commit messages following [Conventional Commits](https://www.conventionalcommits.org/) in English and open a pull request.
|
|
351
|
+
|
|
352
|
+
## β Star History
|
|
353
|
+
|
|
354
|
+
If freemotion saved you a render bill, consider giving it a star. It helps other people find the project.
|
|
355
|
+
|
|
356
|
+
<a href="https://star-history.com/#douglasjunior/freemotion&Date">
|
|
357
|
+
<picture>
|
|
358
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=douglasjunior/freemotion&type=Date&theme=dark">
|
|
359
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=douglasjunior/freemotion&type=Date" width="600">
|
|
360
|
+
</picture>
|
|
361
|
+
</a>
|
|
362
|
+
|
|
363
|
+
## π License
|
|
364
|
+
|
|
365
|
+
[MIT](LICENSE) Β© Douglas Nassif Roma Junior
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { AbsoluteFill, type AbsoluteFillProps } from './runtime/absolute-fill.js';
|
|
2
|
+
export { defineComposition, type AnyComposition, type Composition, type CompositionDefinition, } from './runtime/composition.js';
|
|
3
|
+
export { cancelRender, continueRender, delayRender, type DelayRenderOptions } from './runtime/delay-render.js';
|
|
4
|
+
export { Easing, type EasingFunction } from './runtime/easing.js';
|
|
5
|
+
export { useCurrentFrame, useVideoConfig, type VideoConfig } from './runtime/frame.js';
|
|
6
|
+
export { Img, type ImgProps } from './runtime/img.js';
|
|
7
|
+
export { interpolate, type ExtrapolateType, type InterpolateOptions } from './runtime/interpolate.js';
|
|
8
|
+
export { Player, type PlayerProps } from './runtime/player.js';
|
|
9
|
+
export { random } from './runtime/random.js';
|
|
10
|
+
export { ScrollPlayer, scrollProgress, useScrollFrame, type ScrollFrameOptions, type ScrollPlayerProps, type ScrollRange, } from './runtime/scroll.js';
|
|
11
|
+
export { Sequence, Series, type SequenceProps, type SeriesSequenceProps } from './runtime/sequence.js';
|
|
12
|
+
export { spring, type SpringConfig, type SpringOptions } from './runtime/spring.js';
|
|
13
|
+
export { Stage, type StageProps } from './runtime/stage.js';
|
|
14
|
+
export { staticFile } from './runtime/static-file.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { AbsoluteFill } from './runtime/absolute-fill.js';
|
|
2
|
+
export { defineComposition, } from './runtime/composition.js';
|
|
3
|
+
export { cancelRender, continueRender, delayRender } from './runtime/delay-render.js';
|
|
4
|
+
export { Easing } from './runtime/easing.js';
|
|
5
|
+
export { useCurrentFrame, useVideoConfig } from './runtime/frame.js';
|
|
6
|
+
export { Img } from './runtime/img.js';
|
|
7
|
+
export { interpolate } from './runtime/interpolate.js';
|
|
8
|
+
export { Player } from './runtime/player.js';
|
|
9
|
+
export { random } from './runtime/random.js';
|
|
10
|
+
export { ScrollPlayer, scrollProgress, useScrollFrame, } from './runtime/scroll.js';
|
|
11
|
+
export { Sequence, Series } from './runtime/sequence.js';
|
|
12
|
+
export { spring } from './runtime/spring.js';
|
|
13
|
+
export { Stage } from './runtime/stage.js';
|
|
14
|
+
export { staticFile } from './runtime/static-file.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RenderOptions } from './render.js';
|
|
2
|
+
import type { StudioOptions } from './studio.js';
|
|
3
|
+
export declare const HELP: {
|
|
4
|
+
main: string;
|
|
5
|
+
init: string;
|
|
6
|
+
studio: string;
|
|
7
|
+
render: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function defaultConcurrency(): number;
|
|
10
|
+
export type Command = {
|
|
11
|
+
readonly name: 'help';
|
|
12
|
+
readonly topic: keyof typeof HELP;
|
|
13
|
+
} | {
|
|
14
|
+
readonly name: 'version';
|
|
15
|
+
} | {
|
|
16
|
+
readonly name: 'init';
|
|
17
|
+
readonly directory?: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly name: 'studio';
|
|
20
|
+
readonly options: Omit<StudioOptions, 'log'>;
|
|
21
|
+
} | {
|
|
22
|
+
readonly name: 'render';
|
|
23
|
+
readonly quiet: boolean;
|
|
24
|
+
readonly options: Omit<RenderOptions, 'log'>;
|
|
25
|
+
};
|
|
26
|
+
/** `"30-89"` β `[30, 89]`; `"30"` β `[30, 30]`. */
|
|
27
|
+
export declare function parseFrames(value: string): [number, number];
|
|
28
|
+
/** Inline JSON (`'{"title":"Hi"}'`) or the path of a JSON file. */
|
|
29
|
+
export declare function parseProps(value: string, cwd: string): Record<string, unknown>;
|
|
30
|
+
export declare function parseCommand(argv: readonly string[], { cwd, env }: {
|
|
31
|
+
readonly cwd: string;
|
|
32
|
+
readonly env: NodeJS.ProcessEnv;
|
|
33
|
+
}): Command;
|