Interactive 3D Gaussian Splatting - like Rive/Lottie for 3D
Quick Start · Format Spec · API · Editor · How It Works
A web component that makes gaussian splats reactive. One file, one tag. Eyes follow the cursor, face blinks and reacts to hover/click, expressions transition smoothly. 60fps, client-side.
See it live at afromero.co | Create your own at splattie.app
<splattie-widget src="avatar.splattie"></splattie-widget>
<script type="module" src="https://unpkg.com/@afromero/splattie-widget"></script>Or via npm:
npm install @afromero/splattie-widgetimport '@afromero/splattie-widget';v0.x experimental. Core files (PLY, FLAME bones) follow established standards. Expression basis and states may evolve.
A ZIP bundle with a required manifest.json that declares every asset
and locks the file's formatVersion to the widget version. See
FORMAT.md for the full spec.
avatar.splattie
├── manifest.json # (required) declares every asset + formatVersion
├── *.ply or *.spz # (required) Gaussian splats
├── bone_tree.json # (optional) Skeleton for skinning
├── lbs_weight_20k.json # (optional) Per-splat bone weights
├── expression_basis.bin # (optional) Blendshape basis
└── states.json # (optional) Interaction states
Splat data (.ply or .spz) - standard 3DGS format
Each splat has position, scale, rotation, opacity, and SH color. Auto-detected from file header. Works with any 3DGS method (LAM, DreamGaussian, InstantSplat, etc.). Standard format, unlikely to change.
bone_tree.json - skeleton hierarchy
5 FLAME bones: root > neck > jaw, leftEye, rightEye. Used for SplatSkinning (dual quaternion).
{
"bones": [{
"name": "root",
"position": [x, y, z],
"children": [{
"name": "neck",
"position": [x, y, z],
"children": [
{ "name": "jaw", "position": [x, y, z] },
{ "name": "leftEye", "position": [x, y, z] },
{ "name": "rightEye", "position": [x, y, z] }
]
}]
}]
}Stable structure. Bone names are conventions, not hard requirements. Without it: no eye tracking, no jaw animation.
lbs_weight_20k.json - per-splat bone weights
2D array [num_splats][num_bones], each row sums to ~1.0. Widget selects top 4 per splat.
[[0.8, 0.1, 0.05, 0.03, 0.02], ...]Standard LBS format from FLAME. Without it: bones exist but nothing moves.
expression_basis.bin - FLAME blendshape basis
Per-splat position displacements for each expression coefficient. Moves all splats coherently for smile, lip shapes, etc.
Header: "EXPR" (4B) + num_vertices (u32 LE) + num_expressions (u32 LE)
Data: float32 LE array, shape (num_vertices, num_expressions, 3)
Optional sidecar expression_basis.json with semantic labels:
{ "labels": ["jawDown", "lipsUp", "lipsL", ...], "num_expressions": 50 }Experimental format, may add compression. Without it: bone-driven expressions still work.
states.json - interaction state definitions
Each state (idle, hover, click) sets all 5 dimensions simultaneously.
{
"defaults": {
"camera": { "theta": 0, "phi": 75, "radius": 0.5, "fov": 60 },
"autoBlink": { "interval": [2000, 7000], "duration": 150 }
},
"states": {
"idle": {
"ghost": { "amplitude": 0.003, "frequency": 0.4, "wobble": 0.2 },
"expression": { "jawOpen": 0, "smile": 0 },
"camera": { "theta": 0, "phi": 75, "radius": 0.5, "fov": 60 },
"rotation": [0, 0, 0],
"tracking": { "eyes": 1.0, "head": 0.1 }
},
"hover": { "..." : "..." },
"click": { "..." : "..." }
},
"transitions": {
"idle->hover": { "duration": 0.3, "easing": "ease-out" },
"*->click": { "duration": 0.1, "easing": "snap" }
}
}Most likely to evolve. Without it: sensible defaults (eyes track, gentle float, auto-blink).
Visual editor: npm run dev, adjust sliders, click "Download .splattie".
From scratch: ZIP a .ply with any combination of the optional files.
From a photo: run LAM on a GPU, then bundle with the export script. Try it at splattie.app.
| Dimension | Controls | Example |
|---|---|---|
| Ghost | Floating/bobbing | Gentle hover on idle, freeze on click |
| Expression | FLAME blendshapes + bones | Smile on hover, surprise on click |
| Camera | Spherical position | Zoom in on hover |
| Rotation | Pitch/yaw/roll | Tilt head on hover |
| Tracking | Cursor-follow intensity | Eyes on idle, head follows on hover |
Interpolated between states with configurable easing and duration.
Expression system details
Two layers:
Bone-driven (SplatSkinning, 5 FLAME bones):
- Jaw open/close, neck pitch/yaw/roll
- Eye gaze direction (left/right, up/down)
- Brow raise/frown (left/right independently)
Blendshape-driven (FLAME expression basis, 10+ PCA coefficients):
- Moves all 20K splats coherently
- Smile, lip shapes, jaw articulation, cheek/nose deformation
- Spatial mask prevents beard/neck from deforming
| Attribute | Description |
|---|---|
src |
URL to .splattie file (or .ply/.spz) |
background |
Background color hex (default: #0e0e14) |
width |
CSS width (default: 100%) |
height |
CSS height (default: 400px) |
widget.addEventListener('splatload', () => {}); // ready
widget.addEventListener('splathover', () => {}); // cursor on face
widget.addEventListener('splatclick', () => {}); // clicked face
widget.addEventListener('splatleave', () => {}); // cursor left
widget.setState('hover'); // force transitionnpm run dev # http://localhost:4002Sliders for all 5 dimensions, camera sphere widget, state tabs with copy-forward, FLAME blendshape controls, drag-and-drop .splattie upload, export when done.
Built on Spark 2.0 (MIT, World Labs).
Architecture
- State machine with per-dimension interpolation (lerp, slerp, ease curves)
- SplatSkinning (dual quaternion) driving 5 FLAME bones from expression + cursor data
- Expression basis - per-splat position offsets written to Spark's packed buffer (half-float, ~20K splats/frame)
- Hit detection via
readPixelsafter render (pixel-perfect) - Auto-blink with randomized interval and sine-curve via SplatEdit
- Gyroscope tracking on mobile (iOS permission prompt included)
Touch + gyroscope. Eyes follow device orientation on mobile, touch position on tap. Return to center when finger lifts. iOS motion permission requested automatically.
Chrome, Firefox, Safari, Edge. WebGL 2 required. No COOP/COEP headers needed.
- LAM (SIGGRAPH 2025) - Zixuan Zeng et al., AIGC3D team
- FLAME - Tianye Li, Timo Bolkart, Michael J. Black, Hao Li, Javier Romero
- Spark 2.0 - World Labs (MIT)
- 3D Gaussian Splatting - Kerbl, Kopanas, Leimkuhler, Drettakis (INRIA)
MIT
