On this page:
animate
animate_  map
Pict.duration
Pict.epoch_  extent
Pict.snapshot
Pict.time_  pad
Pict.sustain
Pict.nonsustaining
Pict.time_  clip
Pict.instantaneous
Pict.time_  insert
Pict.epoch_  set_  extent
Pict.epoch_  metadata
Pict.epoch_  set_  metadata
bend.fast_  start
bend.fast_  middle
bend.fast_  edges
bend.fast_  end
cross_  fade
magic_  move
Cross  Fit
Cross  Fit.none
Cross  Fit.scale
Noncommon  Mode
Noncommon  Mode.switch
Noncommon  Mode.fade
explain_  anim
8.16.0.4

5 Pict Animators🔗ℹ

function

fun animate(

  snapshot

    :: Real.in(0, 1) -> StaticPict

    || (Real.in(0, 1), ~deps: List.of(Pict)) -> StaticPict

    || (Real.in(0, 1), ~config: maybe(Map)) -> StaticPict

    || (Real.in(0, 1), ~deps: List.of(Pict), ~config: maybe(Map)) -> StaticPict,

  ~dependencies: deps :: List.of(Pict) = [],

  ~config: config = maybe(Map) = #false,

  ~extent: extent :: NonnegReal = 0.5,

  ~bend: bender :: (Real.in(0, 1) -> Real.in(0, 1))

           = bend.fast_middle,

  ~sustain_edge: sustain_edge :: TimeOrder = #'after

) :: Pict

Creates an animated pict. The snapshot function should accept a Real.in() and produce a StaticPict. When Pict.snapshot is called on the result p from animate, the supplied snapshot function is called. The pict produced by snapshot is wrapped as a sole child of a new pict whose identity is the same as p; see also Pict Findable and Replaceable Identity.

The deps list determines the replaceable dependencies of animate’s result, while a snapshot result determines the findable children. The snapshot function is called with deps as an optional ~deps argument, except that each pict in deps is potentially adjusted through Pict.rebuild or Pict.replace before it is passed back to snapshot. The config argument similarly provides a map that can be passed back to snapshot as a ~config argument, possibly adjusted through Pict.rebuild or Pict.configure.

The resulting pict’s duration is 1, and the extent of that duration is determined by the extent argument. Before the pict’s time box, its drawing and bounding box are the same as Pict.ghost(proc(0, & children)); after its time box, they are the same as Pict.ghost(proc(1, & children)).

The bender function adjusts the argument that is passed to animate. If bender is the identity function, then the number passed to proc ranges from 0 to 1 linearly with animation time. The default bend.fast_middle keeps the argument in the range 0 to 1, but it causes the argument to proc change more slowly near 0 and 1 and more quickly around 0.5.

If sustain_edge is #'before, then Pict.sustain for the resulting pict will extend its time box by adding an epoch to the start with the static representation Pict.ghost(proc(0)). If sustain_edge is #'after, then Pict.sustain instead adds to the end of the time box with the static representation Pict.ghost(proc(1)).

fun bar(n):

  rectangle(~width: 30, ~height: 10 + n*10, ~fill: "lightblue")

> explain_anim(animate(bar))

image

> explain_anim(animate(bar).sustain())

image

> explain_anim(animate(bar, ~sustain_edge: #'before).sustain())

image

function

fun animate_map(

  combine

    :: (Int, Real.in(0, 1),

        ~deps: List.of(StaticPict)) -> StaticPict

    || (Int, Real.in(0, 1),

        ~deps: List.of(StaticPict),

        ~config: maybe(Map)) -> StaticPict,

  ~deps: deps :: List.of(Pict) = [],

  ~config: config :: maybe(Map) = #false,

  ~nonsustain_combine:

    nonsustain_combine

      :: maybe((Int, Real.in(0, 1),

                ~deps: List.of(StaticPict)) -> StaticPict)

      || maybe((Int, Real.in(0, 1),

                ~deps: List.of(StaticPict),

                ~config: maybe(Map)) -> StaticPict)

      = #false,

  ~duration: duration_align :: DurationAlignment = #'sustain,

  ~epoch: epoch_align :: EpochAlignment = #'center

) :: Pict

Creates a new pict by mapping a static-pict combine function over snapshots (on demand) of the potentially animated picts deps. The combine function receives an Int epoch, a Real.in(0, 1) time within that epoch, and deps back as a ~deps argument. If combine accepts a ~config argument, then config is passed back.

The picts in deps might be replaced via Pict.rebuild or Pict.replace on the result of animate_map, in which case the replacements are passed back to combine as the ~deps argument. The optional config similarly can be updated via Pict.rebuild or Pict.configure before it is passed back as the ~config argument to combine.

If all picts in deps satisfy StaticPict and nonsustain_combine is #false, then the result also satisfies StaticPict. In that case, combine is called with 0s as its epoch and time arguments. Picts in deps can be replaced with static or animated picts, and the rebuild result of animate_map is accordingly static or animated.

When deps does not contain only StaticPicts, the deps are made concurrent via concurrent, passing along duration_align and epoch_align. The result pict’s duration is the maximum of the durations of the deps.

The combine function is used for all epochs of the result from animate_map. If additional epochs are added to the result via Pict.sustain, those epochs also use combine. If nonsustain_combine is a function, and if non-sustain epochs are added to the result of animate_map, then nonsustain_combine is used for those epochs.

> def sq = square(~size: 32, ~fill: "lightblue")

> def sq_grow = animate(fun (n): sq.scale(1, 1 + n))

> explain_anim(

    animate_map(~deps: [sq_grow],

                fun (i, n, ~deps: [sq :: Pict]):

                  stack(sq, text(str.f(sq.height,

                                       ~precision: 1))))

  )

image

property

property (pict :: Pict).duration :: Int

 

method

method (pict :: Pict).epoch_extent(i :: Int) :: Real

Properties for a pict’s animation. See Animated Picts for more information.

> text("Hello").duration

1

> text("Hello").epoch_extent(0)

0

> switch(text("Hello"), text("World")).duration

2

> def p = animate(fun (n): text("World").alpha(n))

> p.duration

1

> p.epoch_extent(0)

0.5

> switch(text("Hello"), p).epoch_extent(1)

0.5

method

method (pict :: Pict).snapshot(epoch :: Int = 0,

                               n :: Real.in(0, 1) = 0,

                               ~rebuild_prompt: rebuild_prompt = #true)

  :: StaticPict

Converts an animated pict to a static pict for the animation at time t = epoch + n.

If rebuild_prompt is #false, then a rebuilding operation on the result pict via Pict.rebuild is propagated to the original pict, and a snapshot of the rebuilt pict is taken for the overall rebuild result. If rebuild_prompt is a true value, then then a rebuilding operation rebuilds in the initial snapshot, instead of taking a snapshot of a rebuilt pict.

The result snapshot’s identity is the same as the identity of pict if rebuild_prompt is #true. If rebuild_prompt is #false, the result identity is fresh and it depends on pict, but the snapshot is also findable as pict. See also Pict Findable and Replaceable Identity.

> def p = animate(fun (n): text("World").alpha(1-n))

> p.snapshot()

image

> p.snapshot(0, 0.5)

image

> p.snapshot(0, 0.75)

image

method

method (pict :: Pict).time_pad(

  ~all: all :: Int = 0,

  ~before: before :: Int = all,

  ~after: after :: Int = all

) :: Pict

Returns a pict that is the same as pict, but with its time box adjusted.

> def p = circle(~size: 20, ~fill: "lightgreen")

> def q = p.time_pad(~after: 2)

> p.duration

1

> q.duration

3

> rectangle(~around: q.snapshot(0, 0.0))

image

> rectangle(~around: q.snapshot(2, 0.0))

image

method

method (pict :: Pict).sustain(n :: Int = 1) :: Pict

Similar to Pict.time_pad with ~after, but sustains instead of merely padding.

> def p = circle(~size: 20, ~fill: "lightgreen")

> def q = p.sustain(2)

> q.duration

3

> rectangle(~around: q.snapshot(2, 0.0))

image

method

method (pict :: Pict).nonsustaining() :: Pict

Returns a pict that is the same as pict, but where a sustain operation is treated the same as padding via Pict.time_pad.

> def p = circle(~size: 20, ~fill: "lightgreen").nonsustaining()

> def q = p.sustain(2)

> q.duration

3

> rectangle(~around: q.snapshot(2, 0.0))

image

method

method (pict :: Pict).time_clip(

  ~keep: keep :: maybe(TimeOrder) = #false,

  ~nonsustaining: nonsustaining = keep != #'after

) :: Pict

Returns a pict that is like pict, but whose drawing is confined to its time box in the sense that it is represented by nothing outside of its time box. If keep is #'before or #'after, then the pict is not clipped in that time direction. The resulting pict is nonsustaining (in the sense of Pict.nonsustaining) if nonsustaining is true.

> def p = circle(~size: 20, ~fill: "lightgreen")

> rectangle(~around: p.snapshot(0, 0.0))

image

> rectangle(~around: p.snapshot(2, 0.0))

image

> rectangle(~around: p.time_clip().snapshot(2, 0.0))

image

> rectangle(~around: p.time_clip().snapshot(2, 0.0)) is_a NothingPict

#true

method

method (pict :: Pict).instantaneous() :: Pict

Creates a pict of duration 1 that draws a snapshot of pict at the start of its time box and nothing before or after the start of the time box.

The result is the same as

Pict.time_clip(animate(fun (n): if n .= 0 | pict.snapshot() | nothing,

                       ~extent: 0))

> def p = circle(~size: 20, ~fill: "lightgreen")

> p.snapshot(0, 0.1)

image

> p.instantaneous().snapshot(0, 0.1)

image

> p.instantaneous().snapshot(0, 0.1) is_a NothingPict

#true

method

method (pict :: Pict).time_insert(epoch :: Int,

                                  n_epochs :: NonnegInt) :: Pict

Returns a pict that is like pict, but potentially with a longer duration. The result is pict itself if epoch is outside of pict’s time box or if n_epochs is 0.

When epoch is within pict’s time box—that is, when it is between 0 (inclusive) and the duration of pict (exclusive), then the duration of the resulting pict is larger by n_epochs. The duration is extended by adding epochs that render as Pict.snapshot(pict, epoch, 0) before the epoch at index epoch.

> def p = circle(~size: 20, ~fill: "lightgreen")

> def q = p.time_insert(0, 3)

> rectangle(~around: q.snapshot(3, 0.0))

image

> rectangle(~around: q.snapshot(4, 0.0))

image

method

method (pict :: Pict).epoch_set_extent(i :: Int,

                                       extent :: NonnegReal) :: Pict

Returns a pict that is like pict, but with the extent of one of its epochs adjusted.

> def p = circle(~size: 20, ~fill: "lightgreen")

> p.epoch_extent(0)

0

> def q = q.epoch_set_extent(0, 0.5)

> q.epoch_extent(0)

0.5

method

method (pict :: Pict).epoch_metadata(i :: Int) :: Map

 

method

method (pict :: Pict).epoch_set_metadata(i :: Int,

                                         metadata :: Map) :: Pict

Gets metadata registered for an epoch within a pict, or returns a pict that is like pict but with the given metadata for the given epoch.

When picts are combined through operations like beside or overlay, the combination’s metadata is formed by merging metadata maps from the combined picts, and later picts in the combination take precedence.

function

fun bend.fast_start(n :: Real.in(0, 1)) :: Real.in(0, 1)

 

function

fun bend.fast_middle(n :: Real.in(0, 1)) :: Real.in(0, 1)

 

function

fun bend.fast_edges(n :: Real.in(0, 1)) :: Real.in(0, 1)

 

function

fun bend.fast_end(n :: Real.in(0, 1)) :: Real.in(0, 1)

Functions that are useful as a ~bend argument to animate to map a linear animation effect to a non-linear one.

 

  

bend.fast_start

  

bend.fast_middle

  

bend.fast_edges

  

bend.fast_end

 

  

  

  

  

  

image

  

image

  

image

  

image

function

fun cross_fade(

  from :: StaticPict,

  to :: StaticPict,

  ~fit: fit :: CrossFit = #'none,

  ~horiz: horiz_align :: HorizAlignment = (if dx || dy | #'left | #'center),

  ~vert: vert_align :: VertAlignment = (if dx || dy | #'top | #'center),

  ~order: order :: OverlayOrder = #'front,

  ~extent: extent :: NonnegReal = 0.5,

  ~bend: bender :: (Real.in(0, 1) -> Real.in(0, 1))

           = bend.fast_middle,                 

) :: Pict

Returns a pict that starts with the same drawing and bounding box as from, but animates through epoch 0 to end up with the drawing and bounding box of from. Meanwhile from fades out (via Pict.alpha) and to fades in. The resulting pict has a duration of 1, so Pict.sustain may be useful on the result.

If fit is #'none, from and to are unscaled for intermediate points in the fade, and they are positioned relative to the bounding box using overlay with horiz_align and vert_align. The order argument is also used as for overlay. If fit is #'scale, then from and to are scaled to fit the bounding box as they fade out and in.

The cross fade takes extent seconds. The bender argument maps time within the epoch to an interpolation point, the same as with animate.

When using cross_fade plus Pict.snapshot to compute an interpolation between two picts, then bender should normally be values so that the second argument to Pict.snapshot is the interpolation point.

> explain_anim(cross_fade(circle(~size: 15), square(~size: 25)))

image

> explain_anim(cross_fade(circle(~size: 15), square(~size: 25),

                          ~fit: #'scale))

image

> explain_anim(cross_fade(circle(~size: 15), square(~size: 25),

                          ~horiz: #'left))

image

function

fun magic_move(

  from :: Pict,

  to :: Pict,

  ~join: join :: SequentialJoin = #'step,

  ~other: other :: NoncommonMode = #'switch,

  ~extent: extent :: NonnegReal = 0.5,

  ~bend: bender :: (Real.in(0, 1) -> Real.in(0, 1))

           = bend.fast_middle,                 

) :: Pict

Creates a pict similar to switch(from, to, ~join: join), but the spliced epoch is adjusted so that findable children common to both from and to are moved and scaled from their positions and scales in from to their positions and scales in to.

If other is #'switch elements not common to from and to appear in the first or second half of the animation, depending on whether they are only in from or only in to. If other is #'fade, then non-common element fade out and in.

The move takes extent seconds, and the bender argument maps time the same as with animate.

> def sq = square(~size: 18, ~fill: "lightblue")

> def cr = circle(~size: 12, ~fill: "pink")

> def tri = triangle(~width: 16, ~fill: "black")

> def pre = beside(sq, cr).scale(1.5)

> pre

image

> def post = beside(~sep: 5, cr, tri, sq)

> post

image

> explain_anim(magic_move(pre, post), ~steps: 7, ~end: 1)

image

> explain_anim(magic_move(pre, post, ~other: #'fade), ~steps: 7, ~end: 1)

image

enumeration

enum CrossFit:

  none

  scale

Fit mode for cross_fade.

enumeration

enum NoncommonMode:

  switch

  fade

Mode for elements not common to “from” and “to” for magic_move.

function

fun explain_anim(

  pict :: Pict,

  ~start: start :: Int = 0,

  ~end: end :: Int = math.max(p.duration, start),

  ~steps: steps :: PosInt = 4,

  ~steps_before: steps_before :: NonnegInt = 0,

  ~steps_after: steps_after :: NonnegInt = 0,

  ~show_timebox: show_timebox = #true,

  ~pad_before: pad_before :: NonnegInt = 0,

  ~pad_after: pad_after :: NonnegInt = 0

) :: Pict

Returns a pict that shows a timeline with snapshots of pict’s drawing at different points. The timesline shows epochs start through end, inclusive. If steps is greater than 1, then steps-1 intermediate points are shown for each complete epoch. Use steps_before to add extra steps before start and steps_after to add extra steps after end.

If show_timebox is a true value, then a box is drawn around points on the timeline that are within pict’s time box.

If pad_before or pad_after are not 0, then space is added to the left or right of the timeline, respectively, corresponding to pad_before or pad_after epochs. This padding can be useful for aligning timelines that have different starting or ending epochs.

> explain_anim(animate(fun (n): circle(~fill: "blue").alpha(1-n)),

               ~steps: 5)

image