Skip to content

Tracks ​

A track says how one property of every element in a group changes over one cycle. It is a list of keyframes: keyTimes says when, values says what, and easing says how the value moves between two keyframes.

json
{ "property": "scale", "keyTimes": [0, 0.5, 1], "values": [1, 0.4, 1], "easing": "easeInOut" }

Properties ​

PropertyRest valueUnitMeaning
scale1factorscales width and height
scaleX, scaleY1factorscales one axis. Multiplied with scale
opacity10 to 1multiplies the alpha of the element color
rotate0radiansturns the element clockwise around its center
rotateX, rotateY0radians3D flips around the horizontal or vertical axis (see Perspective)
translateX, translateY0box unitsmoves the element. 1 is the full box side. Negative y is up
strokeStart, strokeEnd0, 10 to 1trims the arcs of a ring shape

A property without a track keeps its rest value. A property can appear at most once in tracks and at most once in groupTracks.

keyTimes and values ​

keyTimes are points of the cycle, from 0 (the start) to 1 (the end). values has one value per key time. Rules:

  • keyTimes and values have the same length, at least 2.
  • keyTimes are within [0, 1] and never decrease.
  • Before the first key time, the value is the first value. After the last key time, it is the last value.

To loop smoothly, start at 0, end at 1, and make the last value equal to the first.

Holding and jumping ​

Two equal values in a row hold the value. Two equal key times in a row make an instant jump. Here the square shrinks, holds, grows, holds, and its opacity jumps from 1 to 0.4 in the middle of the cycle:

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "TrackSteps",
  "duration": 2,
  "layout": { "type": "single", "size": 0.6 },
  "shape": { "type": "rect", "cornerRadius": 0.2 },
  "tracks": [
    { "property": "scale", "keyTimes": [0, 0.25, 0.5, 0.75, 1], "values": [1, 0.5, 0.5, 1, 1], "easing": "easeInOut" },
    { "property": "opacity", "keyTimes": [0, 0.5, 0.5, 1], "values": [1, 1, 0.4, 0.4] }
  ]
}
null

Easing ​

Easing shapes the motion between two keyframes. The default is linear.

NameCubic bezierFeel
linear[0, 0, 1, 1]constant speed
ease[0.25, 0.1, 0.25, 1]the CSS and Core Animation default
easeIn[0.42, 0, 1, 1]starts slow
easeOut[0, 0, 0.58, 1]ends slow
easeInOut[0.42, 0, 0.58, 1]starts and ends slow

You can also give a cubic bezier [x1, y1, x2, y2], like CSS cubic-bezier(). x1 and x2 must be within [0, 1]. y1 and y2 can go outside, which makes the value overshoot.

Dashed line: linear, for comparison. Drag the two handles or use the sliders.
0.42
0.00
0.58
1.00
"easing": "easeInOut"

The three dots below move with linear, easeInOut and the overshooting bezier [0.34, 1.56, 0.64, 1]:

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "TrackEasing",
  "duration": 1.6,
  "parts": [
    {
      "layout": { "type": "single", "size": 0.2, "x": 0.18, "y": 0.2 },
      "shape": { "type": "circle" },
      "tracks": [
        { "property": "translateX", "keyTimes": [0, 0.5, 1], "values": [0, 0.64, 0], "easing": "linear" }
      ]
    },
    {
      "layout": { "type": "single", "size": 0.2, "x": 0.18, "y": 0.5 },
      "shape": { "type": "circle" },
      "tracks": [
        { "property": "translateX", "keyTimes": [0, 0.5, 1], "values": [0, 0.64, 0], "easing": "easeInOut" }
      ]
    },
    {
      "layout": { "type": "single", "size": 0.2, "x": 0.18, "y": 0.8 },
      "shape": { "type": "circle" },
      "tracks": [
        { "property": "translateX", "keyTimes": [0, 0.5, 1], "values": [0, 0.64, 0], "easing": [0.34, 1.56, 0.64, 1] }
      ]
    }
  ]
}
null

One easing per segment ​

A track with n key times has n - 1 segments. easing can be one value for every segment, or a list with exactly one easing per segment. Here each dot rises with easeOut, falls with easeIn, then rests:

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "TrackPerSegment",
  "duration": 0.9,
  "layout": { "type": "row", "count": 3, "gap": 0.12 },
  "shape": { "type": "circle" },
  "stagger": { "each": 0.12 },
  "tracks": [
    {
      "property": "translateY",
      "keyTimes": [0, 0.35, 0.7, 1],
      "values": [0, -0.25, 0, 0],
      "easing": ["easeOut", "easeIn", "linear"]
    }
  ]
}
TrackPerSegmentOpen in playground
null

How a list is read

If the first item of easing is a number, the whole list is one bezier. Otherwise it is one easing per segment. So [0.42, 0, 0.58, 1] is one bezier, and [[0.42, 0, 0.58, 1], "linear"] is two segments.

Translation ​

translateX and translateY move an element from its layout position, in box units. Two tracks with the same key times can draw a path. This dot travels around a diamond:

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "TrackTranslate",
  "duration": 1.2,
  "layout": { "type": "single", "size": 0.25 },
  "shape": { "type": "circle" },
  "tracks": [
    { "property": "translateX", "keyTimes": [0, 0.25, 0.5, 0.75, 1], "values": [-0.3, 0, 0.3, 0, -0.3], "easing": "easeInOut" },
    { "property": "translateY", "keyTimes": [0, 0.25, 0.5, 0.75, 1], "values": [0, -0.3, 0, 0.3, 0], "easing": "easeInOut" }
  ]
}
null

Reading a timeline ​

<TrackTimeline> plots each element's value over one cycle. This is the built-in BallPulse: a scale track from 1 to minScale and back, with a stagger of 0.12 seconds.

0.00
1.060.2400.380.75time (s)
  • Element 0, starts at 0.12 s
  • Element 1, starts at 0.24 s
  • Element 2, starts at 0.36 s

Exact rules ​

The specification defines how a track is sampled and the exact easing algorithm that every engine uses.

Released under the MIT License.