home back

Advanced Animation Techniques

This chapter covers advanced techniques for working with animations in JMathAnim, including combining multiple animations, adding visual effects, controlling timing, and creating complex animation sequences.

Is this chapter for me? If you just want your objects to appear, move and transform, the previous chapter is enough. Come here when you catch yourself thinking "I wish that square did a little jump while moving" . That's exactly what this chapter is about, and it's more fun than it sounds.

The lazy shortcut: if you use DSL animations, most effects in this chapter are available directly through the effects: parameter, with no extra objects involved:

animShift(obj: sq, dx: 1, effects: [jump: 0.5, jumptype: "crane", turns: 1])

Combining Animations

When you need multiple transformations to occur simultaneously, there are important considerations about how animations interact.

The State Management Problem

Suppose you want a square to shift and rotate at the same time. Your first instinct might be to run both animations together:

def sq = shape(
      type: "square",
      style: [fillColor: 'seagreen', thickness: 6],
      transform: [center: true]
)
def shift = animShift(
      runtime: 5,
      obj: sq,
      vector: [1, 0],
      run: false
)
def rotate = animRotate(
      runtime: 5,
      obj: sq,
      angle: -PI/2,
      run: false
)
animGroup(anims: [shift, rotate])   // play both at the same time
scene.waitSeconds(3)

If you run this code, you will see a square rotating, but not shifting at all.

StateFlagAnimation01

To understand why this happens and how to solve it, remember that each animation follows this process: 1. Initialize - Saves the current state of the object 2. Each frame - Restores object to initial state, then applies changes 3. Finalize - Cleanup

When both animations run simultaneously, the rotate animation's state restoration erases the changes made by the shift animation on each frame. In fact if there were n animations applied to the same object(s), the last one will delete the work done by the previous n-1 animations.

The solution is to disable state management for one animation using the parameter useObjectState inside the advanced parameters list. Alternatively, if you are using Groovy syntax, you can disable it with anim.setUseObjectState(false) where anim is your Animation object.

def sq = shape(
      type: "square",
      style: [fillColor: 'seagreen', thickness: 6],
      transform: [center: true]
)
def shift = animShift(
      runtime: 5,
      obj: sq,
      vector: [1, 0],
      run: false
)
def rotate = animRotate(
      runtime: 5,
      obj: sq,
      angle: -PI/2,
      run: false,
      advanced: [useObjectState: false] //Add this line to disable state management
)
animGroup(anims: [shift, rotate])
scene.waitSeconds(3)

Now the square properly shifts and rotates:

StateFlagAnimation01

Rule of thumb: When combining animations on the same object: - Keep state management enabled for the first animation - Disable it for subsequent animations with advanced: [useObjectState: false] or in Groovy syntax, with the method .setUseObjectState(false)


Adding Effects to Animations

Several animation classes inherit from AnimationWithEffects, which allows you to add visual enhancements. These include all movement-related animations and some more:

  • Transform
  • FlipTransform
  • TransformMathExpression
  • shift
  • stack
  • align
  • moveIn
  • moveOut
  • setLayout

Basically there are 4 effect types: jump, scale, alpha and rotation. We will see them one by one:

The Jump Effect

This effect add a "jump" in a general sense.

How It Works

  • Direction: Perpendicular to the shift vector (90° clockwise from motion direction)
  • Height: Specifies jump amplitude (negative values jump in opposite direction)
  • Default trajectory: Parabola (except TransformMathExpression, which uses semicircle)

Basic Example

def hexagon = shape(
      type: "regularpolygon",
      sides: 6,
      transform: [scale: .25, moveto: Vec.relAt(.25, .5)],
      style: [fillColor: 'steelblue']
)
def triangle = shape(
      type: "regularpolygon",
      sides: 3,
      transform: [scale: .5, moveto: Vec.relAt(.75, .5)],
      style: [fillColor: 'orange']
)
morph(
    runtime: 5,
    type: "flip",
    from: hexagon,
    to: triangle,
    effects: [jump: .5]
)

jumpEffect

Jump Types

You can customize the trajectory using the JumpType enum. Here is a visual comparison of different jump paths:

jumpPaths

If you want to use this effect in Groovy syntax, just use the .addJumpEffect(double height) or addJumpEffect(JumpType type,double height) methods in your AnimationWithEffects object.


The Scale Effect

This method creates a "breathing" effect where the object grows and shrinks during animation.

def pol=shape(
    type: "regularpolygon",
    sides: 6,
    style: [fillColor: 'steelblue'],
    transform: [scale: .25, center: true]
)
animShift(
    runtime: 3,
    obj: pol,
    vector: [1,0],// Shift right
    effects: [scale: 2]    // Scale up to 2x and back
)
//Or you can use Groovy style
//def anim = Commands.shift(3, 1, 0, pol)  // Shift right
//anim.addScaleEffect(2)                    // Scale up to 2x and back
//play.run(anim)

scaleEffect

How it works: The object scales from 1.0 to the specified factor and back to 1.0 during the animation.


The Alpha Effect

This method creates a fading in-and-out effect.

def pol=shape(
    type: "regularpolygon",
    sides: 6,
    style: [fillColor: 'steelblue'],
    transform: [scale: .25, center: true]
)
animShift(
    runtime: 3,
    obj: pol,
    vector: [1,0],// Shift right
    effects: [alpha: 0.5]    // Fade to 20% opacity and back,
)
//Or you can use Groovy style
//def anim = Commands.shift(3, 1, 0, pol)  // Shift right
//anim.addAlphaEffect(.2)                   // Fade to 20% opacity and back
//play.run(anim)


The Rotation Effect

The .addRotationEffect(int numTurns) method adds spinning motion to the animation. Use positive values for counter-clockwise rotation and negative for clockwise ones.

def pol=shape(
    type: "regularpolygon",
    sides: 6,
    style: [fillColor: 'steelblue'],
    transform: [scale: .25, center: true]
)
animShift(
    runtime: 3,
    obj: pol,
    vector: [1,0],// Shift right
    effects: [turns: -1]    // One clockwise rotation
)
//Or you can use Groovy style
//def anim = Commands.shift(3, 1, 0, pol)  // Shift right
//anim.addRotationEffect(-1)    // One clockwise rotation
//play.run(anim)

rotateEffect


Combining Multiple Effects

Yes, you can nest multiple effects on the same animation!

def square=shape(
    type: "square",
    transform: [moveto: Vec.relAt(.25,.5), scale: .25],
    style: [fillColor: 'steelblue']
)
def circle = shape(
    type: "circle",
    transform: [scale: .25, moveto: Vec.relAt(.75,.5)],
    style: [fillColor: 'firebrick']
)
morph(
    type: "auto",
    from: square,
    to: circle,
    effects: [scale: .5, jump: .5, jumptype: "folium"]
)
scene.waitSeconds(3)

nestedShiftEffects


Effects in Shift Animations

Shift-type animations (shift, stack, align, moveIn, moveOut, setLayout) inherit from the ShiftAnimation class, which provides additional specialized effects.

Rotation by Arbitrary Angle

Beyond .addRotationEffect() (which uses complete rotations), you can specify exact angles:

// Rotate exactly 45 degrees during the shift
anim.addRotationEffectByAngle(45 * DEGREES)

Important: Animations like setLayout and stack compute shift vectors without considering this rotation, so the object's final position may differ from what you expect.


Per-Object Effects

When animating multiple objects, you can apply different effects to each one.

Example: Different Rotations for Each Square

def squares = MathObjectGroup.make()
for (int n = 0; n < 10; n++) {
    squares.add(Shape.square().scale(.1).fillColor(JMColor.random()))
}
squares.setLayout(LayoutType.RIGHT, .1).center()

// Pass individual squares, not the group
def anim = Commands.shift(5, 0, -1, squares.toArray())

// Apply different rotation to each square
for (int n = 0; n < 10; n++) {
    anim.addRotationEffectByAngle(squares.get(n), PI * n / 9)
}

play.run(anim)
scene.waitSeconds(2)

shiftAnimEffect1

Key insight: Use squares.toArray() to pass individual objects instead of the group, allowing per-object effect control.


The Delay Effect

Creates a staggered, wave-like animation where objects move sequentially rather than simultaneously.

Example Without Delay

def smallSquaresGroup = MathObjectGroup.make()
10.times {
    smallSquaresGroup.add(
          Shape.square().scale(.1).fillColor("random")
    )
}

def centralSquare = shape(
    type: "square",
    transform: [scale: .25],
    stack: [screen: "lower", gaps: .1]
)

// Position small squares to the left of central square
layout(
    apply: smallSquaresGroup,
    refpoint: centralSquare,
    type: "simple",
    layout: "left"
)


scene.add(smallSquaresGroup, centralSquare)
scene.waitSeconds(1)

animSetLayout(
    runtime: 5,
    obj: smallSquaresGroup,
    layout: "upper",
    refpoint: centralSquare,
)

All squares start and end simultaneously:

delayEffect1

With Delay Effect

Just add a delay parameter to the animation:

animSetLayout(
    runtime: 5,
    obj: smallSquaresGroup,
    layout: "upper",
    refpoint: centralSquare,
    delay: 0.5 //50% delay
)

delayEffect2

How Delay Works

The parameter t (0 < t < 1) determines the stagger amount: - Individual animation duration = total runtime × (1 - t) - Animations are distributed evenly across the total runtime

Examples: - .addDelayEffect(.3) Each animation uses 70% of total time, staggered over full duration - .addDelayEffect(.75) Each animation uses 25% of total time, creating a strong wave effect

delayEffect3


Controlling Animations with Lambda Functions

Lambda functions provide fine-grained control over animation timing and behavior, transforming how animations feel.

Understanding Lambda Functions

Every animation has a lambda function that maps normalized time (0 to 1) to animation progress (0 to 1). In a beautiful mathematical notation, it will be something as:

λ: [0,1] → [0,1]

This function transforms the linear time parameter t in doAnim(t) into a new value, enabling: - Smooth starts and stops - Bouncing effects - Reverse playback - Custom timing curves

Available Lambda Functions

The UsefulLambdas class provides several pre-built functions. Here's a visual guide:

//this function given a lambda function and a legend
//returns a graph of the lambda in [0,1] with the legend
def drawGraphFor = { lambda, name ->
    def fg = funcGraph(
        func:  lambda,
        range: [0, 1],
        style: [thickness: 15, drawColor: 'darkblue']
    )
    def text = latex(
        text:      name,
        transform: [scale: 0.5],
        stack:     [to: fg, destinyAnchor: 'lower', gaps: 0.2]
    )
    def axisX = shape(type: 'segment', from: [-0.1, 0], to: [1.1, 0])
    def axisY = shape(type: 'segment', from: [0, -0.1], to: [0, 1.1])

    group(obj: [fg, text, axisX, axisY]) //Return this group
}

//List of graphs to create
def specs = [
    [UsefulLambdas.smooth(),              '{\\tt smooth()}'],
    [UsefulLambdas.smooth(.25d),          '{\\tt smooth(.25d)}'],
    [UsefulLambdas.allocateTo(.25, .75),  '{\\tt allocate(.25,.75)}'],
    [UsefulLambdas.reverse(),             '{\\tt reverse()}'],
    [UsefulLambdas.bounce1(),             '{\\tt bounce1()}'],
    [UsefulLambdas.bounce2(),             '{\\tt bounce2()}'],
    [UsefulLambdas.backAndForthBounce1(), '{\\tt backAndForthBounce1()}'],
    [UsefulLambdas.backAndForthBounce2(), '{\\tt backAndForthBounce2()}']
]

def functions = group(
    obj:    specs.collect { drawGraphFor(it[0], it[1]) },
    layout: [type: 'box', rowSize: 4, gaps: 0.25, refPoint: [0, 0], direction: 'right_down']
)

scene.add(functions)
camera.zoomToAllObjects()
scene.saveImage("lambdas.png") //Save to PNG

lambdas01

Interpreting Lambda Graphs

  • X-axis: Time from 0 to 1 (start to finish)
  • Y-axis: Animation progress from 0 to 1 (0% to 100% complete)
  • Proper lambdas satisfy: λ(0) = 0 and λ(1) = 1

Common Lambda Functions

1. smooth(smoothness) - Default for all animations

UsefulLambdas.smooth()      // Default: 90% smoothness
UsefulLambdas.smooth(0)     // Linear (no smoothing)
UsefulLambdas.smooth(.25)   // 25% smoothness

2. reverse() - Play animation backwards

UsefulLambdas.reverse()     // λ(t) = 1 - t

3. allocateTo(start, end) - Compress animation into time window. This is used mostly to compose with another lambdas. If you want to restrict your animation to a given time interaval in a simple way, use something like advanced: [allocationParameter: [.25,.75]] in your DSL block o .setAllocationParameters(.25,.75) in your Groovy Animation object.

UsefulLambdas.allocateTo(.25, .75)  // Animation runs from 25% to 75% of duration

4. bounce1() / bounce2() - Single or double bounce effect

5. backAndForthBounce1() / backAndForthBounce2() - Returns to start at t=1

Setting Default Lambda

To use linear timing for a single animation:

anim.setLambda(t -> t)  // or UsefulLambdas.smooth(0)

To set a default lambda for all animations in your scene:

config.setDefaultLambda(UsefulLambdas.smooth(.5))  // In setupSketch()

Composing Lambda Functions

Lambda functions are DoubleUnaryOperator objects that support composition via .compose().

Example: Delayed Rotation

Let's revisit the problem of the rotating-shifting square (this time in Groovy style):

def sq = Shape.square()
    .scale(.5)
    .style("solidblue")
    .moveTo(-1, 0)

def ag = AnimationGroup.make(
    Commands.shift(6, 2, 0, sq),
    Commands.rotate(6, PI * .5, sq)
        .setUseObjectState(false)
)
play.run(ag)

Both animations start and end together. Now let's make the rotation occur only during the middle 20% of the animation:

Commands.rotate(6, PI * .5, sq)
    .setUseObjectState(false)
    .setLambda(
        UsefulLambdas.smooth()
            .compose(UsefulLambdas.allocateTo(.4, .6))
    )

lambdas02

How it works: 1. allocateTo(.4, .6) compresses time from [0,1] to [.4, .6] 2. smooth() applies easing to the compressed time 3. Result: Rotation starts at 40% and finishes at 60% of total duration

Example: Bounce Effect

.setLambda(
    UsefulLambdas.bounce2()
        .compose(UsefulLambdas.allocateTo(.2, .75))
)

lambdas03

The bounce occurs between 20% and 75% of the animation duration.


Visualizing Lambda Effects

Here's a complete example showing lambda graphs alongside their effects:

// Axes with 0.25 ticks on both axes (added to the scene automatically)
axes(xRange: [0, 1], xStep: 0.25, yRange: [0, 1], yStep: 0.25)

// Timing lambdas
def shiftLambda  = UsefulLambdas.bounce1()
def rotateLambda = UsefulLambdas.smooth().compose(UsefulLambdas.allocateTo(.3, .6))

// Builds a graph + a point riding on it + a legend that follows the point.
// Returns the moving point (the only thing the animation needs afterwards).
def makeTracer = { lambda, graphColor, pointColor, name ->
    def fg = funcGraph(
          func:  lambda,
          range: [0, 1],
          style: [drawColor: graphColor, thickness: 6]
    )
    def pt = pointOnGraph(
          x: 0,
          graph: fg,
          style: [drawColor: pointColor, thickness: 40]
    )

    def legend = label(
          text: name,
          path: pt,
          scale: .5
    )
    scene.add(legend, fg, pt)
    pt //return this object
}

def pointShift  = makeTracer(shiftLambda,  "brown",  "darkblue", "shift")
def pointRotate = makeTracer(rotateLambda, "orange", "darkred",  "rotate")

camera.setMathXY(-1, 2, .25)

def sq = shape(// Animated square
      type:      'square',
      style:     'solidblue',
      transform: [scale: 0.25, moveTo: [0, -0.25]],
      addToScene: true
)
animGroup(// Play everything at once
      anims: [
          // Points move at constant speed (linear) along the x-axis
          animShift(obj: pointShift,  dx: 1, runtime: 6, lambda: 'linear', run: false),
          animShift(obj: pointRotate, dx: 1, runtime: 6, lambda: 'linear', run: false),
          // Square driven by the custom lambdas
          animShift(obj: sq, dx: 1, runtime: 6, lambda: shiftLambda, run: false),
          animRotate(obj: sq, angle: PI * .5, runtime: 6, lambda: rotateLambda,
                advanced: [useObjectState: false], run: false)
      ])
scene.waitSeconds(1)

lambdas04

What's happening: - Two moving dots show current time on each lambda curve - The square follows the combined behavior of both lambdas - Shift lambda (brown): bounces - Rotate lambda (orange): occurs only from 30% to 60%


Making Procedural Animations

Sometimes predefined animations aren't enough. Procedural animations give you frame-by-frame control for complex, custom movements.

Basic Concept

Procedural animation means manually modifying objects and advancing frames, like stop-motion animation.

Key Variable: dt

The dt variable holds the time step for each frame:

dt = scene.getDt()  // Time per frame (e.g., 1/60 for 60fps)

Example: Random Walk

def A = Point.origin()
scene.add(A)

dt = scene.getDt() //Get the time step for each frame
def numberOfSeconds = 10
for (t in arange(0, numberOfSeconds, dt)) {
    // Random step in x and y
    A.shift((1 - 2 * Math.random()) * dt, (1 - 2 * Math.random()) * dt)
    scene.advanceFrame()
}

You will obtain a rather nervous point:procedural01


Combining Procedural and Predefined Animations

You can mix manual control with predefined animations:

def A = Point.origin()
def square = Shape.square().center()
scene.add(A, square)

// Define and initialize a rotation animation
def rotation = Commands.rotate(5, 90 * DEGREES, square)
rotation.initialize()

dt = scene.getDt()
double numberOfSeconds = 10

for (t in arange(0, numberOfSeconds, dt)) {
    // Manual: random walk for point A
    A.shift((1 - 2 * Math.random()) * dt, (1 - 2 * Math.random()) * dt)

    // Predefined: process rotation animation
    //For most simple animations you don't need to call finishAnimation,
    //but it is a good practice to do so
    if (rotation.processAnimation()) rotation.finishAnimation()

    scene.advanceFrame()
}

procedural02

Important notes: 1. Initialize the animation before the loop 2. Call processAnimation() each frame 3. Once the animation finishes, subsequent calls have no effect


Reusing Animations

You can safely skip this section unless you are explicitly looking for this. In practice, you will rarely need to reuse existing animations, but it is better to be prepared for any eventuality...

Animation Lifecycle

Understanding the animation lifecycle is crucial when reusing animations. Every animation follows this flow:

  1. Creation - Animation object created, auxiliary objects initialized
  2. Initialization - Object states saved (at t=0)
  3. For each frame (t from 0 to 1):
  4. Restore objects to initial state (t=0)
  5. Apply transformations for current time t
  6. Cleanup - cleanAnimationAt(t) performs necessary cleanup

The Reinitialization Problem

When you reuse an animation, it automatically reinitializes, capturing the current state as the new "initial state."

Example: Forward and Reverse Rotation

Attempt 1: Naive approach

def sq = Shape.square()
    .scale(2, 1).center()
    .style("solidgreen")

def text = LatexMathObject.make("Forward...")
    .stack()
    .withGaps(.1)
    .toScreen(ScreenAnchor.LOWER_RIGHT)
scene.add(text)

def rotate = Commands.rotate(2, 45 * DEGREES, sq).setLambda(t -> t)
play.run(rotate)
scene.waitSeconds(1)

text.setLatex("Reverse...")
play.run(rotate.setLambda(UsefulLambdas.reverse()))
text.setLatex("End")
scene.waitSeconds(1)

resettingAnimations1

What happened?? The reverse animation starts from the rotated position (which is now the "initial state"), not the original position. This is because the "initial state" has changed in the second run. We need to tell the animation to use the original t=0 state, otherwise, playing the animation backwards won't run properly.


Solution 1: Disable Reinitialization

def sq = Shape.square()
    .scale(2, 1).center()
    .style("solidgreen")

def text = LatexMathObject.make("Forward...")
    .stack()
    .withGaps(.1)
    .toScreen(ScreenAnchor.LOWER_RIGHT)
scene.add(text)

def rotate = Commands.rotate(2, 45 * DEGREES, sq).setLambda(t -> t)
rotate.setShouldResetAtFinish(false)  // Prevents reinitialization
play.run(rotate)
scene.waitSeconds(1)

text.setLatex("Reverse...")
play.run(rotate.setLambda(UsefulLambdas.reverse()))
text.setLatex("End")
scene.waitSeconds(1)

resettingAnimations2

Ah, that's better, but we meet an unexcpected problem: The rectangle disappears at the end! Is this a bug?? Well, no, as the joke says, it's is not a bug, it's a feature! When playing in reverse and exiting at t=0, JMathAnim tries to restore the original state, and that includes the object being or not in the scene. If the object wasn't originally in the scene when animation was called, it removes it to ensure everything is left as before (yes, I know, JMathAnim sometimes is just too smart...)


Solution 2: Ensure Object is in Scene

def sq = Shape.square()
    .scale(2, 1).center()
    .style("solidgreen")

def text = LatexMathObject.make("Forward...")
    .stack()
    .withGaps(.1)
    .toScreen(ScreenAnchor.LOWER_RIGHT)

scene.add(sq, text)  // Add square BEFORE animating

def rotate = Commands.rotate(2, 45 * DEGREES, sq).setLambda(t -> t)
rotate.setShouldResetAtFinish(false)
play.run(rotate)
scene.waitSeconds(1)

text.setLatex("Reverse...")
play.run(rotate.setLambda(UsefulLambdas.reverse()))
text.setLatex("End")
scene.waitSeconds(1)

resettingAnimations3

And the rectangle now behaves correctly!


Creating Complex Animations

JMathAnim provides special Animation subclasses for building sophisticated animation sequences.

The WaitAnimation

Does exactly what it says: waits for a specified duration.

def wait = WaitAnimation.make(2)  // Wait 2 seconds

If you want to add pauses between animations, there is a shortcut scene.waitSeconds(numSeconds) equivalent to play.run(WaitAnimation.make(numSeconds))

You can use this animation to generate a given number of seconds in frames. We can rewrite the example of the nervous point in this way:

def A = Point.origin()
scene.add(A)

dt = scene.getDt() //Get the time step for each frame
def numberOfSeconds = 10

def anim=WaitAnimation.make(numberOfSeconds)
anim.initialize()
while (!anim.processAnimation()) {
    // Random step in x and y
    A.shift((1 - 2 * Math.random()) * dt, (1 - 2 * Math.random()) * dt)
    scene.advanceFrame()
}
anim.finishAnimation()

The AnimationGroup

Plays multiple animations simultaneously. Finishes when the last animation completes.

Basic Example

def sq1 = shape(
    type:  'square',
    style: [fillColor: 'seagreen', thickness: 7]
)

def sq2 = shape(
    type:  'square',
    style: [fillColor: 'crimson', thickness: 7],
    stack: [to: sq1, destinyAnchor: 'left']
)

animGroup(anims: [
    animShift(obj: sq1, dx:  .5, dy: -.5, runtime: 2, run: false),
    animShift(obj: sq2, dx: -.5, dy: -.5, runtime: 2, run: false)
])
scene.waitSeconds(1)

Or if you prefer using a pure Groovy style:

def sq1 = Shape.square()
    .fillColor("seagreen")
    .thickness(7)

def sq2 = Shape.square()
    .fillColor('crimson')
    .thickness(7)
    .stack().withDestinyAnchor(AnchorType.LEFT).toObject(sq1)

def shift1 = Commands.shift(2, .5, -.5, sq1)
def shift2 = Commands.shift(2, -.5, -.5, sq2)

def ag = AnimationGroup.make(shift1, shift2)
play.run(ag)
scene.waitSeconds(1)

animationGroup1

Both squares move simultaneously but in different directions.

With Delay Effect

AnimationGroup also supports delay effects:

def anims = (1..10).collect {
    def orangeRectangle = shape(
        type:       'square',
        transform:  [center: true],
        style:      [fillColor: 'orange', fillAlpha: 0.2],
        addToScene: true
    )
    animScale(obj: orangeRectangle, sx: 2, sy: .7, center: [0, 0], runtime: 5, run: false)
}

animGroup(anims: anims, delay: 0.5)   // 50% stagger
scene.waitSeconds(1)

Or in a more straightforward Groovy way:

def orangeRectangles = new Shape[10]
def ag = AnimationGroup.make()

for (int i = 0; i < 10; i++) {
    // Create 10 rectangles
    orangeRectangles[i] = Shape.square().center()
        .fillColor("orange").fillAlpha(.2)

    // Create scaling animation for each
    ag.add(
        Commands.scale(5, Point.origin(), 2, .7, 1, orangeRectangles[i])
    )
}

scene.add(orangeRectangles)
ag.addDelayEffect(.5)  // 50% stagger
play.run(ag)
scene.waitSeconds(1)

delayEffect4


The JoinAnimation

The JoinAnimationtreats all contained animations as a single unified animation.

def sq = shape(
    type:      'square',
    transform: [center: true],
    style:     [fillColor: 'seagreen', thickness: 7]
)

animJoin(anims: [
    animShift(obj: sq, dx: 1, runtime: 2, run: false),
    animRotate(obj: sq, angle: -PI/2, runtime: 2, run: false)
])
scene.waitSeconds(1)

concatenate01

First the square shifts, then it rotates. If unspecified, the runtime if the sum of runtimes of every animations in the list.

Another example. This time we have 3 animations, with runtimes that sum to 4 seconds, but the runtime of the JoinAnimationis set to 6:

def sq = shape(
    type:      'regularpolygon',
    sides:     5,
    transform: [center: true],
    style:     'solidred'
)

animJoin(
    runtime: 6,                                        // total runtime
    anims: [
        appear(obj: sq, runtime: 2, run: false),       // ShowCreation (2s)
        animShift(obj: sq, dx: 1, runtime: 1, run: false),   // (1s)
        animRotate(obj: sq, angle: PI/4, runtime: 1, run: false)  // (1s)
    ]
)
scene.waitSeconds(3)

joinAnimation1

How duration works in this case: Each animation is played with runtime proportionally to the whole duration. - Total duration: 6 seconds - Runtime ratios: 2:1:1 - ShowCreation takes: 6 × (2/4) = 3 seconds - shift takes: 6 × (1/4) = 1.5 seconds - rotate takes: 6 × (1/4) = 1.5 seconds

One good thing about JoinAnimation is that you can apply lambdas as a one, complex animation! Add this line to the DSL block of the animJoin:

animJoin(
    runtime: 6,                                        // total runtime
    anims: [
        appear(obj: sq, runtime: 2, run: false),       // ShowCreation (2s)
        animShift(obj: sq, dx: 1, runtime: 1, run: false),   // (1s)
        animRotate(obj: sq, angle: PI/4, runtime: 1, run: false)  // (1s)
    ],
    lambda: "backAndForth" //Set this lambda for the global animation
)

joinAnimation2

The entire sequence plays forward then backward as a single unit.

Note: The default lambda for JoinAnimation is linear (t -> t), unlike most animations which use smooth().

Summary

This chapter covered advanced animation techniques:

  • Combining animations with proper state management
  • Adding effects (jump, scale, alpha, rotation) to enhance animations
  • Lambda functions for custom timing and easing
  • Procedural animations for frame-by-frame control
  • Reusing animations correctly to avoid common pitfalls
  • Complex animation structures (groups, sequences, joins)

With these tools, you can create sophisticated, professional-quality animations that combine multiple objects, effects, and timing controls.


home back