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
| Term | Meaning |
|---|---|
| Box | The 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. |
| Layout | Places elements in the box: single, stack, row, grid or ring. See Layouts. |
| Shape | What each element draws: circle, rect, ring, triangle or line. See Shapes. |
| Track | How one property (scale, opacity, rotate, ...) changes over one cycle, with keyframes. See Tracks. |
| Cycle | One loop of the animation. duration is its length in seconds. |
| Stagger | A start delay per element, so elements move one after another. See Timing. |
| Params | Named 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.
{
"$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] }
]
}FirstDotOpen in playground What each field does:
schemaVersionis always1for now.duration: 1makes one cycle last 1 second.layoutissingle: one element in the middle of the box.size: 0.5makes it half the box.shapeiscircle: the element draws a filled circle.- The track animates
scale. At the start of the cycle (keyTimes0) 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.
{
"$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] }
]
}FirstRowOpen in playground 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.
{
"$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] }
]
}FirstStaggerOpen in playground 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.
{
"$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" }
]
}FirstEasingOpen in playground - 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.
{
"$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" }
]
}FirstParamsOpen in playground Use it in an app
Load the JSON with the engine of your platform. See Using a spec for each platform.
<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>loader.spec = IndicatorSpec.parse(json)
loader.params = mapOf("count" to 5.0)loader.spec = try IndicatorSpec(json: json)
loader.params = ["count": 5]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.
npm install @loader-kit/specimport { 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.