rustmotion 0.3.0

A CLI tool that renders motion design videos from JSON scenarios. No browser, no Node.js — just a single Rust binary.
rustmotion-0.3.0 is not a library.

rustmotion

A CLI tool that renders motion design videos from JSON scenarios. No browser, no Node.js — just a single Rust binary.

Crates.io docs.rs License: MIT

Install

cargo install rustmotion

Requirements: Rust toolchain + C++ compiler (for openh264). Optional: ffmpeg CLI for H.265/VP9/ProRes/WebM/GIF output.

Quick Start

# Render a video
rustmotion render scenario.json -o video.mp4

# Render with a specific codec
rustmotion render scenario.json -o video.webm --codec vp9 --crf 30

# Export as PNG sequence
rustmotion render scenario.json -o frames/ --format png-seq

# Export as animated GIF
rustmotion render scenario.json -o output.gif --format gif

# Render a single frame for preview
rustmotion render scenario.json --frame 42 -o frame.png

# Validate without rendering
rustmotion validate scenario.json

# Export JSON Schema (for editor autocompletion or LLM prompts)
rustmotion schema -o schema.json

# Show scenario info
rustmotion info scenario.json

CLI Reference

rustmotion render

Flag Description Default
input Path to the JSON scenario file (required)
-o, --output Output file path output.mp4
--frame <N> Render a single frame to PNG (0-indexed)
--codec <CODEC> Video codec: h264, h265, vp9, prores h264
--crf <0-51> Constant Rate Factor (lower = better quality) 23
--format <FMT> Output format: mp4, webm, mov, gif, png-seq auto from extension
--transparent Transparent background (PNG sequence, WebM, ProRes 4444) false
--output-format json Machine-readable JSON output for CI pipelines
-q, --quiet Suppress all output except errors

JSON Scenario Format

{
  "version": "1.0",
  "video": { ... },
  "audio": [ ... ],
  "scenes": [ ... ]
}

Video Config

{
  "video": {
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "background": "#0f172a",
    "codec": "h264",
    "crf": 23
  }
}
Field Type Default Description
width u32 (required) Video width in pixels (must be even)
height u32 (required) Video height in pixels (must be even)
fps u32 30 Frames per second
background string "#000000" Default background color (hex)
codec string "h264" Video codec: h264, h265, vp9, prores
crf u8 23 Constant Rate Factor (0-51, lower = better quality)

Audio Tracks

{
  "audio": [
    {
      "src": "music.mp3",
      "start": 0.0,
      "end": 10.0,
      "volume": 0.8,
      "fade_in": 1.0,
      "fade_out": 2.0
    }
  ]
}
Field Type Default Description
src string (required) Path to audio file (MP3, WAV, OGG, FLAC, AAC)
start f64 0.0 Start time in the output video (seconds)
end f64 End time (omit for full track)
volume f32 1.0 Volume multiplier (0.0 - 1.0)
fade_in f64 Fade-in duration (seconds)
fade_out f64 Fade-out duration (seconds)

Scenes

Each scene has a duration, optional background, layers rendered in order, and an optional transition to the next scene. Scene entries can also be include directives that inject scenes from external files (see Include below).

{
  "scenes": [
    {
      "duration": 3.0,
      "background": "#1a1a2e",
      "freeze_at": 2.5,
      "layers": [ ... ],
      "transition": {
        "type": "fade",
        "duration": 0.5
      }
    }
  ]
}
Field Type Default Description
duration f64 (required) Scene duration in seconds
background string Scene background (overrides video.background)
freeze_at f64 Freeze the scene at this time (seconds). All frames after this point render the frozen state
layers Layer[] [] Layers rendered bottom-to-top
transition Transition Transition effect to the next scene

Transitions

Transitions blend between two consecutive scenes. Set on the second scene.

{
  "transition": {
    "type": "clock_wipe",
    "duration": 0.8
  }
}
Type Description
fade Linear crossfade between scenes
wipe_left Horizontal wipe revealing scene B from the left
wipe_right Horizontal wipe revealing scene B from the right
wipe_up Vertical wipe revealing scene B from the top
wipe_down Vertical wipe revealing scene B from the bottom
zoom_in Scene A zooms in and fades out, revealing scene B
zoom_out Scene B zooms out from larger to normal size
flip 3D Y-axis flip simulation (scene A folds away, scene B unfolds)
clock_wipe Circular clockwise sweep from 12 o'clock
iris Expanding circle from the center reveals scene B
slide Scene B pushes scene A to the left
dissolve Per-pixel noise dissolve (each pixel switches independently)
none Hard cut at the midpoint
Field Type Default Description
type string (required) One of the transition types above
duration f64 0.5 Transition duration in seconds

Include (Composable Scenarios)

Scene entries can reference external scenario files to inject their scenes inline. This enables reusable intros, outros, and shared sequences.

{
  "scenes": [
    { "include": "shared/intro.json" },
    {
      "duration": 5.0,
      "layers": [
        { "type": "text", "content": "Main content", "position": { "x": 540, "y": 960 } }
      ]
    },
    { "include": "shared/outro.json" }
  ]
}
Field Type Default Description
include string (required) Path (relative to parent file) or URL (http:///https://) to a scenario JSON
scenes usize[] Only include scenes at these 0-based indices (e.g. [0, 2]). Omit to include all
  • The included file's video config is ignored — the parent's config is used
  • Audio tracks from included files are merged into the parent
  • Includes can be nested recursively (max depth: 8)
  • Local paths are resolved relative to the parent scenario file
  • Remote URLs (http:///https://) are fetched at load time

Layers

All layers share a common set of fields for animation, timing, and effects, plus type-specific fields. Layers are rendered in array order (first = bottom, last = top).

Common Fields

These fields are available on all layer types (except group and caption where noted):

Field Type Default Description
position {x, y} {x: 0, y: 0} Position in pixels
opacity f32 1.0 Layer opacity (0.0 - 1.0)
animations Animation[] [] Custom keyframe animations
preset string Preset animation name (see Animation Presets)
preset_config PresetConfig Preset timing configuration
start_at f64 Layer appears at this time (seconds within scene)
end_at f64 Layer disappears after this time (seconds within scene)
wiggle WiggleConfig[] Procedural noise-based animation (see Wiggle)
motion_blur f32 Motion blur intensity (0.0 - 1.0). Uses temporal multi-sampling
padding f32 | {top, right, bottom, left} null Inner spacing — enlarges the bounding box and insets the content
margin f32 | {top, right, bottom, left} null Outer spacing — offsets the layer and affects card layout

Padding and margin accept either a uniform number ("padding": 16) or per-side values ("margin": {"top": 32, "right": 16, "bottom": 32, "left": 16}). In card layouts, margin adds space around the child; padding insets the content within the child. For standalone layers, margin offsets the rendered position, and padding offsets the content.

Note: CardLayer and CodeblockLayer manage their own internal padding. The global padding field does not apply to them — only margin is used.


Text Layer

{
  "type": "text",
  "content": "Hello World",
  "position": { "x": 540, "y": 960 },
  "font_size": 64,
  "color": "#FFFFFF",
  "font_family": "Inter",
  "font_weight": "bold",
  "align": "center",
  "max_width": 800,
  "line_height": 80,
  "letter_spacing": 2.0,
  "preset": "fade_in_up"
}
Field Type Default Description
content string (required) Text to display. Supports \n for line breaks
font_size f32 48.0 Font size in pixels
color string "#FFFFFF" Text color (hex). Can be animated via "color" property
font_family string "Inter" Font family name (uses system fonts)
font_weight string "normal" "normal" or "bold"
align string "left" "left", "center", or "right"
max_width f32 Maximum text width before word-wrapping
line_height f32 font_size * 1.3 Line height in pixels
letter_spacing f32 0.0 Additional spacing between characters

Shape Layer

{
  "type": "shape",
  "shape": "rounded_rect",
  "position": { "x": 100, "y": 200 },
  "size": { "width": 300, "height": 200 },
  "corner_radius": 16,
  "fill": "#3b82f6",
  "stroke": { "color": "#ffffff", "width": 2 },
  "preset": "scale_in"
}
Field Type Default Description
shape ShapeType (required) Shape type (see below)
size {width, height} {width: 100, height: 100} Shape dimensions
fill string | Gradient Fill color (hex string) or gradient object
stroke Stroke Stroke outline
corner_radius f32 8.0 Corner radius (for rounded_rect)

Shape Types

Type Description Extra Fields
"rect" Rectangle
"circle" Circle (fits inside size rect)
"rounded_rect" Rectangle with rounded corners corner_radius
"ellipse" Ellipse (fits inside size rect)
"triangle" Equilateral triangle pointing up
"star" Star polygon points (default: 5)
"polygon" Regular polygon sides (default: 6)
"path" SVG path data (SVG path string, required)

Star and polygon examples:

{ "shape": { "star": { "points": 6 } } }
{ "shape": { "polygon": { "sides": 8 } } }
{ "shape": { "path": { "data": "M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" } } }

Fill

Fill accepts either a solid hex color string or a gradient object:

"fill": "#ff6b6b"
"fill": {
  "type": "linear",
  "colors": ["#667eea", "#764ba2"],
  "angle": 135,
  "stops": [0.0, 1.0]
}
Gradient Field Type Default Description
type string (required) "linear" or "radial"
colors string[] (required) Array of hex colors
stops f32[] Color stop positions (0.0 - 1.0)
angle f32 0.0 Angle in degrees (linear gradients only)

Stroke

Field Type Default Description
color string (required) Stroke color (hex)
width f32 2.0 Stroke width in pixels

Image Layer

{
  "type": "image",
  "src": "photo.png",
  "position": { "x": 0, "y": 0 },
  "size": { "width": 1080, "height": 1080 },
  "fit": "cover",
  "preset": "fade_in"
}
Field Type Default Description
src string (required) Path to image file (PNG, JPEG, WebP)
size {width, height} Target size (uses native image size if omitted)
fit string "contain" "cover", "contain", or "fill"

SVG Layer

{
  "type": "svg",
  "src": "icon.svg",
  "position": { "x": 100, "y": 100 },
  "size": { "width": 200, "height": 200 }
}

Or with inline SVG:

{
  "type": "svg",
  "data": "<svg viewBox='0 0 100 100'><circle cx='50' cy='50' r='40' fill='red'/></svg>",
  "position": { "x": 100, "y": 100 }
}
Field Type Default Description
src string Path to .svg file
data string Inline SVG markup
size {width, height} Target size (uses SVG intrinsic size if omitted)

One of src or data is required.


Icon Layer

Renders an icon from the Iconify library. The icon SVG is fetched automatically from the Iconify API.

{
  "type": "icon",
  "icon": "lucide:home",
  "color": "#38bdf8",
  "position": { "x": 540, "y": 400 },
  "size": { "width": 64, "height": 64 }
}
Field Type Default Description
icon string (required) Iconify identifier "prefix:name" (e.g. "lucide:home", "mdi:account")
color string "#FFFFFF" Icon color (replaces currentColor)
size {width, height} 24x24 Icon size in pixels

Browse available icons at icon-sets.iconify.design.


Video Layer

Embeds a video clip as a layer. Requires ffmpeg on PATH.

{
  "type": "video",
  "src": "clip.mp4",
  "position": { "x": 0, "y": 0 },
  "size": { "width": 1080, "height": 1920 },
  "trim_start": 2.0,
  "trim_end": 8.0,
  "playback_rate": 0.5,
  "fit": "cover",
  "volume": 0.0
}
Field Type Default Description
src string (required) Path to video file
size {width, height} (required) Display size
trim_start f64 0.0 Start offset in the source clip (seconds)
trim_end f64 End offset in the source clip (seconds)
playback_rate f64 1.0 Playback speed (0.5 = half speed, 2.0 = double)
fit string "contain" "cover", "contain", or "fill"
volume f32 1.0 Audio volume (0.0 = mute)
loop_video bool Loop the clip

GIF Layer

Displays an animated GIF, synced to the scene timeline.

{
  "type": "gif",
  "src": "animation.gif",
  "position": { "x": 100, "y": 100 },
  "size": { "width": 300, "height": 300 },
  "loop_gif": true,
  "fit": "contain"
}
Field Type Default Description
src string (required) Path to .gif file
size {width, height} Display size (uses GIF native size if omitted)
fit string "contain" "cover", "contain", or "fill"
loop_gif bool true Loop the GIF animation

Caption Layer

TikTok-style word-by-word subtitles with active word highlighting.

{
  "type": "caption",
  "words": [
    { "text": "Hello", "start": 0.0, "end": 0.5 },
    { "text": "world!", "start": 0.5, "end": 1.0 }
  ],
  "position": { "x": 540, "y": 1600 },
  "font_size": 56,
  "color": "#FFFFFF",
  "active_color": "#FFFF00",
  "background": "#00000088",
  "style": "highlight",
  "max_width": 900
}
Field Type Default Description
words CaptionWord[] (required) Array of timed words
font_size f32 48.0 Font size
font_family string "Inter" Font family
color string "#FFFFFF" Default (inactive) word color
active_color string "#FFFF00" Active word color
background string Background pill color (hex with alpha, e.g. "#00000088")
style string "highlight" Caption style (see below)
max_width f32 Maximum width before word-wrapping

CaptionWord

Field Type Description
text string The word text
start f64 Start timestamp (seconds, within scene)
end f64 End timestamp (seconds, within scene)

Caption Styles

Style Description
"highlight" All words visible, active word changes color
"karaoke" All words visible, active word highlighted (same rendering as highlight)
"word_by_word" Only the active word is shown at a time

Codeblock Layer

Renders code with syntax highlighting (powered by Syntect), carbon.now.sh-style chrome (title bar with dots), reveal animations, and animated diff transitions between code states.

{
  "type": "codeblock",
  "code": "fn main() {\n    println!(\"Hello\");\n}",
  "language": "rust",
  "theme": "base16-ocean.dark",
  "position": { "x": 200, "y": 150 },
  "font_size": 18,
  "show_line_numbers": true,
  "chrome": { "enabled": true, "title": "main.rs" },
  "reveal": { "mode": "typewriter", "start": 0, "duration": 2.5 },
  "highlights": [{ "lines": [2], "color": "#FFFF0022", "start": 3.0, "end": 4.5 }],
  "states": [
    {
      "code": "fn main() {\n    println!(\"Hello, world!\");\n}",
      "at": 5.0,
      "duration": 2.0,
      "easing": "ease_in_out",
      "cursor": { "enabled": true, "color": "#E0E0E0", "blink": true }
    }
  ]
}
Field Type Default Description
code string (required) Initial code content
language string "plain" Language for syntax highlighting (e.g. "rust", "javascript", "python")
theme string "base16-ocean.dark" Theme name (see available themes below)
font_family string "JetBrains Mono" Monospace font family
font_size f32 16.0 Font size in pixels
font_weight u16 400 Font weight (100=Thin, 300=Light, 400=Normal, 500=Medium, 600=SemiBold, 700=Bold, 900=Black)
line_height f32 1.5 Line height multiplier
background string Background color (uses theme default if omitted)
show_line_numbers bool false Show line numbers in the gutter
corner_radius f32 12.0 Background corner radius
padding CodeblockPadding Padding around the code area

Chrome (Title Bar)

Carbon.now.sh-style title bar with colored dots.

Field Type Default Description
chrome.enabled bool true Show the title bar
chrome.title string Title text (e.g. filename)
chrome.color string "#343d46" Title bar background color

Line Highlights

Highlight specific lines with a colored background, optionally timed.

Field Type Default Description
highlights[].lines u32[] (required) Line numbers to highlight (1-indexed)
highlights[].color string "#FFFF0033" Highlight color (hex with alpha)
highlights[].start f64 Start time (always visible if omitted)
highlights[].end f64 End time

Reveal Animation

Animate the initial code appearance.

Field Type Default Description
reveal.mode string (required) "typewriter" (char by char) or "line_by_line"
reveal.start f64 0.0 Start time (seconds)
reveal.duration f64 1.0 Duration (seconds)
reveal.easing string "linear" Easing function

Code States (Diff Transitions)

Animate between code versions with automatic diff detection. Unchanged lines slide smoothly, deleted lines fade out, inserted lines fade in, and modified lines show a cursor editing effect.

Field Type Default Description
states[].code string (required) New code content
states[].at f64 (required) Transition start time (seconds)
states[].duration f64 0.6 Transition duration
states[].easing string "ease_in_out" Easing function
states[].cursor.enabled bool true Show editing cursor on modified lines
states[].cursor.color string "#FFFFFF" Cursor color
states[].cursor.width f32 2.0 Cursor width in pixels
states[].cursor.blink bool true Blink the cursor (~530ms)
states[].highlights CodeblockHighlight[] Override highlights for this state

Available Themes (72)

Syntect built-in: base16-ocean.dark, base16-ocean.light, base16-eighties.dark, base16-mocha.dark, InspiredGitHub, Solarized (dark), Solarized (light)

Catppuccin: catppuccin-latte, catppuccin-frappe, catppuccin-macchiato, catppuccin-mocha

Shiki / VS Code: andromeeda, aurora-x, ayu-dark, ayu-light, ayu-mirage, dark-plus, dracula, dracula-soft, everforest-dark, everforest-light, github-dark, github-dark-default, github-dark-dimmed, github-dark-high-contrast, github-light, github-light-default, github-light-high-contrast, gruvbox-dark-hard, gruvbox-dark-medium, gruvbox-dark-soft, gruvbox-light-hard, gruvbox-light-medium, gruvbox-light-soft, horizon, horizon-bright, houston, kanagawa-dragon, kanagawa-lotus, kanagawa-wave, laserwave, light-plus, material-theme, material-theme-darker, material-theme-lighter, material-theme-ocean, material-theme-palenight, min-dark, min-light, monokai, night-owl, night-owl-light, nord, one-dark-pro, one-light, plastic, poimandres, red, rose-pine, rose-pine-dawn, rose-pine-moon, slack-dark, slack-ochin, snazzy-light, solarized-dark, solarized-light, synthwave-84, tokyo-night, vesper, vitesse-black, vitesse-dark, vitesse-light


Counter Layer

Animated number counter that interpolates from a start value to an end value over the layer's visible duration. Uses text rendering internally.

{
  "type": "counter",
  "from": 0,
  "to": 1250,
  "decimals": 0,
  "separator": " ",
  "prefix": "+",
  "suffix": "",
  "easing": "ease_out",
  "position": { "x": 540, "y": 960 },
  "font_size": 72,
  "color": "#FFFFFF",
  "font_family": "Inter",
  "font_weight": "bold",
  "align": "center",
  "start_at": 0.5,
  "end_at": 2.5,
  "preset": "fade_in_up"
}
Field Type Default Description
from f64 (required) Start value
to f64 (required) End value
decimals u8 0 Number of decimal places
separator string Thousands separator (e.g. " ", ",")
prefix string Text before the number (e.g. "$", "+")
suffix string Text after the number (e.g. "%", "€")
easing string "linear" Easing for the counter interpolation (see Easing Functions)
font_size f32 48.0 Font size in pixels
color string "#FFFFFF" Text color (hex)
font_family string "Inter" Font family name
font_weight string "normal" "normal" or "bold"
align string "left" "left", "center", or "right"
letter_spacing f32 Additional spacing between characters
shadow TextShadow Drop shadow behind text
stroke Stroke Text outline/stroke

The counter animates over the layer's visible duration (start_at to end_at, or the full scene duration if omitted). The easing field controls the interpolation curve of the number, independently from any visual animation presets applied to the layer.


Flex Layer (Layout Container)

A pure layout container that automatically positions its children using flex or grid layout. It has no visual styling (no background, border, or shadow by default) — use it to stack or arrange layers without adding visual chrome.

flex is an alias for card — they share the same properties. Use flex when you only need layout, and card when you need visual styling.

{
  "type": "flex",
  "position": { "x": 100, "y": 200 },
  "size": { "width": 750, "height": "auto" },
  "direction": "column",
  "gap": 20,
  "layers": [
    { "type": "card", "size": { "width": 750, "height": "auto" }, "background": "#1a1a2e", ... },
    { "type": "card", "size": { "width": 750, "height": "auto" }, "background": "#1a1a2e", ... },
    { "type": "card", "size": { "width": 750, "height": "auto" }, "background": "#1a1a2e", ... }
  ]
}

See Card Layer for all available properties (direction, align, justify, gap, wrap, padding, grid properties, etc.).


Card Layer (Flex & Grid Container)

A visual container with CSS-like layout (flex by default, grid optional) for its children. Unlike group (which uses absolute positioning with no visual style), card automatically positions children and supports background, border, shadow, corner radius, and padding.

Flex Layout Example

{
  "type": "card",
  "direction": "row",
  "size": { "width": 800, "height": 100 },
  "gap": 16,
  "layers": [
    { "type": "shape", "shape": "rect", "size": { "width": 100, "height": 100 }, "fill": "#FF0000" },
    { "type": "shape", "shape": "rect", "size": { "width": 100, "height": 100 }, "fill": "#00FF00", "flex_grow": 1 },
    { "type": "shape", "shape": "rect", "size": { "width": 100, "height": 100 }, "fill": "#0000FF" }
  ]
}

Grid Layout Example

{
  "type": "card",
  "display": "grid",
  "size": { "width": 600, "height": 400 },
  "grid_template_columns": [{ "fr": 1 }, { "fr": 1 }],
  "grid_template_rows": [{ "fr": 1 }, { "fr": 1 }],
  "gap": 16,
  "padding": 24,
  "background": "#1a1a2e",
  "corner_radius": 16,
  "layers": [
    { "type": "text", "content": "Cell 1", "color": "#FFFFFF" },
    { "type": "text", "content": "Cell 2", "color": "#FFFFFF" },
    { "type": "text", "content": "Cell 3", "color": "#FFFFFF" },
    { "type": "text", "content": "Cell 4", "color": "#FFFFFF" }
  ]
}
Field Type Default Description
display string "flex" Layout mode: "flex" or "grid"
size {width, height} Container size. Each dimension can be a number or "auto". Auto-calculated from children if omitted
background string Background color (hex)
corner_radius f32 12.0 Corner radius for rounded corners
border CardBorder Border stroke
shadow CardShadow Drop shadow
padding f32 | {top, right, bottom, left} Padding inside the card (uniform number or per-side)
direction string "column" "column", "row", "column_reverse", "row_reverse"
wrap bool false Wrap children to next line when they exceed available space
align string "start" Cross-axis alignment: "start", "center", "end", "stretch"
justify string "start" Main-axis justification: "start", "center", "end", "space_between", "space_around", "space_evenly"
gap f32 0.0 Spacing between children (pixels)
grid_template_columns GridTrack[] Grid column definitions (required for grid display)
grid_template_rows GridTrack[] Grid row definitions (defaults to [{"fr": 1}])
layers CardChild[] [] Child layers (positioned automatically; their position field is ignored)

Per-child Layout Properties

These properties are set directly on child layers within layers:

Field Type Default Description
flex_grow f32 0 How much the child grows to fill remaining space (flex only)
flex_shrink f32 1 How much the child shrinks when space is insufficient (flex only)
flex_basis f32 Base size before grow/shrink (defaults to natural size)
align_self string Per-child cross-axis alignment override: "start", "center", "end", "stretch"
grid_column GridPlacement Grid column placement (grid only)
grid_row GridPlacement Grid row placement (grid only)

GridTrack

Format Description
{"px": N} Fixed size in pixels
{"fr": N} Fractional unit (distributes remaining space)
"auto" Sized to fit content

GridPlacement

Field Type Default Description
start i32 1-indexed grid line (1 = first column/row)
span u32 1 Number of tracks to span

CardBorder

Field Type Default Description
color string (required) Border color (hex)
width f32 1.0 Border width in pixels

CardShadow

Field Type Default Description
color string (required) Shadow color (hex with alpha, e.g. "#00000040")
offset_x f32 0.0 Horizontal shadow offset
offset_y f32 0.0 Vertical shadow offset
blur f32 0.0 Shadow blur radius

Group Layer

Groups nested layers with a shared position and opacity.

{
  "type": "group",
  "position": { "x": 100, "y": 100 },
  "opacity": 0.8,
  "layers": [
    { "type": "shape", "shape": "rect", "size": { "width": 400, "height": 300 }, "fill": "#1a1a2e" },
    { "type": "text", "content": "Inside group", "position": { "x": 50, "y": 150 } }
  ]
}
Field Type Default Description
layers Layer[] [] Nested layers (positions relative to group)
position {x, y} {x: 0, y: 0} Group offset
opacity f32 1.0 Group opacity (applied to all children)

Animations

Custom Keyframe Animations

Animate any property over time with keyframes:

{
  "animations": [
    {
      "property": "opacity",
      "keyframes": [
        { "time": 0.0, "value": 0.0 },
        { "time": 0.5, "value": 1.0 }
      ],
      "easing": "ease_out"
    },
    {
      "property": "color",
      "keyframes": [
        { "time": 0.0, "value": "#FF0000" },
        { "time": 1.0, "value": "#0000FF" }
      ],
      "easing": "linear"
    }
  ]
}

Animatable Properties

Property Type Description
opacity number Layer opacity (0.0 - 1.0)
position.x number Horizontal offset in pixels
position.y number Vertical offset in pixels
scale number Uniform scale (1.0 = 100%)
scale.x number Horizontal scale
scale.y number Vertical scale
rotation number Rotation in degrees
blur number Gaussian blur radius
visible_chars number Number of visible characters (for text)
visible_chars_progress number Character reveal progress 0.0 - 1.0 (for text)
color color Color interpolation (hex strings, e.g. "#FF0000")

Easing Functions

Easing Description
linear Constant speed
ease_in Cubic ease in (slow start)
ease_out Cubic ease out (slow end) — default
ease_in_out Cubic ease in and out
ease_in_quad Quadratic ease in
ease_out_quad Quadratic ease out
ease_in_cubic Cubic ease in
ease_out_cubic Cubic ease out
ease_in_expo Exponential ease in
ease_out_expo Exponential ease out
spring Spring physics (uses spring config)

Spring Config

When using "easing": "spring", provide a spring object:

{
  "easing": "spring",
  "spring": {
    "damping": 12.0,
    "stiffness": 100.0,
    "mass": 1.0
  }
}
Field Type Default Description
damping f64 15.0 Damping coefficient (higher = less oscillation)
stiffness f64 100.0 Spring stiffness (higher = faster)
mass f64 1.0 Mass (higher = slower, more inertia)

Animation Presets

Presets are ready-to-use animations. Set preset on any layer:

{
  "type": "text",
  "content": "Animated!",
  "preset": "bounce_in",
  "preset_config": {
    "delay": 0.2,
    "duration": 0.8,
    "loop": false
  }
}

Preset Config

Field Type Default Description
delay f64 0.0 Delay before animation starts (seconds)
duration f64 0.8 Animation duration (seconds)
loop bool false Loop the animation continuously

Entrance Presets

Preset Description
fade_in Fade from transparent
fade_in_up Fade in + slide up
fade_in_down Fade in + slide down
fade_in_left Fade in + slide from left
fade_in_right Fade in + slide from right
slide_in_left Slide in from far left
slide_in_right Slide in from far right
slide_in_up Slide in from below
slide_in_down Slide in from above
scale_in Scale up from 0 with spring bounce
bounce_in Bouncy scale from small to normal
blur_in Fade in from blurred
rotate_in Rotate + scale from half size
elastic_in Elastic underdamped spring scale

Exit Presets

Preset Description
fade_out Fade to transparent
fade_out_up Fade out + slide up
fade_out_down Fade out + slide down
slide_out_left Slide out to the left
slide_out_right Slide out to the right
slide_out_up Slide out upward
slide_out_down Slide out downward
scale_out Scale down to 0
bounce_out Bouncy scale to small
blur_out Fade out with blur
rotate_out Rotate + scale to half size

Continuous Presets

These presets loop automatically when "loop": true is set in preset_config:

Preset Description
pulse Gentle scale oscillation (1.0 - 1.05)
float Vertical floating motion
shake Horizontal shake
spin 360-degree continuous rotation

Special Presets

Preset Description
typewriter Progressive character reveal (left to right)
wipe_left Slide in from left with fade
wipe_right Slide in from right with fade

Wiggle

Wiggle adds procedural noise-based motion to any animatable property. Unlike keyframe animations, wiggle produces continuous organic movement.

{
  "type": "text",
  "content": "Wobbly",
  "wiggle": [
    { "property": "position.x", "amplitude": 5.0, "frequency": 3.0, "seed": 42 },
    { "property": "rotation", "amplitude": 2.0, "frequency": 2.0, "seed": 99, "decay": 0.5 }
  ]
}
Field Type Default Description
property string (required) Property to wiggle (same as animatable properties)
amplitude f64 (required) Maximum deviation (pixels for position, degrees for rotation, etc.)
frequency f64 (required) Oscillations per second
seed u64 0 Random seed for reproducible results
octaves u32 3 Noise complexity (more octaves = more organic detail)
phase f64 0.0 Phase offset (shifts the noise pattern in time)
decay f64 Exponential decay rate (amplitude diminishes over time)
easing string Remap noise output through an easing curve

Wiggle offsets are applied additively on top of keyframe animations and presets.


Layer Timing

Control when layers appear and disappear within a scene using start_at and end_at:

{
  "type": "text",
  "content": "Appears at 1s, gone at 3s",
  "start_at": 1.0,
  "end_at": 3.0,
  "preset": "fade_in"
}
  • start_at: the layer is invisible before this time. Animation time is offset so t=0 in keyframes corresponds to start_at
  • end_at: the layer is invisible after this time
  • Both are optional and independent
  • start_at must be less than end_at when both are set

Motion Blur

Adds physically-correct motion blur by rendering multiple sub-frames and compositing them:

{
  "type": "shape",
  "shape": "circle",
  "motion_blur": 0.8,
  "preset": "slide_in_left"
}
Value Effect
0.0 No blur
0.5 Moderate blur
1.0 Full frame-duration blur

The renderer samples 5 sub-frames around the current time, each with proportional opacity.


Glow Effect

Adds a soft luminous halo around any component (text, shapes, icons, etc.). The glow renders as a pre-pass behind the component, keeping the content crisp and readable.

{
  "type": "text",
  "content": "NEON",
  "position": { "x": 540, "y": 400 },
  "font_size": 72,
  "color": "#ff00ff",
  "font_weight": "bold",
  "glow": {
    "color": "#ff00ff",
    "radius": 20,
    "intensity": 2.5
  }
}
Field Type Default Description
glow.color string "#FFFFFF" Glow color (hex #RRGGBB or #RRGGBBAA)
glow.radius f32 10.0 Blur radius of the glow
glow.intensity f32 1.0 Brightness multiplier (higher = brighter, more visible glow)

Works on all component types: text, shape, icon, image, svg, card, etc.


Freeze Frame

Freeze a scene at a specific point in time. All frames after freeze_at render the frozen state (animations stop, layers stay in place):

{
  "duration": 5.0,
  "freeze_at": 2.0,
  "layers": [ ... ]
}

The scene continues for its full duration but the visual output is frozen from freeze_at onward.


Output Formats

Format Command Requires
MP4 (H.264) rustmotion render in.json -o out.mp4 Built-in
MP4 (H.265) rustmotion render in.json -o out.mp4 --codec h265 ffmpeg
WebM (VP9) rustmotion render in.json -o out.webm --codec vp9 ffmpeg
MOV (ProRes) rustmotion render in.json -o out.mov --codec prores ffmpeg
Animated GIF rustmotion render in.json -o out.gif --format gif Built-in
PNG Sequence rustmotion render in.json -o frames/ --format png-seq Built-in
Single Frame rustmotion render in.json --frame 0 -o preview.png Built-in

Transparency is supported with --transparent for PNG sequences, WebM (VP9), and ProRes 4444.


Full Example

{
  "video": {
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "background": "#0f172a"
  },
  "audio": [
    { "src": "bgm.mp3", "volume": 0.3, "fade_in": 1.0, "fade_out": 2.0 }
  ],
  "scenes": [
    {
      "duration": 4.0,
      "layers": [
        {
          "type": "shape",
          "shape": { "star": { "points": 5 } },
          "position": { "x": 390, "y": 660 },
          "size": { "width": 300, "height": 300 },
          "fill": {
            "type": "radial",
            "colors": ["#fbbf24", "#f59e0b"]
          },
          "preset": "scale_in",
          "wiggle": [
            { "property": "rotation", "amplitude": 3.0, "frequency": 1.5, "seed": 1 }
          ]
        },
        {
          "type": "text",
          "content": "rustmotion",
          "position": { "x": 540, "y": 1100 },
          "font_size": 72,
          "color": "#FFFFFF",
          "align": "center",
          "preset": "fade_in_up",
          "preset_config": { "delay": 0.5 },
          "animations": [
            {
              "property": "color",
              "keyframes": [
                { "time": 1.5, "value": "#FFFFFF" },
                { "time": 3.0, "value": "#fbbf24" }
              ],
              "easing": "ease_in_out"
            }
          ]
        },
        {
          "type": "caption",
          "words": [
            { "text": "Motion", "start": 1.0, "end": 1.5 },
            { "text": "design", "start": 1.5, "end": 2.0 },
            { "text": "from", "start": 2.0, "end": 2.3 },
            { "text": "JSON", "start": 2.3, "end": 3.0 }
          ],
          "position": { "x": 540, "y": 1400 },
          "font_size": 48,
          "color": "#94a3b8",
          "active_color": "#FFFFFF",
          "background": "#1e293b",
          "style": "highlight",
          "max_width": 800
        }
      ],
      "transition": { "type": "iris", "duration": 0.8 }
    },
    {
      "duration": 3.0,
      "background": "#1e293b",
      "layers": [
        {
          "type": "text",
          "content": "No browser needed.",
          "position": { "x": 540, "y": 960 },
          "font_size": 56,
          "color": "#e2e8f0",
          "align": "center",
          "preset": "typewriter",
          "preset_config": { "duration": 1.5 }
        }
      ]
    }
  ]
}

Architecture

  • Rendering: skia-safe (same engine as Chrome/Flutter)
  • Video encoding: openh264 (Cisco BSD, compiled from source) + ffmpeg (optional, for H.265/VP9/ProRes)
  • Audio encoding: AAC via minimp4
  • SVG rendering: resvg + usvg
  • GIF decoding/encoding: gif crate
  • MP4 muxing: minimp4
  • JSON Schema: schemars (auto-generated from Rust types)
  • Parallelism: rayon (multi-threaded frame rendering)

License

MIT