The svgmo file format
Version 1.
An svgmo file is a JSON document describing a vector animation: an artboard, a list of layers, and values that change over time through keyframes. It is what the svgmo editor exports and what the svgmo player plays.
| Extension | .json. The "format": "svgmo" field identifies the file, not the extension |
| Content type | application/json (any JSON or text type also works) |
| Encoding | UTF-8 JSON |
The source of truth is packages/engine/src/types.ts (the data) and packages/engine/src/svgmo.ts (the envelope).
Envelope
{
"format": "svgmo",
"v": 1,
"meta": { "name": "Logo intro", "generator": "svgmo" },
"comp": { }
}
| Field | Type | Meaning |
|---|---|---|
format |
"svgmo" |
Identifies the file. Readers also accept the earlier value "vectormotion". |
v |
number | Format version. A reader refuses a file whose v is higher than it knows. |
meta.name |
string, optional | Title of the animation |
meta.generator |
string | What wrote the file |
comp |
object | The composition |
A reader also accepts a bare composition (the comp object on its own, as in editor project files).
Composition
{
"version": 1,
"name": "Logo intro",
"width": 512,
"height": 512,
"fps": 30,
"duration": 90,
"background": "#ffffff",
"layers": []
}
| Field | Type | Meaning |
|---|---|---|
version |
1 |
Document model version |
name |
string, optional | Title |
width, height |
number above 0 | Artboard size in pixels |
fps |
number above 0 | Frames per second |
duration |
number above 0 | Length in frames. Frames run from 0 to duration. |
background |
string, optional | Hex colour behind everything. Absent means transparent. |
layers |
array | Layers, bottom-most first: later entries draw on top |
Coordinates: origin at the top-left of the artboard, x to the right, y down, in pixels.
Animatable values
Any value marked animatable below is one of:
{ "value": 100 }
{ "keys": [
{ "t": 0, "v": 0, "ease": { "x1": 0.42, "y1": 0, "x2": 0.58, "y2": 1 } },
{ "t": 30, "v": 100 }
] }
| Field | Meaning |
|---|---|
value |
A constant |
keys |
Keyframes, sorted by t, at least one |
t |
Frame number of the key |
v |
Value at that frame |
ease |
How the value moves from this key to the next. Optional, default "linear". |
Rules:
- Before the first key the value is the first key's value; after the last key it is the last key's value.
- Between two keys the value is interpolated: numbers and number arrays component by component, paths point by point.
easebelongs to the segment that starts at its key. Aneaseon the last key has no effect.
Easing
ease |
Meaning |
|---|---|
"linear" or absent |
Constant rate |
"hold" |
Stay on this key's value until the next key, then jump |
{ "x1", "y1", "x2", "y2" } |
Cubic bezier timing curve, as in CSS cubic-bezier(x1, y1, x2, y2). x values are in 0..1; y values may go outside 0..1 to overshoot. |
Layer
{
"id": "badge",
"name": "Badge",
"transform": { },
"geometry": { },
"fill": { },
"stroke": null,
"inPoint": 10,
"outPoint": 80,
"parent": "group-1",
"blend": "multiply",
"motionPath": null
}
| Field | Type | Meaning |
|---|---|---|
id |
string | Unique within the file |
name |
string | Label |
transform |
object | Position, scale, rotation, opacity |
geometry |
object | The shape |
fill |
object or null |
Inside paint. null or absent: no fill. |
stroke |
object or null |
Outline paint. null or absent: no stroke. |
inPoint |
number, optional | First frame the layer is visible. Default 0. |
outPoint |
number, optional | Last frame the layer is visible, inclusive. Default: the composition's duration. |
parent |
string, optional | id of a layer whose position, rotation and scale this layer follows |
blend |
string, optional | Blend mode. Default normal. |
motionPath |
object or null |
Makes the layer travel along a path |
hidden, locked |
boolean, optional | Editor settings. Hidden layers are not drawn. Playback exports omit both. |
Transform
{
"anchor": [0, 0],
"position": { "value": [256, 256] },
"scale": { "value": [1, 1] },
"rotation": { "value": 0 },
"opacity": { "value": 1 }
}
| Field | Type | Meaning |
|---|---|---|
anchor |
[x, y] |
The point of the shape, in the shape's own coordinates, that sits at position and that scale and rotation turn around. Not animatable. |
position |
animatable [x, y] |
Where the anchor is, in the parent's space (the artboard when there is no parent) |
scale |
animatable [sx, sy] |
1 is 100%. Negative values mirror. |
rotation |
animatable number | Degrees, clockwise |
opacity |
animatable number | 0 to 1 |
A point of the shape is placed by, in order: subtract the anchor, scale, rotate, add the position. As an SVG transform: translate(position) rotate(rotation) scale(scale) translate(-anchor).
Parenting
A layer with parent is transformed by its own transform and then by its parent's, and so on up the chain. Opacity, visibility (inPoint, outPoint, hidden) and blend mode are not inherited. A parent that names a missing layer, or a chain that loops, is ignored from that point.
Geometry
Shapes are drawn around the layer's own origin (0, 0).
Rectangle:
{ "kind": "rect", "size": { "value": [160, 90] }, "radius": { "value": 12 } }
Ellipse:
{ "kind": "ellipse", "size": { "value": [120, 120] } }
Path:
{ "kind": "path", "path": { "value": { "closed": true, "points": [ ] } } }
| Field | Type | Meaning |
|---|---|---|
size |
animatable [width, height] |
Rectangle and ellipse are centred on the origin |
radius |
animatable number | Corner radius of a rectangle |
path |
animatable path | The outline |
Path
{
"closed": true,
"points": [
{ "x": 0, "y": -50, "inX": -28, "inY": 0, "outX": 28, "outY": 0 },
{ "x": 50, "y": 0, "inX": 0, "inY": -28, "outX": 0, "outY": 28 }
],
"sub": []
}
| Field | Meaning |
|---|---|
closed |
Whether the last point joins back to the first |
points |
Bezier vertices in order |
x, y |
Vertex position |
inX, inY |
Incoming handle, relative to the vertex |
outX, outY |
Outgoing handle, relative to the vertex |
sub |
Optional extra sub-paths drawn with the same paint: holes and separate pieces of a compound path |
The segment from point A to point B is the cubic bezier A, A + out, B + in, B. Handles of 0, 0 give a straight segment and a sharp corner.
Morphing: a path animates between two keys only when both have the same number of points and the same number of sub-paths (each with matching point counts). Otherwise the shape holds the earlier key until the next one.
Fill
{ "color": { "value": [32, 150, 243, 1] }, "rule": "evenodd", "gradient": { } }
| Field | Type | Meaning |
|---|---|---|
color |
animatable [r, g, b, a] |
r, g, b from 0 to 255; a from 0 to 1 |
rule |
"nonzero" or "evenodd" |
How overlapping sub-paths make holes. Default nonzero. |
gradient |
object, optional | When present, replaces color |
Gradient
{ "type": "linear", "angle": 90, "stops": [
{ "offset": 0, "color": [99, 132, 255, 1] },
{ "offset": 1, "color": [255, 99, 132, 1] }
] }
| Field | Meaning |
|---|---|
type |
"linear" or "radial" |
stops |
At least two { offset, color }. offset is 0..1; color is [r, g, b, a]. |
angle |
Linear: direction in degrees. 0 is left to right, 90 is top to bottom. Default 0. |
cx, cy |
Radial: centre as a fraction of the shape's bounds. Default 0.5. |
radius |
Radial: radius as a fraction of the larger side of the bounds. Default 0.5. |
A gradient is placed relative to the shape's bounding box, so it follows the shape as it resizes or morphs. Stops and gradient geometry are not animatable.
Stroke
{
"color": { "value": [0, 0, 0, 1] },
"width": { "value": 4 },
"cap": "round",
"join": "round",
"dash": [8, 6],
"trim": { "start": { "value": 0 }, "end": { "value": 1 } }
}
| Field | Type | Meaning |
|---|---|---|
color |
animatable [r, g, b, a] |
Outline colour |
width |
animatable number | Thickness in pixels |
cap |
"butt", "round", "square" |
Line ends. Default butt. |
join |
"miter", "round", "bevel" |
Corners. Default miter. |
dash |
[dash, gap], optional |
Dashed outline, lengths in pixels. Ignored while trim is active. |
trim |
object, optional | Draw only part of the outline |
trim.start, trim.end |
animatable number | Fractions of the outline's length, 0..1. Animate end from 0 to 1 to draw the line on. |
Motion path
{
"path": { "closed": false, "points": [ ] },
"progress": { "keys": [ { "t": 0, "v": 0 }, { "t": 60, "v": 1 } ] },
"orient": true,
"orientOffset": 0,
"repeat": 2,
"pingpong": true
}
When present, the layer's position comes from the path instead of transform.position.
| Field | Type | Meaning |
|---|---|---|
path |
path | The route, in the parent's space. Needs at least 2 points. Not animatable. |
progress |
animatable number | How far along the route, 0..1, measured by length |
orient |
boolean | Turn the layer to face along the route. The heading is added to transform.rotation. |
orientOffset |
number, optional | Degrees added to the heading, for artwork that does not point right |
repeat |
number 1..1000, optional | Play the progress keys this many times back to back. Default 1. |
pingpong |
boolean, optional | With repeat above 1, every second pass runs backwards |
source |
string, optional | Editor link to the layer the route came from. Playback exports omit it. |
Blend modes
normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, luminosity.
Validity
A reader rejects a file when:
width,height,fpsordurationis missing or not above 0layersis not an array- a layer has no string
id, or two layers share one - a layer's
transformis missing, oranchoris not[x, y] geometry.kindis notrect,ellipseorpath- an animatable value has neither
valuenor a non-empty, time-sortedkeys blendis not in the list above- a gradient has fewer than 2 stops or an unknown type
- a motion path has fewer than 2 points, or
repeatis outside 1..1000
A complete small file
A blue circle that fades in and slides right:
{
"format": "svgmo",
"v": 1,
"meta": { "name": "Slide", "generator": "by hand" },
"comp": {
"version": 1,
"width": 400,
"height": 200,
"fps": 30,
"duration": 45,
"layers": [
{
"id": "dot",
"name": "Dot",
"transform": {
"anchor": [0, 0],
"position": { "keys": [
{ "t": 0, "v": [60, 100], "ease": { "x1": 0, "y1": 0, "x2": 0.58, "y2": 1 } },
{ "t": 30, "v": [340, 100] }
] },
"scale": { "value": [1, 1] },
"rotation": { "value": 0 },
"opacity": { "keys": [ { "t": 0, "v": 0 }, { "t": 10, "v": 1 } ] }
},
"geometry": { "kind": "ellipse", "size": { "value": [60, 60] } },
"fill": { "color": { "value": [32, 150, 243, 1] } },
"stroke": null
}
]
}
}
Reading and writing from code
import { packSvgmo, readSvgmo } from "@vectormotion/engine";
const text = packSvgmo(composition); // playback file
const editable = packSvgmo(composition, { editable: true, pretty: true });
const result = readSvgmo(text);
if (result.ok) use(result.comp);
else console.warn(result.errors);
packSvgmo options:
| Option | Default | Meaning |
|---|---|---|
precision |
3 |
Decimals kept in numbers |
editable |
false |
Keep hidden layers and editor settings |
pretty |
false |
Indent the JSON |
A playback file (the default) drops hidden layers. A hidden layer that is the parent of a visible one is kept, without fill or stroke, so its child still moves with it.
Earlier names
The format was first called VectorMotion. Readers keep accepting:
| Earlier | Now |
|---|---|
extensions .svgmo, .vmotion |
.json |
"format": "vectormotion" |
"format": "svgmo" |
Nothing else differs, so an old file can simply be renamed. The editor writes the new names.
Versioning
vincreases when a change would make an older reader draw the file wrongly.- New optional fields that older readers can ignore do not change
v. - Readers ignore fields they do not know.