On this page:
1.2.1 Time Boxes and Durations
1.2.2 Epochs
1.2.3 Pad versus Sustain
8.12

1.2 Animated Picts🔗ℹ

An animated pict is based a function that takes a time t and produces a static pict to draw a that point in time. In the same way that a static pict needs a bounding box to determine how it draws in combination with other pictures, an animated pict needs a time box that determines how it synchronizes with other animated picts. A time box’s duration is analogous to a bounding box’s width or height.

See Using Slideshow in Rhombus for examples of how animated picts work with slide presentations.

1.2.1 Time Boxes and Durations🔗ℹ

A static pict can be used as a animated pict of duration 1 whose rendering is the static pict during its time box. An animated pict’s rendering is not constrained to its time box, however; just like a static pict can potentially affect any pixel outside its bounding box in an infinite drawing plane, an animated pict has a rendering for all time before and after its time box. A static pict’s rendering outside of its time box is a ghosted version of the static pict (in the sense of Pict.ghost).

Using the static rectangle pict rect from before, the following diagram shows its rendering at four different points on a timeline, where t is relative to the pict’s time box, and the time box is shown in pink on the timeline. Nothing is drawn under t = -1, t = 1, or t = 2 because the pict is ghosted at those times.

image

An animated pict’s rendering in time can be shifted relative to its time box, just like a static pict’s drawing can be shifted relative to its bounding box. The Pict.time_pad function extends time box on either end, which can shift the pict’s drawing relative to the start of its time box. Let’s shift rect to define late_rect:

def late_rect = rect.time_pad(~before: 1)

Here’s the rendering of late_rect over time, which shows that its duration is 2, but it doesn’t become unghosted until relative time t = 1:

image

Operations like beside, stack, and Pict.pad operate on all picts, both static and animated. Placing an animated pict beside another corresponds to rendering the two picts concurrently and placing each rendered form beside the other. Padding an animated pict means padding every rendering that is generated by the animated pict over time to produce a new animated pict—one with the same duration as the unpadded original.

Time-box padding plus composite operations like stack allow picts to be staged in an animation. If we pad circ’s time box on the “after” side to define early_circ, then early_circ and late_rect have the same timeboxes, and combining them with stack makes a composite pict ping_pong:

def early_circ = circ.time_pad(~after: 1)

def ping_pong = stack(early_circ, late_rect)

Since we padded circ with ~after and rect with ~before, the circ and rect components of ping_pong are unghosted at different times—but their ghosts preserve the space that they will sometimes fill:

image

We could have used overlay instead of stack to have the two shapes overlap, and then a circle would appear to change to a square in place:

def together_apart = overlay(early_circ, late_rect)

The combined picture would have a bounding box based on combined bounding boxes, since that what overlay does. In this picture, we show each image with a little padding and then a rectangle for the rulting bound box:

image

The switch function, in contrast, takes care of shifting its arguments to make them sequential, and it stops using each pict at the end of its time box and start using the next. That way, we don’t have to shift manually, and the bounding boxes are independent.

def one_at_a_time = switch(early_circ, late_rect)

image

Whether overlay or switch is the right choice depends on the context. The sequential function can help with shifting time boxes to make picts sequential without otherwise combining them.

1.2.2 Epochs🔗ℹ

We have not yet defined the units of time t for an animated pict. Animated picts are meant to represent slide presentations, where we want a notion of time that is compatible with both slide-based stepping and real-time animations to transition between slides. Thus, an increment of 1 in t corresponds to advancing the slide, and increments between 0 and 1 are transition points from one slide to the next. More abstractly, we refer to each increment of t by 1 as an epoch. A pict’s duration is always measured in integer epochs.

When an epoch advances (i.e., when the presenter hits the space bar to advance the slide), then an epoch-specific animation function receives a sequence of numbers between 0 and 1 to represent the intermediate transition points between the old slide and the new one. The rate at which those intermediate results are used is based the epoch’s extent, which is measured in seconds. An animated pict can have a different extent for each of its epochs. A static pict’s epochs all have extent 0, meaning that it doesn’t need to draw any transitions on the way out of the epoch.

Suppose that we have a pict whose duration is 3 epochs and the epochs have extents 0.5, 1.0, and 0.5 seconds, respectively. If we stretch that pict’s conceptual timeline to match real time, then it might look something like this, dependeing on how fast the presenter advances each slide:

image

The red dots to start epochs become ovals to represent a slide at rest. The right end end of a red oval is the start of a transition to a new slide, and the new slide arrives fully at the left edge of the next red oval. The space between between red ovals for epoch 1 is twice as large as the other spaces, because that epoch’s extent is twice as long. During that middle animation, when the conceptual time is t = 1.5, the animation function associated with the epoch receives the epoch-relative number 0.5 (not a time-box-relative 1.5).

To create an animated picture with a duration of 1 epoch and a non-0 extent for that epoch, use the animate constructor:

def fade_out = animate(fun (n): circ.alpha(1-n))

The fade_out pict’s one epoch in this example uses the default extent of 0.5 seconds, but an extent could have been specified with an ~extent argument. This pict’s animation is illustrated by the following conceptual timeline, which shows the circle solid during its slide pause, fading afterward to the next slide, and ghosted outside its time box:

image

We can combine a fade-in animation with a fade-out animation, and then shift the pict’s time box so that it corresponds to the point where the pict is fully faded in.

def fade_in = animate(fun (n): circ.alpha(n))

def fade_in_out = switch(fade_in, fade_out).time_pad(~before: -1)

image

Functions like sequential and Pict.epoch_set_extent support splicing within-epoch animations and fine-grained control over epoch extents.

1.2.3 Pad versus Sustain🔗ℹ

Suppose that we want our circle pict to appear first, and then our rectangle pict, but we want the circle to stick around. We could use switch to combine two instances of circ, and then combine that with late_rect:

def long_circ = switch(circ, circ)

def together = stack(long_circ, late_rect)

image

This strategy is not quite right in general, however. If we use fade_in_out instead of circ, then the circle first fades out, and then it suddenly reappears with the square:

def two_fade = switch(fade_in_out, fade_in_out)

def together_try = stack(two_fade, late_rect)

image

Instead of this behavior, we want to insert epochs before the current last epoch, where each inserted epoch has a static pict matching the beginning of the last epoch. This sustain transformation of an animated pict is needed so often that it is part of the definition of an animated pict; sustain a pict using Pict.sustain, and the details of what sustain means for a pict can be customized in an option to animate. The default in animate is not what we want in this case, so we’ll create a variant of fade_out that sustains at the desired side of the animation:

def fade_out2 = animate(fun (n): circ.alpha(1-n),

                        ~sustain_edge: #'before)

def fade_in_out2 = switch(fade_in, fade_out2).time_pad(~before: -1)

In fact, sustaining an animated pict is the right choice so often that it is the default way that operations like overlay and stack extend the duration of each pict to match others in the combination. In other words, we don’t have to manually force fade_in_out and late_rect to have the same duration, and stack will sustain fade_in_out to make its duration match late_rect:

def together_fade = stack(fade_in_out2, late_rect)

image

While sustaining is usually the right choice for combining animated picts, functions like overlay and stack accept a ~duration optional argument to select a mode. The argument can be #'pad instead of #'sustain to normalize durations by padding instead of sustaining. Sometimes, the choice is better associated with a pict than at a combination, and pict can be made nonsustaining through Pict.nonsustaining, which causes a sustain on the pict to be the same as time padding.