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.
  • ease belongs to the segment that starts at its key. An ease on 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, fps or duration is missing or not above 0
  • layers is not an array
  • a layer has no string id, or two layers share one
  • a layer's transform is missing, or anchor is not [x, y]
  • geometry.kind is not rect, ellipse or path
  • an animatable value has neither value nor a non-empty, time-sorted keys
  • blend is 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 repeat is 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

  • v increases 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.