Spine 4.2 Bezier curves in JSON, explained
The curve field is the most confusing part of the Spine animation format, and the official format page in places still describes Spine 3.8, where it meant something else. Here is what the numbers mean in 4.x, with frames taken from the official example exports.
Three kinds of curve
A key's curve describes the way from this key to the next one, so a curve on the last key is ignored. It comes in three forms:
- no
curveat all — linear interpolation; "stepped"— the value holds until the next key and then jumps;- an array of numbers — a cubic Bezier, four numbers per component of the value.
An empty key {} is valid too: time 0 and the default values of its timeline. Omitted values fall back to the timeline's default, not to the previous key.
The four numbers are absolute
For each component the array holds [cx1, cy1, cx2, cy2] — the two control points of the curve that runs from (time1, value1) of this key to (time2, value2) of the next. In 4.x they are absolute: cx1 and cx2 are seconds and must stay inside the segment, time1 ≤ cx1 ≤ cx2 ≤ time2; cy1 and cy2 are values in the timeline's own units — degrees for rotate, pixels for translate, 0 to 1 for color components and mixes.
A rotate swing from the celestial-circus example:
[
{ "value": -13.18, "curve": [0.607, -13.18, 0.733, 15.86] },
{ "time": 1.1, "value": 15.86, "curve": [1.707, 15.86, 1.833, -13.18] },
{ "time": 2.2, "value": -13.18 }
]In the first segment, 0 s to 1.1 s and −13.18° to 15.86°, cy1 equals the start value and cy2 the end value: flat tangents at both keys, the classic ease-in-out pendulum.
Four numbers per component
A timeline with several components needs a quadruple for each, in order — even for a component that does not change. Stepped is one string for the whole key; linear, stepped and Bezier cannot be mixed within one key.
| Timeline | Components | Numbers |
|---|---|---|
| rotate, translatex, scaley, alpha… | value | 4 |
| translate, scale, shear | x, y | 8 |
| rgb | r, g, b | 12 |
| rgba | r, g, b, a | 16 |
| deform | progress 0 → 1 | 4 |
A two-component translate from the coin example: x eases from −8.3 to 0, while y stays at 0 and still gets its own flat quadruple.
{ "time": 0.6696, "x": -8.3,
"curve": [0.794, -7.08, 1.167, 0, 0.794, 0, 1.167, 0] }Deform is the odd one: its value is a single progress from one mesh shape to the next, so its curve is always four numbers and its cy runs from 0 to 1, whatever the vertices do.
Stepped
Stepped holds a pose, or switches instantly. The coin flips its color with two keys almost on the same time, the first one stepped:
{ "time": 1.3318, "color": "858585ff", "curve": "stepped" },
{ "time": 1.3333, "color": "858585ff" }Why 3.8 curves break in 4.2
In Spine 3.8 all four numbers were normalized to 0…1 — shares of the time interval and of the value change, exactly like CSS cubic-bezier — and one curve served all components. The current encoding came with 4.0. Paste normalized numbers into a 4.x file and they are read as seconds and degrees: the file loads, and the animation jerks. To convert a CSS easing or a 3.8-style curve, use the cubic-bezier to Spine 4.2 curve converter.
Recipes, and one trap
- Linear — leave
curveout. - Linear as an explicit Bezier — control points on the chord:
cx1 = t1 + Δt/3,cy1 = v1 + Δv/3,cx2 = t1 + 2Δt/3,cy2 = v1 + 2Δv/3. - Ease-in-out —
cy1 = v1,cy2 = v2, withcx1andcx2at about a third and two thirds of the segment. The editor's default curves look like this.
The trap: stretching an animation in time moves the curves too. The first and third numbers of every quadruple are times, so scaling the key times and forgetting the curves leaves control points outside their segment — the runtime draws jerks instead of an error. The Spine JSON validator flags such handles. Which curves animators actually use, and where they leave keys linear, is measured in Idle animation timing, measured on 127 official Spine animations.