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.


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.square()
    .fillColor("seagreen")
    .thickness(6)
    .center()

def shift = Commands.shift(5, 1, 0, sq)
def rotate = Commands.rotate(5, -PI/2, sq)
play.run(shift, rotate)
scene.waitSeconds(3)

Problem: The square will rotate but won't shift!

Why This Happens

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.

The Solution: setUseObjectState

Disable state management for one animation using .setUseObjectState(false):

def sq = Shape.square()
    .fillColor("seagreen")
    .thickness(6)
    .center()

def shift = Commands.shift(5, 1, 0, sq)
def rotate = Commands.rotate(5, -PI/2, sq).setUseObjectState(false)
play.run(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 .setUseObjectState(false)


Adding Effects to Animations

Several animation classes inherit from AnimationWithEffects, which allows you to add visual enhancements. These include:

Compatible Animations: - Transform - FlipTransform - TransformMathExpression - shift - stack - align - moveIn - moveOut - setLayout

The Jump Effect

The .addJumpEffect(double height) method adds a jumping motion to animations.

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.regularPolygon(6)
    .scale(.25)
    .moveTo(Point.relAt(.25, .5))
    .fillColor("steelblue")

def triangle = Shape.regularPolygon(3)
    .scale(.5)
    .moveTo(Point.relAt(.75, .5))
    .fillColor("orange")

def anim = FlipTransform.make(5, OrientationType.HORIZONTAL, hexagon, triangle)
anim.addJumpEffect(.5)  // Adds a jump with height 0.5
play.run(anim)

jumpEffect

Jump Types

You can customize the trajectory using the JumpType enum:

// Different jump trajectories
anim.addJumpEffect(.5, JumpType.CRANE)       // Crane-like motion
anim.addJumpEffect(.5, JumpType.ELLIPTICAL)  // Elliptical path
anim.addJumpEffect(.5, JumpType.PARABOLICAL) // Parabolic path (default)
anim.addJumpEffect(.5, JumpType.SEMICIRCLE)  // Semicircular arc (height sign only)
anim.addJumpEffect(.5, JumpType.SINUSOIDAL)  // Sine wave (0 to π)
anim.addJumpEffect(.5, JumpType.SINUSOIDAL2) // Sine wave (0 to 2π)
anim.addJumpEffect(.5, JumpType.TRIANGULAR)  // Triangular roof shape
anim.addJumpEffect(.5, JumpType.FOLIUM)  // A beautiful curve

Visual comparison of different jump paths:

jumpPaths


The Scale Effect

The .addScaleEffect(double scale) method creates a "breathing" effect where the object grows and shrinks during animation.

def pol = Shape.regularPolygon(6)
    .scale(.25).center()
    .fillColor("steelblue")

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

The .addAlphaEffect(double alphaScale) method creates a fading in-and-out effect.

def pol = Shape.regularPolygon(6)
    .scale(.25).center()
    .fillColor("steelblue")

def anim = Commands.shift(3, 1, 0, pol)  // Shift right
anim.addAlphaEffect(.2)                   // Fade to 20% opacity and back
play.run(anim)

alphaEffect

How it works: The object's opacity changes from 1.0 (fully opaque) to the specified alpha value and back to 1.0.


The Rotation Effect

The .addRotationEffect(int numTurns) method adds spinning motion to the animation.

def pol = Shape.regularPolygon(6)
    .scale(.25).center()
    .fillColor("steelblue")

def anim = Commands.shift(3, 1, 0, pol)  // Shift right
anim.addRotationEffect(-1)                // One counter-clockwise rotation
play.run(anim)

rotateEffect

Parameters: - Positive values: clockwise rotation - Negative values: counter-clockwise rotation - The number indicates complete 360° rotations


Combining Multiple Effects

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

def square = Shape.square()
    .scale(.25)
    .moveTo(Point.relAt(.25, .5))
    .fillColor("steelblue")

def circle = Shape.circle()
    .scale(.25)
    .moveTo(Point.relAt(.75, .5))
    .fillColor("firebrick")

def anim = Transform.make(5, square, circle)
anim.addRotationEffect(1)              // Spin once
    .addScaleEffect(.5)                // Shrink to 50% and grow back
    .addJumpEffect(JumpType.FOLIUM, .5) // Jump with folium trajectory

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

nestedShiftEffects

Tip: Effects are applied in the order they're added, creating rich, complex animations from simple combinations.


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()
for (int n = 0; n < 10; n++) {
    smallSquaresGroup.add(Shape.square().scale(.1).fillColor(JMColor.random()))
}

def centralSquare = Shape.square().scale(.25)
    .stack().withGaps(.1).toScreen(ScreenAnchor.LOWER)

// Position small squares to the left of central square
smallSquaresGroup.setLayout(centralSquare, LayoutType.LEFT, 0)

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

def anim = Commands.setLayout(5, centralSquare, LayoutType.UPPER, 0, smallSquaresGroup)
// anim.addDelayEffect(.5)  // Commented out to see default behavior
play.run(anim)

All squares start and end simultaneously:

delayEffect1

With Delay Effect

Now uncomment the delay effect:

def anim = Commands.setLayout(5, centralSquare, LayoutType.UPPER, 0, smallSquaresGroup)
anim.addDelayEffect(.5)  // 50% delay
play.run(anim)

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):

λ: [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:

private drawGraphFor(lambda, name) {
    def resul = MathObjectGroup.make()

    def fg = FunctionGraph.make(lambda, 0, 1)
        .thickness(15)
        .drawColor('darkblue')

    def text = LatexMathObject.make(name)
        .scale(.5)
        .stack()
        .withGaps(.2)
        .withDestinyAnchor(AnchorType.LOWER)
        .toObject(fg)

    def segX = Shape.segment(Vec.to(-.1, 0), Vec.to(1.1, 0))
    def segY = segX.copy().rotate(Vec.origin(), .5 * PI)

    resul.add(fg, text, segX, segY)
    resul
}

def functions = MathObjectGroup.make(
    drawGraphFor(UsefulLambdas.smooth(), "{\\tt smooth()}"),
    drawGraphFor(UsefulLambdas.smooth(.25d), "{\\tt smooth(.25d)}"),
    drawGraphFor(UsefulLambdas.allocateTo(.25, .75), "{\\tt allocate(.25,.75)}"),
    drawGraphFor(UsefulLambdas.reverse(), "{\\tt reverse()}"),
    drawGraphFor(UsefulLambdas.bounce1(), "{\\tt bounce1()}"),
    drawGraphFor(UsefulLambdas.bounce2(), "{\\tt bounce2()}"),
    drawGraphFor(UsefulLambdas.backAndForthBounce1(), "{\\tt backAndForthBounce1()}"),
    drawGraphFor(UsefulLambdas.backAndForthBounce2(), "{\\tt backAndForthBounce2()}")
)
functions.setLayout(
    BoxLayout.make(Vec.origin(), 4, .25, .25)
        .setBoxDirection(BoxDirection.RIGHT_DOWN)
)
scene.add(functions)
camera.zoomToAllObjects()
scene.saveImage("lambdaGraphs.png") //Save to PNG image

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

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

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:

def axes = Axes.make()
axes.generatePrimaryXTicks(0, 1, .25)
axes.generatePrimaryYTicks(0, 1, .25)
scene.add(axes)

// Define lambdas for rotate and shift
def rotateLambda = UsefulLambdas.smooth()
    .compose(UsefulLambdas.allocateTo(.3, .6))
def shiftLambda = UsefulLambdas.bounce1()

// Create function graphs
def fgShift = FunctionGraph.make(shiftLambda, 0, 1)
    .drawColor("brown").thickness(6)

// Moving point on shift graph
def pointFgShift = PointOnFunctionGraph.make(0, fgShift)
    .drawColor("darkblue")
    .thickness(40)

// Label that follows the point
def legendShift = LatexMathObject.make("shift")
    .color("brown")
    .scale(.5)
legendShift.registerUpdater(
    StackToUpdater.make(AnchorType.LOWER, 0, .05, pointFgShift)
)

scene.add(legendShift, fgShift, pointFgShift)

// Same for rotate graph
def fgRotate = FunctionGraph.make(rotateLambda, 0, 1)
    .drawColor("orange")
    .thickness(6)

def pointFgRotate = PointOnFunctionGraph.make(0, fgRotate)
    .drawColor("darkred")
    .thickness(40)

def legendRotate = LatexMathObject.make("rotate")
    .color("orange")
    .scale(.5)
legendRotate.registerUpdater(
    StackToUpdater.make(AnchorType.LOWER, 0, .05, pointFgRotate)
)

scene.add(legendRotate, fgRotate, pointFgRotate)

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

// The animated square
def sq = Shape.square()
    .scale(.25)
    .style("solidblue")
    .moveTo(Point.at(0, -.25))
scene.add(sq)

// Animate everything
def ag = AnimationGroup.make(
    // Move points linearly along x-axis
    Commands.shift(6, 1, 0, pointFgShift)
        .setLambda(t -> t),
    Commands.shift(6, 1, 0, pointFgRotate)
        .setLambda(t -> t),
    // Animate square with custom lambdas
    Commands.shift(6, 1, 0, sq)
        .setLambda(shiftLambda),
    Commands.rotate(6, PI * .5, sq)
        .setUseObjectState(false)
        .setLambda(rotateLambda)
)
play.run(ag)
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()
double numberOfSeconds = 10

for (double t = 0; t < numberOfSeconds; t += dt) {
    // Random step in x and y
    A.shift((1 - 2 * Math.random()) * dt, (1 - 2 * Math.random()) * dt)
    scene.advanceFrame()
}

procedural01

How it works: - Each frame, the point moves by a random amount - advanceFrame() saves the current state as a frame - Loop continues for 10 seconds


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 (double t = 0; t < numberOfSeconds; t += dt) {
    // Manual: random walk for point A
    A.shift((1 - 2 * Math.random()) * dt, (1 - 2 * Math.random()) * dt)

    // Predefined: process rotation animation
    rotation.processAnimation()

    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

Understanding the animation lifecycle is crucial when reusing animations.

Animation Lifecycle

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

Problem: The reverse animation starts from the rotated position (which is now the "initial state"), not the original position.


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

New problem: The rectangle disappears at the end!

Why? When playing in reverse and exiting at t=0, JMathAnim tries to restore the original state. If the object wasn't originally in the scene, it removes it.


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

Success! The rectangle now behaves correctly.

Best practice: Always add objects to the scene before animating them if you plan to reuse animations or play them in reverse.


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

Use case: Adding pauses between animations in complex sequences.


The AnimationGroup

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

Basic Example

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 rects = new Shape[10]
def ag = AnimationGroup.make()

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

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

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

delayEffect4

The rectangles scale in a wave pattern.


The Concatenate Animation

Plays animations sequentially (one after another).

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

def shift = Commands.shift(2, 1, 0, sq)
def rotate = Commands.rotate(2, -PI/2, sq)

def c = Concatenate.make(shift, rotate)
play.run(c)
scene.waitSeconds(1)

concatenate01

First the square shifts, then it rotates.


The JoinAnimation

Similar to Concatenate, but treats all contained animations as a single unified animation.

Basic Example

def sq = Shape.regularPolygon(5)
    .center()
    .style("solidred")

def anim = JoinAnimation.make(
    6,                                  // Total duration
    ShowCreation.make(2, sq),          // First (2s runtime)
    Commands.shift(1, 1, 0, sq),       // Second (1s runtime)
    Commands.rotate(1, PI / 4, sq)     // Third (1s runtime)
)

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

joinAnimation1

How duration works: - 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

Advantage Over Concatenate

You can apply lambdas to the entire sequence as one animation:

anim.setLambda(UsefulLambdas.backAndForth())

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