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:

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)

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:

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)

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)

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)

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)

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)

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:

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)

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

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

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

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

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)

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()
}

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()
}

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:
- Creation - Animation object created, auxiliary objects initialized
- Initialization - Object states saved (at t=0)
- For each frame (t from 0 to 1):
- Restore objects to initial state (t=0)
- Apply transformations for current time t
- 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)

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)

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)

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)

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)

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)

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)

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

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.