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:
- An animator is built, then started. A named function builds it as a value that does nothing: it can be stored, grouped, or put in a sequence.
startputs it into the tick, the world's step of time, and returns a handle. - One value per name. An animator writes its value directly and leaves it where it ends. One motion per value at a time: the newest start takes over from wherever the value has got to, so nothing jumps. Several effects on one thing are separate values, combined by a formula the author writes.
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
| Kind | Function | Pure? | What it does |
|---|---|---|---|
| Transition | transition(target, to:, over:, ease:, from: current); on a pose, also lift: fn(s: fraction) meters | yes | From 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. |
| Keyframes | keys(target, [key(time, value, ease), ...], loop: once | loop | pingpong) | yes | Through a list of values at times. |
| Along a curve | along(target, spine:, over:, ease:, face: forward | none | byPoint(views)) | yes | A 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". |
| Clip | clip(name, on: body, mask: bones([...]), fadeIn:, fadeOut:) | yes | A gesture or gait played on a pose, over some bones. |
| Chase | chase(target, toward: expr, speed: rate(amount, per: d)) or settle: rate(share, per: d) | no: it remembers where it got to | Follows a target that may move. |
| Spring | spring(target, to:, stiffness:, damping:) | while its target is fixed | A damped spring towards a goal, carrying its speed when the goal moves. |
| Your own | stepAnimator(target, step: fn(target, dt: duration) bool) | no: it is state | Calls 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 is the engine's. The simulation ticks in fixed slices (Terms), whatever the frame rate, so
dtis always the same length. A slow frame runs several ticks; a fast machine draws several frames for each tick and smooths between the last two; a paused clock runs none. The content states nothing about this. Fixed slices are what make the result the same on every machine that starts from the same state, and what stop one long frame from becoming one huge jump (a bird through the ground, a spring flung apart). - The timing options (above) apply:
delay:,begin:andrate:as for any animator. A step function has no length of its own, sorepeat:anddirection:do not apply. - The flock on the island is this:
boidflock.flock(...)(Package behaviors) is the library's step function with its tuned numbers, started by the package. A world may use it, wrap it, or write its own flock.
Timing, on every animator
| Option | Meaning |
|---|---|
delay: d | Wait d before beginning. Nothing is written meanwhile; the value keeps what it had. |
repeat: n or repeat: forever | Run n times (fractions allowed), or for ever. |
direction: normal | reverse | alternate | Forward, backward, or forward and back on alternate passes. |
hold | What shows outside its active time (see below). |
rate: r | Its own clock's speed: 2 runs a 2 s transition in 1 s. |
begin: moment | Start 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:
- Ownership goes by the order of start calls. A start supersedes every earlier animator on the fields it writes, running or still waiting to begin, at the call. One that no longer writes anything ends with the reason "superseded"; a waiting one never starts.
- The new one starts from the value as it is then, so replacing one by another shows no jump. A spring also takes the speed the value had, from the last two ticks.
- A group that loses some of its values to a later start goes on writing the rest.
- An animator that has run its course writes its end value and leaves; the value stays.
"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:
- A pure animator goes back with it. Its clock moves back by the time times its rate: evaluated at the earlier moment, it shows what it showed then. A linear 0 to 10 over 10 s shows 2, 8, 3 and 5 at 2, 8, 3 and 5 s, in that order.
- One that had ended comes back if the moment is before its end, and ends again going forward, with a second "ended" event. One set back before its start is waiting again, and the value shows what it held before the animator took it.
- Only the value's latest animator is kept. Once another has started on the value, time set back to before that one shows the value as that one found it, not what an earlier, superseded animator showed.
- A stepped animator does not run backward. Its clock holds while time runs backward, and it goes on from where it is when time next moves forward.
- The jump back raises no event. It is a seek, not playback. Crossing a marker, a repeat or an end forward again raises it again.
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:
| Spelling | Motion | Example |
|---|---|---|
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:
- A curve that never overshoots. A centripetal Catmull-Rom curve never loops however unevenly its points are spaced.
spine.throughis a smooth curve, not a polyline: its length is measured from 64 samples a segment, a fixed number, and the position at a distance is found from that table. Curves available are uniform and centripetal Catmull-Rom and Bezier chains, and polylines. For example, through (0,0,0), (10,0,0), (10,0.5,0), (0,0.5,0), half way along the middle span the uniform spline swings out to x = 11.25, the centripetal one to 10.102157. - Steady speed by arc length. Moving by the curve's parameter runs fast where points are far apart; moving by arc length runs at one speed.
- End ramps.
ease: ends(sim(20 s)): the speed rises as 1 − cos from rest over the first 20 s, is steady, and falls the same way over the last 20 s. The ramps are fixed durations, not fractions, so lengthening the ride lengthens only the steady part. For example, over 10 s with 2 s ramps, progress is 0.125 at 2 s, 0.5 at 5 s, 0.875 at 8 s. - A heading curve.
face: byPoint(views):viewshas one entry for each waypoint, a flat point to face (the heading is the direction from the waypoint to it) orNone, which keeps the heading the part has as it stands. Each heading is taken the short way round from the one before, and between waypoints a Catmull-Rom curve runs through them, so a rider can look at a view while the path goes elsewhere. The heading is a second value the same curve position drives, not a second animator. Headings 0.1, 3.0 and −3.0 radians unwind to 0.1, 3.0 and 3.283185. - The frame of the course. The spine's points are offsets from the part's rest place, in its parent's frame: the first point is usually (0, 0, 0), and the part's place at any moment is its rest place plus the curve's point. The heading it gives replaces the part's own rotation while the ride runs.
- The whole tree goes with it. An animator on a parent's place moves every child: parts, platforms, areas, and the physics bodies of the platforms (a platform is a kinematic body that carries its riders; an area moves with its part). Nothing is written for the children.
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:
| Call | Type | What it answers |
|---|---|---|
ride.length() | duration | how long it runs, repeats not counted: the ride's 170 s |
ride.timeAt(along: meters) | duration | the 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:
- What it writes is known. It may assign its target and anything inside it, and any other value the rules of who may change what allow (Events: who may change what). The compiler reads the assignments, so the engine knows every value a step can write without the author listing them. It uses this to order steps, to run unrelated ones side by side, to send only what changed, and to find two animators writing one value (one value per name, above).
- It sees one moment. Every step in a tick reads values as they stood at the start of the tick, and all of their writes land at its end. A step reads its own target's new values as it assigns them. Two steps never see each other's half-finished work, so neither depends on which ran first.
- It does not wait. A step finishes within its budget, like every block (Control flow). Anything slow, such as asking something outside the world, is started by the step and does not return inside it: the answer arrives later as an event, which a rule handles (a direction for the design of input and output, not yet written).
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
- Events: what starts and stops animators;
endedandmarker. - Subscriptions: the formulas that read animated values.
- Values: moments, durations and clocks.
- Package behaviors: a gait, a mind or a face started as an animator.
- Frames:
alongon a part's place, and physics. - The balloon ride: the worked example.
Open questions
- "Newest takes over" against "ignore a second press": (a) the rule is a default the author overrides with a guard, as the examples write it; (b) an alternative is stated on the animator. A second trigger can be met in several ways (events.html).
- Animators before or after physics: before, as proposed (a door or a lift moves, and physics answers in the same tick); after (an animator can follow a body's new place); or per animator.
%means two things: a share insettle: rate(99%, per: sim(0.35 s)), and Cone's remainder operator. Not yet reconciled.- Two writers of one value:
alongon a being's place while its walking also writes it. (On a body's pose the rule is proposed: the animator takes the body out of physics, above.) - The first library: the six kinds only, or named presets as well (an entrance, an emphasis, an exit). With
stepAnimator, a preset can be a library step function rather than a new kind. - A long sleep: whether a
stepAnimatorin a region that wakes after hours should run every missed step, run a capped number, or be told the gap once and catch up itself. - Pure step functions: a pure kind (a transition) can be sampled at any moment, which a step function cannot. Whether a world can also write its own pure kind, as a function of the time since start, is not yet written.
- Easing:
steps(n)is in the notation and is to be built in the package (the cap's dip is one step). holdafter the end: the notation allows "nothing"; the package always leaves the end value (above).- Names and handles: an animator named with
let, or the handle kept in a field. trail, for Ariadne's thread, stateful like a chase: now a step function a world or the library can write withstepAnimator, not a seventh kind.- Keys whose values are lines of speech, and steps as time ("four steps" had to become
sim(0.067 s)). - Which clock a view's own smoothing counts, such as the sound's shelter.
- Behaviors as animators (behaviors.html).