
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
High-performance Lottie animation frame renderer using Skia. Renders animations to PNG frames for video encoding.
brew tap matrunchyk/lotio
brew install lotio
This installs:
lotio/opt/homebrew/include/lotio/ (or /usr/local/include/lotio/)/opt/homebrew/lib/ (Skia static libraries)Prerequisites (macOS):
brew install fontconfig freetype harfbuzz icu4c libpng ninja python@3.11
xcode-select --install
Build:
# Build lotio (binary build with zero bundled dependencies)
./scripts/build_binary.sh
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, or mov--output <file> - Output file path (use - to stream to stdout, required for raw, ffv1, mov formats)--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 messageOutput 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/
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.mov
Available images:
matrunchyk/lotio:latest - lotio binary with built-in video encoding supportMulti-platform support: The image supports linux/arm64 and linux/amd64.
See Docker Documentation for detailed usage.
Install from npm:
npm install lotio
Basic 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>
The samples/ directory contains example Lottie animations and configurations:
samples/sample1/ - Basic animation with layer overrides
data.json - Lottie animation filelayer-overrides.json - Text and image customization configurationoutput/ - Rendered frames (run lotio to generate)samples/sample2/ - Animation with external images
data.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, tp omitted); lotio's Skottie build includes a track-matte patch so the matte source is the nearest prior layer with td !== 0 and 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/
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>
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_app
Or use pkg-config (recommended):
g++ $(pkg-config --cflags --libs lotio) your_app.cpp -o your_app
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_app
The pkg-config file includes all necessary include paths:
-I${includedir} - Lotio headers-I${includedir}/skia - Skia core headers-I${includedir}/skia/gen - Skia generated headersThe 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:#f3e5f5
build-lotio.yml - Builds and publishes matrunchyk/lotio Docker image
matrunchyk/skia:latest as base image (Skia pre-built), only compiles lotio sourceDockerfile.skia → Dockerfile.lotio (uses pre-built Skia)matrunchyk/lotio:latest and matrunchyk/lotio:v1.2.3 (multi-platform: arm64, amd64)-arm64 and -amd64 tags for clarityrelease.yml - Builds all release artifacts and creates GitHub release
build_binary.sh (zero bundled dependencies, fast build)test.yml - Integration tests for all built artifacts
release.yml completes successfully, manual dispatch--help, --version, library functionality, video generation with --debug--help, --version, basic functionalitypages.yml - Builds and deploys documentation to GitHub Pages
src/
├── core/ # Core functionality (argument parsing, animation setup, rendering)
├── text/ # Text processing (configuration, font handling, sizing)
└── utils/ # Utilities (logging, string utils, crash handling)
The project includes IDE configuration for Cursor/VS Code:
.vscode/c_cpp_properties.json - C/C++ extension settings.clangd - clangd language server settingsReload Cursor/VS Code after cloning: Cmd+Shift+P → "Reload Window"
Skia build fails:
scripts/build_binary.sh outputLinker errors:
third_party/skia/skia/out/Release/IDE include errors:
.vscode/c_cpp_properties.json has correct pathsSee individual component licenses:
third_party/skia/skia/LICENSEFAQs
High-performance Lottie animation frame renderer for the browser
We found that lotio demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.