Skip to content

Custom indicators ​

A custom indicator is a JSON spec that you write. Every LoaderKit engine reads it and draws it natively, with no new native code. This page builds one step by step. Each step shows the full spec and a live preview. Drag the slider under a preview to scrub through one cycle.

Experimental

Writing your own spec is experimental. Until the schema is declared stable, a minor release may change schema version 1. Built-in indicators and their params are not affected.

Concepts ​

TermMeaning
BoxThe square the indicator is drawn in. Its side is the smaller edge of the view. All lengths are fractions of this side, from 0 to 1. The origin is the top-left corner and y points down.
LayoutPlaces elements in the box: single, stack, row, grid or ring. See Layouts.
ShapeWhat each element draws: circle, rect, ring, triangle or line. See Shapes.
TrackHow one property (scale, opacity, rotate, ...) changes over one cycle, with keyframes. See Tracks.
CycleOne loop of the animation. duration is its length in seconds.
StaggerA start delay per element, so elements move one after another. See Timing.
ParamsNamed numbers that users can override, such as count. See Params.

Angles are in radians (a full turn is 6.283185307) and a positive angle turns clockwise. Times are in seconds.

Step 1: one dot that pulses ​

The smallest useful spec has a name, a duration, a layout, a shape and one track.

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "FirstDot",
  "duration": 1,
  "layout": { "type": "single", "size": 0.5 },
  "shape": { "type": "circle" },
  "tracks": [
    { "property": "scale", "keyTimes": [0, 0.5, 1], "values": [1, 0.4, 1] }
  ]
}
null

What each field does:

  • schemaVersion is always 1 for now.
  • duration: 1 makes one cycle last 1 second.
  • layout is single: one element in the middle of the box. size: 0.5 makes it half the box.
  • shape is circle: the element draws a filled circle.
  • The track animates scale. At the start of the cycle (keyTimes 0) the scale is 1, in the middle (0.5) it is 0.4, at the end (1) it is 1 again. Between keyframes, the value moves in a straight line.

The $schema line is optional. It lets your editor check the file and suggest fields. See JSON Schema.

Step 2: three dots in a row ​

Change the layout to a row of 3 elements with a gap of 0.1 between them. The row fills the width of the box, so each dot is (1 - 2 × 0.1) / 3 wide.

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "FirstRow",
  "duration": 1,
  "layout": { "type": "row", "count": 3, "gap": 0.1 },
  "shape": { "type": "circle" },
  "tracks": [
    { "property": "scale", "keyTimes": [0, 0.5, 1], "values": [1, 0.4, 1] }
  ]
}
null

The track applies to every element, so the three dots pulse together.

Step 3: one after another ​

stagger delays each element. With { "each": 0.15 }, element 0 starts at 0 seconds, element 1 at 0.15 seconds and element 2 at 0.3 seconds.

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "FirstStagger",
  "duration": 1,
  "layout": { "type": "row", "count": 3, "gap": 0.1 },
  "shape": { "type": "circle" },
  "stagger": { "each": 0.15 },
  "tracks": [
    { "property": "scale", "keyTimes": [0, 0.5, 1], "values": [1, 0.4, 1] }
  ]
}
null

Before an element starts, it shows its rest values (scale 1, opacity 1). See Rest values.

Step 4: smoother motion ​

Add easing to slow the motion down near each keyframe, and a second track that fades the dots as they shrink. A list of tracks can animate each property at most once.

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "FirstEasing",
  "duration": 1,
  "layout": { "type": "row", "count": 3, "gap": 0.1 },
  "shape": { "type": "circle" },
  "stagger": { "each": 0.15 },
  "tracks": [
    { "property": "scale", "keyTimes": [0, 0.5, 1], "values": [1, 0.4, 1], "easing": "easeInOut" },
    { "property": "opacity", "keyTimes": [0, 0.5, 1], "values": [1, 0.3, 1], "easing": "easeInOut" }
  ]
}
null
0.00
1.050.3500.51time (s)
  • Element 0, starts at 0 s
  • Element 1, starts at 0.15 s
  • Element 2, starts at 0.3 s

The timeline plots the value of a track over one cycle for each element. The offsets between the curves are the stagger.

Step 5: let users change it ​

Declare params with their default values, then use { "$param": "name" } in place of a number. Users can now ask for 5 dots, or a deeper pulse, without a new spec.

json
{
  "$schema": "https://maitrungduc1410.github.io/loader-kit/schema/v1.json",
  "schemaVersion": 1,
  "name": "FirstParams",
  "duration": 1,
  "params": { "count": 3, "minScale": 0.4 },
  "layout": { "type": "row", "count": { "$param": "count" }, "gap": 0.1 },
  "shape": { "type": "circle" },
  "stagger": { "each": 0.15 },
  "tracks": [
    { "property": "scale", "keyTimes": [0, 0.5, 1], "values": [1, { "$param": "minScale" }, 1], "easing": "easeInOut" },
    { "property": "opacity", "keyTimes": [0, 0.5, 1], "values": [1, 0.3, 1], "easing": "easeInOut" }
  ]
}
null

Use it in an app ​

Load the JSON with the engine of your platform. See Using a spec for each platform.

html
<loader-kit id="dots" params='{"count": 5}'></loader-kit>
<script type="module">
  import '@loader-kit/web/element';
  document.querySelector('#dots').spec = await (await fetch('/first-params.json')).json();
</script>
kotlin
loader.spec = IndicatorSpec.parse(json)
loader.params = mapOf("count" to 5.0)
swift
loader.spec = try IndicatorSpec(json: json)
loader.params = ["count": 5]
csharp
indicator.Spec = json;
indicator.Params = new Dictionary<string, double> { ["count"] = 5 };

Write it in TypeScript ​

You can also write a spec in TypeScript with @loader-kit/spec. defineIndicator() checks the spec when it runs and fills in schemaVersion, and param() writes a $param reference. Serialize the result to get the JSON that every engine reads.

sh
npm install @loader-kit/spec
ts
import { defineIndicator, param } from '@loader-kit/spec';

export const Blink = defineIndicator({
  name: 'Blink',
  duration: 0.9,
  params: { count: 4, low: 0.15 },
  layout: { type: 'row', count: param('count'), gap: 0.08 },
  shape: { type: 'rect', cornerRadius: 0.25 },
  stagger: { each: 0.15 },
  tracks: [
    { property: 'opacity', keyTimes: [0, 0.5, 1], values: [1, param('low'), 1], easing: 'easeInOut' },
  ],
});

console.log(JSON.stringify(Blink));

defineIndicator() throws an InvalidIndicatorError that lists every problem. The 50 built-in indicators are written this way.

Where to next ​

  • Layouts: rows, grids, rings and stacks.
  • Shapes: circles, arcs, rings, rectangles, triangles and lines.
  • Tracks: keyframes and easing in detail.
  • Timing: stagger, durations, parts, group tracks and 3D.
  • Params: make a spec configurable.
  • Playground: edit a spec with live preview and validation.
  • Specification: the normative rules for every field.

Released under the MIT License.