Package Exports
- lotio
- lotio/browser/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (lotio) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Lotio
High-performance Lottie animation frame renderer using Skia. Renders animations to PNG frames for video encoding.
Installation
Homebrew (Recommended)
brew tap matrunchyk/lotio
brew install lotioThis installs:
- Binary:
lotio - Headers:
/opt/homebrew/include/lotio/(or/usr/local/include/lotio/) - Libraries:
/opt/homebrew/lib/(Skia static libraries)
From Source
Prerequisites (macOS):
brew install fontconfig freetype harfbuzz icu4c libpng ninja python@3.11
xcode-select --installBuild:
# Build lotio (binary build with zero bundled dependencies)
./scripts/build_binary.shUsage
Command Line
lotio --data <input.json> [--output-format <format>] [--output <file>] [--debug] [--layer-overrides <config.json>] [--fonts <dir>] [--text-padding <0.0-1.0>] [--text-measurement-mode <fast|accurate|pixel-perfect>] [--fps <fps>] <output_dir>Options:
--data <input.json>- Path to input Lottie animation JSON file (required)--output-format <format>- Output format:png(default),raw,ffv1, ormov--output <file>- Output file path (use-to stream to stdout, required forraw,ffv1,movformats)--debug- Enable debug output--layer-overrides <config.json>- Path to layer overrides JSON (for text auto-fit, dynamic text values, and image path overrides)--fonts <dir>- Directory containing font files (.ttf); fonts are looked up here first, then in the JSON directory--text-padding <0.0-1.0>- Text padding factor (0.0-1.0, default: 0.97 = 3% padding)--text-measurement-mode <fast|accurate|pixel-perfect>- Text measurement mode:fast|accurate|pixel-perfect(default:accurate)--fps <fps>- Frames per second for output (optional, default: animation fps or 30)--version- Print version information and exit--help, -h- Show help message
Output Formats:
png(default) - PNG frames written to directory (or streamed to stdout with--output -)raw- Uncompressed RGBA video file (fastest, largest files, preserves alpha)ffv1- Lossless FFV1 codec in Matroska container (good compression, preserves alpha)mov- MOV container with QTRLE codec (fast encoding, preserves alpha, widely compatible)
Examples:
# Render to PNG frames (default)
lotio --data animation.json --fps 30 frames/
# Direct video encoding (no ffmpeg binary needed)
lotio --data animation.json --output-format mov --output video.mov --fps 30
lotio --data animation.json --output-format ffv1 --output video.mkv --fps 30
lotio --data animation.json --output-format raw --output video.rgb --fps 30
# Stream PNG to stdout (for piping to ffmpeg)
lotio --data animation.json --output-format png --output - --fps 30 | ffmpeg -f image2pipe -i - output.mp4
# With layer overrides
lotio --data animation.json --layer-overrides layer-overrides.json --fps 30 frames/
# With custom font directory
lotio --data animation.json --fonts ./fonts --fps 30 frames/Docker
Quick start:
docker run --rm -v $(pwd):/workspace matrunchyk/lotio:latest \
--data data.json --fps 30 --layer-overrides layer-overrides.json --output-format mov --output video.movAvailable images:
matrunchyk/lotio:latest- lotio binary with built-in video encoding support
Multi-platform support: The image supports linux/arm64 and linux/amd64.
See Docker Documentation for detailed usage.
Browser (WebAssembly)
Install from npm:
npm install lotioBasic Usage:
import Lotio, { State, TextMeasurementMode } from 'lotio';
// Load fonts
const fontResponse = await fetch('./fonts/OpenSans-Bold.ttf');
const fontData = new Uint8Array(await fontResponse.arrayBuffer());
// Load animation
const animationResponse = await fetch('./animation.json');
const animationData = await animationResponse.json();
// Create animation instance
const animation = new Lotio({
fonts: [{ name: 'OpenSans-Bold', data: fontData }],
fps: 30,
animation: animationData,
layerOverrides: { /* optional layer overrides */ },
textPadding: 0.97, // Optional: text padding factor (default: 0.97)
textMeasurementMode: TextMeasurementMode.ACCURATE, // Optional: TextMeasurementMode.FAST | TextMeasurementMode.ACCURATE | TextMeasurementMode.PIXEL_PERFECT
wasmPath: './lotio.wasm'
});
// Event handlers (fluent interface)
animation
.on('error', (error, anim) => {
console.error('Animation error:', error);
})
.on('loaded', (anim) => {
console.log('Animation loaded');
anim.start();
})
.on('start', (anim) => {
console.log('Animation started');
})
.on('pause', (anim) => {
console.log('Animation paused');
})
.on('stop', (anim) => {
console.log('Animation stopped');
})
.on('end', (anim) => {
console.log('Animation ended');
})
.on('frame', (frameNumber, time, anim) => {
// Render to canvas
const canvas = document.getElementById('canvas');
anim.renderToCanvas(canvas, '#2a2a2a');
});
// Control methods
animation
.setFps(60) // Change FPS
.seek(10) // Seek to frame 10
.start() // Start playback
.pause() // Pause
.stop(); // Stop and reset
// Getters
const fps = animation.getFps();
const state = animation.getState(); // 'stopped' | 'paused' | 'loaded' | 'error' | 'playing'
const frame = animation.getCurrentFrame();
const info = animation.getAnimationInfo();
// Render current frame to canvas
const canvas = document.getElementById('canvas');
animation.renderToCanvas(canvas, '#ffffff');
// Cleanup
animation.destroy();Full Example with Canvas:
<!DOCTYPE html>
<html>
<head>
<title>Lotio Animation</title>
</head>
<body>
<canvas id="canvas"></canvas>
<button id="playBtn">Play</button>
<button id="pauseBtn">Pause</button>
<button id="stopBtn">Stop</button>
<script type="module">
import Lotio from 'lotio';
let animation;
async function init() {
// Load font
const fontRes = await fetch('./fonts/OpenSans-Bold.ttf');
const fontData = new Uint8Array(await fontRes.arrayBuffer());
// Load animation
const animRes = await fetch('./animation.json');
const animData = await animRes.json();
// Create animation
animation = new Lotio({
fonts: [{ name: 'OpenSans-Bold', data: fontData }],
fps: 30,
animation: animData,
wasmPath: './lotio.wasm'
});
const canvas = document.getElementById('canvas');
// Render frames
animation.on('frame', () => {
animation.renderToCanvas(canvas);
});
// Controls
document.getElementById('playBtn').onclick = () => animation.start();
document.getElementById('pauseBtn').onclick = () => animation.pause();
document.getElementById('stopBtn').onclick = () => animation.stop();
}
init();
</script>
</body>
</html>Samples
The samples/ directory contains example Lottie animations and configurations:
samples/sample1/- Basic animation with layer overridesdata.json- Lottie animation filelayer-overrides.json- Text and image customization configurationoutput/- Rendered frames (run lotio to generate)
samples/sample2/- Animation with external imagesdata.json- Lottie animation file with image referencesimages/- External image assets referenced by the animationoutput/- Rendered frames (run lotio to generate)
samples/sample5/- Text under track matte (Bodymovin-style,tpomitted); lotio's Skottie build includes a track-matte patch so the matte source is the nearest prior layer withtd !== 0and it renders correctly without editing data.json.
Try the samples:
# Sample 1: Basic animation with text customization
cd samples/sample1
lotio --data data.json --layer-overrides layer-overrides.json --fps 30 output/
# Sample 2: Animation with external images
cd samples/sample2
lotio --data data.json --fps 30 output/Using as a Library
Headers
Headers are installed at /opt/homebrew/include/lotio/ (or /usr/local/include/lotio/):
#include <lotio/core/animation_setup.h>
#include <lotio/text/text_processor.h>
#include <lotio/utils/logging.h>Linking
Link with Skia libraries:
g++ -I/opt/homebrew/include -L/opt/homebrew/lib \
-llotio -lskottie -lskia -lskparagraph -lsksg -lskshaper \
-lskunicode_icu -lskunicode_core -lskresources -ljsonreader \
your_app.cpp -o your_appOr use pkg-config (recommended):
g++ $(pkg-config --cflags --libs lotio) your_app.cpp -o your_appUsing Skia Directly
The lotio package includes Skia headers and libraries, so you can use Skia features directly in your code:
// Use Skia directly
#include <skia/core/SkCanvas.h>
#include <skia/core/SkSurface.h>
#include <skia/modules/skottie/include/Skottie.h>
// Use lotio
#include <lotio/core/animation_setup.h>
int main() {
// Use Skia API directly
SkImageInfo info = SkImageInfo::MakeN32(800, 600, kOpaque_SkAlphaType);
auto surface = SkSurfaces::Raster(info);
// Use lotio functions
AnimationSetupResult result = setupAndCreateAnimation("input.json", "");
return 0;
}Compile with:
g++ $(pkg-config --cflags --libs lotio) your_app.cpp -o your_appThe pkg-config file includes all necessary include paths:
-I${includedir}- Lotio headers-I${includedir}/skia- Skia core headers-I${includedir}/skia/gen- Skia generated headers
CI/CD Pipeline
The project uses GitHub Actions workflows for automated building, testing, and deployment:
graph TB
subgraph triggers["Event Triggers"]
mainPush["Push to main<br/>(creates semver tag)"]
tagPush["Tag push v*"]
lotioChanges["Changes to<br/>src/** or Dockerfile.lotio<br/>or build_binary.sh"]
docsChanges["Changes to<br/>docs/** or examples/**"]
manual["Manual<br/>workflow_dispatch"]
end
subgraph lotioWorkflow["build-lotio.yml<br/>Concurrency: build-lotio<br/>Cancel in-progress: true"]
buildLotioImg["Build Skia & lotio<br/>push matrunchyk/lotio"]
end
subgraph releaseWorkflow["release.yml<br/>Concurrency: release<br/>Cancel in-progress: true"]
versionTag["Generate version<br/>& create tag"]
buildMac["Build macOS<br/>binary & dev package"]
buildWasm["Build WASM<br/>library"]
buildHomebrew["Build Homebrew<br/>bottle"]
buildDocs["Build<br/>documentation"]
publishRelease["Create GitHub<br/>release"]
versionTag --> buildMac
versionTag --> buildWasm
versionTag --> buildHomebrew
buildWasm --> buildDocs
buildHomebrew --> buildDocs
buildDocs --> publishRelease
end
subgraph testWorkflow["test.yml<br/>Concurrency: test<br/>Cancel in-progress: true"]
testDocker["Test Docker<br/>image"]
testWasm["Test JS/WASM<br/>library"]
testHomebrew["Test Homebrew<br/>package"]
end
subgraph pagesWorkflow["pages.yml<br/>Concurrency: pages<br/>Cancel in-progress: false"]
buildPages["Build & deploy<br/>documentation"]
end
mainPush --> lotioWorkflow
mainPush --> releaseWorkflow
mainPush --> pagesWorkflow
tagPush --> lotioWorkflow
tagPush --> releaseWorkflow
lotioChanges --> lotioWorkflow
docsChanges --> pagesWorkflow
manual --> lotioWorkflow
manual --> releaseWorkflow
manual --> testWorkflow
manual --> pagesWorkflow
lotioWorkflow -->|Docker image ready| releaseWorkflow
releaseWorkflow -->|workflow_run<br/>after completion| testWorkflow
style lotioWorkflow fill:#e1f5ff
style releaseWorkflow fill:#fff4e1
style testWorkflow fill:#e8f5e9
style pagesWorkflow fill:#f3e5f5Workflow Descriptions
build-lotio.yml - Builds and publishes matrunchyk/lotio Docker image
- Purpose: Create lotio binary Docker image using pre-built Skia base image
- Triggers: Main branch push, tag pushes, source code changes, Dockerfile.lotio changes, build_binary.sh changes, manual dispatch
- Logic: Uses
matrunchyk/skia:latestas base image (Skia pre-built), only compiles lotio source - Build chain:
Dockerfile.skia→Dockerfile.lotio(uses pre-built Skia) - Concurrency: Single instance per workflow (cancels in-progress runs when new one starts)
- Output:
matrunchyk/lotio:latestandmatrunchyk/lotio:v1.2.3(multi-platform: arm64, amd64) - Architecture tags: Also creates
-arm64and-amd64tags for clarity
release.yml - Builds all release artifacts and creates GitHub release
- Purpose: Build and package all distribution formats (binaries, WASM, Homebrew, docs)
- Triggers: Push to main (creates semver tag automatically), tag pushes (v*), manual dispatch
- Logic:
- Generates semver version from tag or creates new tag on main push
- Builds Skia from scratch using
build_binary.sh(zero bundled dependencies, fast build) - Builds in parallel: macOS, Linux, WASM, Homebrew
- Injects version into all artifacts
- Concurrency: Single instance per workflow (cancels in-progress runs when new one starts)
- Output: macOS dev package, WASM package, Homebrew bottle, GitHub release
test.yml - Integration tests for all built artifacts
- Purpose: Validate that all release artifacts work correctly
- Triggers: After
release.ymlcompletes successfully, manual dispatch - Tests:
- Docker image:
--help,--version, library functionality, video generation with--debug - JS/WASM library: Load, API functions, frame rendering
- Homebrew package: Installation,
--help,--version, basic functionality
- Docker image:
- Concurrency: Single instance per workflow (cancels in-progress runs when new one starts)
pages.yml - Builds and deploys documentation to GitHub Pages
- Purpose: Generate and deploy documentation with version injection
- Triggers: Changes to docs, examples, or build scripts; manual dispatch
- Logic: Installs lotio npm package, injects version from git tag
- Concurrency: Single instance per workflow (does not cancel in-progress runs)
- Output: Deployed to GitHub Pages
Project Structure
src/
├── core/ # Core functionality (argument parsing, animation setup, rendering)
├── text/ # Text processing (configuration, font handling, sizing)
└── utils/ # Utilities (logging, string utils, crash handling)IDE Setup
The project includes IDE configuration for Cursor/VS Code:
.vscode/c_cpp_properties.json- C/C++ extension settings.clangd- clangd language server settings
Reload Cursor/VS Code after cloning: Cmd+Shift+P → "Reload Window"
Troubleshooting
Skia build fails:
- Ensure all dependencies are installed
- Check sufficient disk space (Skia build is large)
- Review error messages in
scripts/build_binary.shoutput
Linker errors:
- Verify Skia libraries exist in
third_party/skia/skia/out/Release/ - Check library paths in build script
IDE include errors:
- Reload Cursor/VS Code
- Verify
.vscode/c_cpp_properties.jsonhas correct paths
License
See individual component licenses:
- Skia:
third_party/skia/skia/LICENSE