An animator takes a value from here to there over time: the lamp level from 0 to 1 over 15 seconds at dusk, the balloon along its course for 170 seconds, a smile easing in. It is the third of the three ways a value changes, beside subscriptions and events (subscriptions.html). Two ideas carry most of the design:

The animators are provided by the tweenstack package.

Terms

Tick
One step of the world's time. The simulation steps, for example, 60 times a second.
Pure
An animator whose value at any moment is a formula of the time since it started: a fade, keys, a ride along a curve. Two machines that agree on its start moment agree on everything.
Stepped
An animator that remembers where it has got to and moves on from there each tick: a chase following a moving target, or a flock. It is state. A world writes its own with stepAnimator (below).
Easing
How progress from 0 to 1 is spread over the duration: linear moves the same amount each second; an S-shaped ease starts slowly, is quickest in the middle and ends slowly.
Clock
Every duration names the clock it counts: sim(0.4 s), simulation time, which can pause or slow under load and is meant when none is named; world(15 min), the world's day and calendar, which runs as fast against simulation time as the world's day length says (Values: the day length); real(0.2 s), the person's own time, only for comfort and interface.

How you write it

Build, start, stop

let ride = along(envelope.place, spine: course, over: sim(170 s), ease: ends(sim(20 s)), face: byPoint(views))
...
on(clicked(cap), {
  if !riding { set(launched, now(sim)); start(ride); }
})

let h = start(transition(opacity, to: 0, over: sim(6 s), delay: sim(14 s)))
stop(h)
on(ended(h), ...)

An animator is named with let, as in Cone. The handle start returns may instead be kept in a field, set(hailing, start(...)), so a handler can test it and wait for it; both are in use. Whether a one-word "build and start at once" is worth adding as sugar is left for later.

The kinds

KindFunctionPure?What it does
Transitiontransition(target, to:, over:, ease:, from: current); on a pose, also lift: fn(s: fraction) metersyesFrom the value it has when it starts (or a given from) to to, over a duration, eased (cubic in-out when ease: is left out and it does not repeat). A pose's place and rotation move together (below). The target may also be a record whose fields are all numbers, colors or places (the weather's weatherState, Values: records): each field fades together, in a straight line, with the same eased progress.
Keyframeskeys(target, [key(time, value, ease), ...], loop: once | loop | pingpong)yesThrough a list of values at times.
Along a curvealong(target, spine:, over:, ease:, face: forward | none | byPoint(views))yesA place carried along a curved line in space, a spine (Appendix: shape tools), at one speed by arc length (the ease shapes the ends only), optionally turning as it goes. The spine's points are offsets from the target's rest place, in its parent's frame. byPoint(views) is a list with one entry for each of the spine's points: a flat point (x, z) to face there, or None for "as it stands".
Clipclip(name, on: body, mask: bones([...]), fadeIn:, fadeOut:)yesA gesture or gait played on a pose, over some bones.
Chasechase(target, toward: expr, speed: rate(amount, per: d)) or settle: rate(share, per: d)no: it remembers where it got toFollows a target that may move.
Springspring(target, to:, stiffness:, damping:)while its target is fixedA damped spring towards a goal, carrying its speed when the goal moves.
Your ownstepAnimator(target, step: fn(target, dt: duration) bool)no: it is stateCalls a function the world writes on every tick, with the tick's length; the function calculates and assigns (below).

The six named kinds are the library's ready-made animators. A world that needs something they do not do writes its own with stepAnimator, the same mechanism the library's stepped kinds use. Chase and spring are step functions the library ships, and a world could write either itself.

Your own animator: a step function

part flock, lives: client
  field birds: List[bird] = startFlock(seed: 7), mutable: true
  on(created, start(stepAnimator(birds, step: flockStep)))

fn flockStep(birds: List[bird], dt: duration) bool {
  each b in birds {
    b.velocity = steer(b, birds, ground: land, dt)
    b.place = b.place + b.velocity * dt
  }
  true                                   // keep going; false ends the animator
}

stepAnimator(target, step:) builds an animator like any other. It is started, stopped and replaced in the same way, and raises the same events. On every tick of the simulation it calls step with its target and dt, the tick's length. The step reads whatever it needs, assigns new values, and returns true to carry on or false to end (ended(h) then gives the reason "finished").

Timing, on every animator

OptionMeaning
delay: dWait d before beginning. Nothing is written meanwhile; the value keeps what it had.
repeat: n or repeat: foreverRun n times (fractions allowed), or for ever.
direction: normal | reverse | alternateForward, backward, or forward and back on alternate passes.
holdWhat shows outside its active time (see below).
rate: rIts own clock's speed: 2 runs a 2 s transition in 1 s.
begin: momentStart at a stated moment, which may be in the past: the lamps' begin: dusk.due. A moment in the future makes the animator wait, holding its starting value (nothing is written meanwhile), until that moment.
ease:The easing (below). Left out, a repeating animator is linear and any other is cubic in-out, chosen once when it starts.

Composition

together([a, b, c])            // all at once; ends when the last ends
sequence([a, b, c])            // each starts when the one before ends
stagger([a, b, c], lag: d)     // each starts d after the one before
one_of([a, b, c])              // starting one stops the others

Each takes its animators as one list.

A composed animator is an animator, so groups nest and take every timing option.

Easing

linear; smoothstep, an S-shaped ease, 3s2 - 2s3 of the progress s, flat at both ends (the props' glide home); ease(family, in | out | inOut) over the standard families (quadratic, cubic, quartic, quintic, sine, exponential, circular, back, elastic, bounce); bezier(...); steps(n), which jumps instead of gliding. The ride's ends(d) is an easing of its own: see "End ramps" below.

The default. When ease: is left out, a repeating animator (more than one pass, or for ever) is linear and any other is cubic in-out, chosen once, when it starts, so it does not depend on the order the options were written in. A member of a repeating group is judged by its own repeat.

A pose: place and rotation together

A part's pose, its place and its rotation (Values: rotations and poses), may be moved as one value. A transition on it eases both with the same progress: the place along the straight line between, the rotation the shortest way round. lift adds a height to the place as the progress s goes from 0 to 1, so a thing can rise over what lies between and come down where it belongs.

start(transition(p.pose, to: p.home, over: sim(1.4 s), ease: smoothstep,
                 lift: fn(s: fraction) meters { 0.7 m * sin(180 deg * s) }))

At eased progress s, the place is lerp(from.place, to.place, t: s) + (0, lift(s), 0) and the rotation mix(from.rotation, to.rotation, s). The props' glide home rises 0.7 m at its middle and lands with no lift, in 1.4 s. A transition is started per part, so a rule over many parts starts one for each that should move (each p in ...), and the others stay as they are (Physics: bowling on the lawn). Started on a body's pose, it takes the body out of the physics world until it ends, and the body is then asleep where it was left (The tick, and physics).

What it means

One value per name, the newest taking over

An animated value is a cell that formulas read. Its motion changes it each tick; when no motion is running it holds its value, its starting value or wherever its last motion left it. A flash that goes up and comes back rests at 0 because its motion ends there. There is no hidden stored value under a shown one.

Within one value, one motion at a time. If dawn's fade starts on lampLevel while dusk's is still running, dawn's takes over from wherever the level has got to. Each switch starts a linear transition on the lamp level's one field, and the newest takes over. Two motions meant to act together are two values and a formula: a flicker and a flash on one light are flicker and flash, summed by derive(light.intensity, clamp(... * gain + flicker + flash, within: 0 ..= maxIntensity)) (an illustration; subscriptions.html). A flash that starts while the flicker runs does not interrupt it, because they are different values; a second flash on flash takes over from the first.

The rule is precise:

"The newest takes over" is not always what a world wants. The hail ignores a second press during a hail; the ride ignores a click during the ride; a spell starts afresh rather than from the current value; the cap stays down 0.25 s after the latest click. The examples write each exception as a guard in the handler (if hailing == None { ... }, if !riding { ... }). The cap's case turns out to need none: a click during the dip starts a new dip, which takes over and holds the cap down 0.25 s from that click. Whether the rule is a default the author overrides with a guard, or needs a stated alternative on the animator, is open (see Open questions).

Starting in the past

An animator may begin at a moment already gone: begin: dusk.due. A pure animator started that way is sampled at the time since that moment, so a machine that reads the dusk message late, or a visitor who arrives mid-fade, agrees with one that was there. One already past its end writes its end value and ends. No event of the skipped time is raised (a "started" event is, now). Whatever it reads from the value when it starts (a transition's from, when none is given) is the value at the call.

A moment in the future is the other way: the animator waits, holding its starting value (what the value had when it was started), and begins at that moment. The island's dusk and dawn fades use the past case (begin: dusk.due) and are timed from the moment the message was for.

A stepped animator (a chase, or a group holding one) cannot be replayed from a moment it was not stepped through: its value depends on the targets it was given along the way. It starts now, whatever begin says. A spring is pure until its goal moves, so it is sampled in the past like a transition.

For example, a linear 0 to 10 over 10 s started at 15 s and first seen at 20 s shows 5.

The clock running backward

Time is in the language, and a clock can be paused, run fast, or set back. When the world's clock is set back:

Separately, one animator's own rate may be negative, which runs that animator backward while the world's clock runs forward.

Two kinds of chase: speed and settle

A chase follows a target that may move, so it remembers where it got to. Both kinds are used:

SpellingMotionExample
chase(gain, toward: if near && lampLevel > 0 { nearGain } else { 1 }, speed: rate(nearGain - 1, per: sim(0.4 s)))Steady rate. Moves towards the target by a fixed amount per time and stops on it, never past it. A full swing from 1 to nearGain, 1.45, takes 0.4 s; a visitor inside for only 0.2 s takes the gain to about 1.22 before it turns back.A lantern's nearness gain, whose goal moves when nearness or the lamp level changes it.
chase(smile, toward: if hailing != None { 0.85 } else { 0 }, settle: rate(99%, per: sim(0.35 s)))Settling. Closes a share of the remaining gap in each stated time: quick at first, slow at the end. From 0 it is at about 0.54, 63% of the way, after 0.076 s, and within 0.01 of 0.85 after 0.35 s.A smile; a sound's shelter, settle: rate(63%, per: sim(0.8 s)).

A steady-rate chase stays registered on its target, ready for the target to move, unless it is told to end on arrival. A chase's toward: is an expression: the engine gives it the target's current value each tick.

Along a curve: arc length, end ramps and a heading curve

The balloon's ride is a demanding animator: the envelope's place carried through twelve points over 170 s, from rest, speeding up over the first 20 s, steady between, slowing to rest over the last 20 s, turning to face each point's chosen view, and landing on its spot. It is one along animator, begun at the press's moment. What it needs:

Asking an animator

A built animator is a value of the type animator, so a field may hold it (field ride: animator = along(...)) and two rules may use the one. It answers two questions:

CallTypeWhat it answers
ride.length()durationhow long it runs, repeats not counted: the ride's 170 s
ride.timeAt(along: meters)durationthe time since its start at which an along animator reaches that distance along its spine, with the ease taken into account (the summit, about 70 s in)

A distance along is got from the spine. A spine built with pointNames: [(summit, 5)], a list of a name and the number of a point (the first is 0), answers course.pointNamed(summit), the distance along at that point. indexAt and alongAt turn a distance into a point's number and back (Shape tools).

Hold

hold says what shows after an animator ends: its end value, or nothing. For a member of a group it says what shows before it starts and after it ends (nothing, the end, the start, or both), and what a lone animator writes during its delay; a lone animator that ends always leaves its end value, with no hold, commit or vanish option. That reading is the one consistent with one value per name: a value with nothing showing would be a hidden layer.

Events of an animator

ended(h) and marker(h, name) are triggers like any other (events.html). An animator raises four: started; ended, with a reason (finished, stopped, or superseded by a newer animator); repeated, with the number of passes completed; and marker, with the marker's id and the clip time. A marker on a clip lets a handler act at a named moment, as a sword jab might test its blade at the clip's mark 0.25 s in (an illustration).

What a step may do

A step function is ordinary ConeScript: it calculates, loops and assigns. Three things are fixed about it:

An animator in a region that is asleep sleeps with it and runs again when the region wakes. The time it slept is run as the ticks it missed. (Proposal; whether a long sleep should instead be skipped is open below.)

The tick, and physics

Proposed order, each tick: events are handled (they may start or stop animators); animators are stepped and write their values, a pure one sampled at its time since start, a stepped one moved by the tick's length; subscriptions whose inputs changed are recomputed in dependency order; physics steps, moving animated parts that are not bodies (a lift, a door) as things the script moves, pushing others and not pushed.

An animator on a body's pose takes the body out of the physics world while it drives it: the body is moved by the animator alone and touches nothing, so a pin gliding home passes through a pin still standing. When the animator ends, or is stopped, the body is simulated again where it was left, with no speed, asleep (Physics: bodies).

What crosses the network

A pure animator is fixed by its parameters and its start, so in principle it can be sent once and sampled identically on every machine; a stepped one must be stepped on every machine from the same inputs, or streamed. Network cost is never written in the content: names follow meaning only (transition goes to a value, chase follows something that moves). To start with, the server runs the animators and sends their results.

How it connects

Open questions

← Actions | Contents | Package behaviors →