Players, events and layers

A player plays a sealed clip with its slots bound to targets, by Web Animations' procedures on the host's clock. Players report what they cross as event records, and layer over each other by composite operations and weights.

Contents

Playing

mtwnTargetId slide;
mtwnResult playing = mtwnCreateTarget(context, mtwn_valueScalar, (mtwnVec4){0}, &slide);
mtwnBinding bindings[2] = {mtwnMakeBinding(0, opacity), mtwnMakeBinding(1, slide)};
mtwnClipPlayerDef play = mtwnDefaultClipPlayerDef();
play.clip = clip;
play.bindings = bindings;
play.bindingCount = 2;
play.keep = true; // kept when finished, to play again
mtwnPlayerId intro;
playing = mtwnCreateClipPlayer(context, &play, &intro); // plays from the next update
playing = mtwnClipPlayer_Pause(context, intro);
playing = mtwnClipPlayer_Seek(context, intro, 0.2);
playing = mtwnClipPlayer_SetRate(context, intro, -1.0); // backwards
playing = mtwnClipPlayer_Play(context, intro);

A player plays, pauses, reverses, finishes, cancels and seeks by time, by progress (mtwnClipPlayer_SeekProgress) or to a label of the clip's root (mtwnClipPlayer_SeekLabel). A play that needs a start time takes the next update's, as Web Animations' pending play waits for the ready time; pause, seek, finish and cancel take effect at once. mtwnClipPlayer_GetState reads its time, rate, duration and play state (idle, running, paused, finished).

A player not kept commits what it fills forward as its targets' bases when it finishes, and frees itself, its id stale. A kept player stays, finished, to be played again. A slot the bindings leave out plays on nothing.

A rate that eases

A rate set each frame eases a player, as tweening a GSAP timeline's timeScale slows it: a transition on a scalar target runs the rate, and after each update the host gives the player what it reached. The player keeps its time at every change, so it slows without a jump and holds at 0. A moving player takes a new rate at the next update, as Web Animations' seamless change does, so it runs a frame behind the transition:

// After each update: the rate the speed transition has reached.
mtwnVec4 rate;
made = mtwnTarget_GetValue(context, speed, &rate);
made = mtwnClipPlayer_SetRate(context, player, (double)rate.x);

Where it stops then depends on the frame rate: a falling rate overshoots by about a frame and a half at its start speed (2.5 of 133.3 at 60 frames a second, 1.0 at 144). A program that replays or keeps hosts in step uses the exact form instead: the player paused where the ramp starts and swept each frame to the time the eased speed has travelled, t0 + r0 t + (r1 - r0) d E(t / d) for a rate eased from r0 to r1 over d by an easing whose integral is E. For 1 to 0 over a second by a quadratic out, E(u) is (1 - (1 - u)^3) / 3:

// Exact at any frame rate: paused where the ramp starts, swept
// each frame to the time the eased speed has travelled.
double u = fmin(seconds - rampStart, 1.0);
double swept = rampTime + (1.0 - pow(1.0 - u, 3.0)) / 3.0;
made = mtwnClipPlayer_Sweep(context, player, swept);

Scroll-driven playback

A paused player swept to the scroll's progress before each update plays as CSS's scroll timelines do. mtwnClipPlayer_Sweep crosses everything between, so marks and starts give their records as they are scrolled past, either way. To smooth it, as GSAP's scrub: 0.5 does, the playhead chases the scroll on a spring the host retargets at each scroll event, from where it is with the velocity it has, so a new event never jolts it:

// At a scroll event: the chase from where it is, toward the new progress.
double offset = 0.0;
double speed = 0.0;
mtwnResult chased = mtwnSampleSpring(&chase, away, pace, now - since, &offset, &speed);
away = goal + offset - scrolled; // the same place, against the new goal
pace = speed;
goal = scrolled;
since = now;
// Each frame, before the update: the paused player swept to the chase.
chased = mtwnSampleSpring(&chase, away, pace, now - since, &offset, NULL);
mtwnResult swept = mtwnClipPlayer_Sweep(context, reader, (goal + offset) * length);

Events

What a player's time crosses between updates is recorded exactly, however far it moved: a track's or timeline's start and end, each iteration, each mark, and the player's own seeks and cancels. A count of 2^64 or more is UINT64_MAX. An endless node whose iteration passes the largest double is held there, as mtwnComputeTiming holds it, and its values stand still: between two times past it nothing is crossed. A transition records its start when its delay ends and its end when it commits. Each record says when it happened in host time and in clip time, how many times it was crossed, which way, and in which iterations. The count is exact whatever the iteration start; the iterations it names are what mtwnComputeTiming reports, which past 2^53 is the nearest double.

mtwnEvent events[32];
uint32_t count = 0;
uint64_t dropped = 0;
mtwnResult read = mtwnContext_ReadEvents(context, events, 32, &count, &dropped);
for (uint32_t i = 0; i < count; ++i)
{
    if (events[i].kind == mtwn_eventMark && events[i].key == 7)
    {
        // Play the sound: crossed events[i].count times since the last read.
    }
}

Records are never callbacks: they wait in the context's queue until the host reads them, sorted by host time, then the players' creation order, then the clip's order. An update makes at most one record per player, node or mark, kind and direction, with the count of crossings, so a clip that loops a thousand times in one frame costs one record.

Layers

Players write their targets in the order they were made. A binding replaces what lies under it or adds to it, at a weight, so one clip can play over another: an idle loop with a hit added, or two clips cross-faded by their weights.

mtwnBinding layer = mtwnMakeBinding(0, opacity);
layer.composite = mtwn_compositeAdd; // added to what lies under it
layer.weight = 0.5f;
play.bindings = &layer;
play.bindingCount = 1;
mtwnPlayerId pulse;
mtwnResult layered = mtwnCreateClipPlayer(context, &play, &pulse);
layered = mtwnClipPlayer_SetWeight(context, pulse, 0.25f); // the whole player's weight

What lies under a player is what earlier players wrote in this update, or the target's base, so an added layer never compounds on its own last frame. A target bound to a clip's player that no player writes in an update shows its base.

Not obvious