home back

Animations

So far, we've learned to draw basic objects and position them where we want. Now let's explore what this library was designed for: animations. The Animation class stores any kind of animation you can apply to objects. Not only can MathObject instances be animated, but the Camera object can also be animated.

Quick Start: Your First Animation

Animating an object is straightforward. Every common animation has a DSL command (insert them from the snippets palette with Alt+S, category Animations) that plays immediately and takes readable named parameters:

def sq = Shape.square().center()                  // A square centered on screen
animShift(obj: sq, dx: 0.5, dy: 0.5, runtime: 1)  // Move it (0.5, 0.5) in 1 second

Important: Note that sq was not explicitly added to the scene. The animation adds it automatically.

Every anim* command autocompletes with Ctrl+Space and is listed in the DSL cheatsheet. A few you will use constantly:

appear(obj: sq, type: "draw", runtime: 2)      // draw it into being
animRotate(obj: sq, degrees: 90, runtime: 2)   // angles in degrees, no DEGREES needed
disappear(obj: sq, type: "shrinkout")          // bye bye square

In plain Groovy: the same animations live on the play object, which is more compact once you know it. animShift(obj: sq, dx: 0.5, dy: 0.5, runtime: 1) is play.shift(1, 0.5, 0.5, sq). You can also build an animation explicitly and run it:

def anim = Commands.shift(1, Vec.to(.5, .5), sq)
play.run(anim)   // play.run(anim) is an alias for scene.playAnimation(anim)

play.xxx and the DSL do exactly the same thing; mix them freely.

Understanding the Animation Lifecycle

An Animation object has four key methods that control its lifecycle. Understanding these is essential if you want to implement custom animations or control them manually:

  1. initialize() - Prepares the objects to be animated. This should be called immediately before the animation begins. No modifications should be made to the objects between calling this method and starting the animation.

  2. processAnimation() - Computes the time based on the frame rate and calls the doAnim() method. Returns true when the animation is finished.

  3. doAnim(double t) - Actually performs the animation. The parameter t ranges from 0 to 1, where:

  4. t = 0 represents the beginning of the animation
  5. t = 1 represents the end

Note: t represents the percentage of animation completed, not actual time. Internally, a "smoothed" version of t is used to make animations start and end smoothly rather than linearly. This smooth function is a lambda that you can get or set using getLambda() and setLambda(). We'll explore custom lambdas in the next chapter.

  1. finishAnimation() - Performs all necessary cleanup and finishing tasks.

Manual Animation Control

While you can use playAnimation() to handle everything automatically, you can also control animations manually:

// Manual control (equivalent to play.run(anim))
def anim = <define your animation here>
anim.initialize()
while (!anim.processAnimation()) {
    scene.advanceFrame()
}
anim.finishAnimation()

is equivalent to:

// Automatic control (recommended for most cases)
def anim = <define your animation here>
play.run(anim)

The play Object

The play object is a convenient shortcut for accessing commonly used animations. It's an instance of the PlayAnim class.

Animation Parameter Structure

In general, animation parameters follow this consistent structure:

(runTime, parameters, object_1, …, object_n)

The last part is a varargs MathObject, allowing you to apply the animation to multiple objects simultaneously.


Basic Animations

The three basic transformations — shift, rotate and scale — have DSL animation commands: animShift, animRotate and animScale.

Example: Moving a Square

As they say, a GIF (and its code) is worth a thousand words:

def sq = Shape.square().fillColor("#87556f")
animShift(obj: sq, dx: 0.75, dy: -0.5, runtime: 3)  // move over 3 seconds
scene.waitSeconds(1)

animationShift1

In plain Groovy: animShift(obj: sq, dx: 0.75, dy: -0.5, runtime: 3) is play.shift(3, 0.75, -0.5, sq) (or play.shift(3, Vec.to(0.75, -0.5), sq)). You can also build the animation first and play it: play.run(Commands.shift(3, Vec.to(0.75, -0.5), sq)).


Moving, Rotating, and Scaling

The three commands cover the most common transformations:

// Rotate 45 degrees around its own center, in 3 seconds
animRotate(obj: sq, degrees: 45, runtime: 3)

// Rotate 120 degrees around the origin
animRotate(obj: sq, degrees: 120, center: [0, 0], runtime: 5)

// Scale uniformly to 70%, around its center, in 3 seconds
animScale(obj: sq, scale: 0.7, runtime: 3)

// Scale to 70% in X and 150% in Y, around (1, 0), in 3 seconds
animScale(obj: sq, sx: 0.7, sy: 1.5, center: [1, 0], runtime: 3)

In plain Groovy: these are play.rotate(3, 45*DEGREES, sq), play.rotate(5, Vec.origin(), 120*DEGREES, sq), play.scale(3, .7, sq) and play.scale(3, Vec.to(1, 0), .7, 1.5, sq).

Rotating several objects — around each center or a shared one: with the Groovy play.rotate, passing objects as separate arguments (play.rotate(3, 45*DEGREES, a, b, c)) rotates them all around their combined center, as a rigid block; passing them as a list (play.rotate(3, 45*DEGREES, [a, b, c])) rotates each around its own center. The same distinction applies with an explicit center: play.rotate(3, center, 45*DEGREES, [a, b, c]).


Animating the Camera

The camera view is animated with the animCamera command, whose type selects the motion:

// Pan the camera 4 seconds with vector (1, -1)
animCamera(type: "shift", dx: 1, dy: -1, runtime: 4)

// Zoom in to 200% (scale factor 0.5), in 3 seconds
animCamera(type: "scale", scale: 0.5, runtime: 3)

// Zoom out to 25% (scale factor 4)
animCamera(type: "scale", scale: 4, runtime: 3)

// Pan and zoom to fit specific objects
animCamera(type: "zoomToObjects", obj: [sq, circ, A, B], runtime: 3)

// Pan and zoom to fit every object in the scene
animCamera(type: "zoomToAllObjects", runtime: 3)

In plain Groovy: play.cameraShift(4, 1, -1), play.cameraScale(3, .5), play.adjustCameraToObjects(3, sq, circ, A, B) and play.adjustCameraToAllObjects(3).

Tip: Before adjusting the camera to objects, you can define the gaps (padding) between objects and the screen border using:

camera.setGaps(hGap, vGap)

Entering and Exiting Animations

These commands help you smoothly add or remove objects from the scene. Bringing objects in is done with appear(...), taking them out with disappear(...); the type chooses the effect.

Fade Animations

appear(obj: sq, type: "fadein", runtime: 2)     // alpha 0 → 1, adds it to the scene
disappear(obj: sq, type: "fadeout", runtime: 2) // alpha 1 → 0, removes it from the scene

In plain Groovy: play.fadeIn(2, sq), play.fadeOut(2, sq), and play.fadeOutAll(2) to fade out every object in the scene at once.

Grow and Shrink Animations

appear(obj: sq, type: "growin", runtime: 2)                     // scales 0 → 1
appear(obj: sq, type: "growin", angle: 30*DEGREES, runtime: 2)  // ...also rotating 30°
disappear(obj: sq, type: "shrinkout", runtime: 2)               // scales → 0, then removes
disappear(obj: sq, type: "shrinkout", angle: 45*DEGREES, runtime: 2)

In plain Groovy: play.growIn(2, sq), play.growIn(2, 30*DEGREES, sq), play.shrinkOut(2, sq), play.shrinkOut(2, 45*DEGREES, sq).

Move In/Out Animations

// The object enters from / exits through a screen edge
appear(obj: sq, type: "movein", enter: "left")     // entering from the left
disappear(obj: sq, type: "moveout", exit: "left")  // exiting through the left

Edge values are left, right, upper, lower (and the corners upper_left, ...).

In plain Groovy: play.moveIn(1, ScreenAnchor.LEFT, sq), play.moveOut(1, ScreenAnchor.LEFT, sq).

Using Default Timing

Omit runtime and the animation uses its default duration (1 second):

appear(obj: sq, type: "fadein")   // a 1-second fade in

In plain Groovy: most play.xxx methods can also be called without the time argument (e.g. play.fadeIn(sq)); the defaults are the public play.defaultRunTime... variables.

Complete Example

def sq = Shape.square().fillColor("#87556f").center()
def text = latex(text: r"{\tt fade in}", stack: [screen: "lower", gaps: .1], addToScene: true)

appear(obj: sq, type: "fadein")
scene.waitSeconds(1)

text.setLatex(r"{\tt scale}")   // Changes the text
animScale(obj: sq, sx: 1.5, sy: 1, runtime: 1)
scene.waitSeconds(1)

text.setLatex(r"{\tt shrink out}")
disappear(obj: sq, type: "shrinkout", angle: 45*DEGREES)
scene.waitSeconds(1)

fadeHighLightShrinkDemo

Note: These commands give quick access to simple animations. For fine-tuning parameters like lambda functions or adding effects, see the next chapter — the DSL exposes them through effects: and lambda:, and the low-level Commands/animation objects expose them as methods.


Highlighting Animations

Highlighting animations briefly attract the viewer's attention to specific objects:

Available Highlighting Types

All of these are reached through the highlight(...) DSL command; the type picks the flavor:

  1. highlight (default) — scales the object back and forth (default: 150%)
  2. twist — like highlight but adds a small twist (default: ±15 degrees)
  3. boxHighlight — draws growing boxes around objects
  4. contour — draws a "snake" running over the shape's contour

Example: All Highlight Types

def sq = Shape.square().center()

def importantPoint1 = sq.getPoint(1).drawColor("red")
def importantLabel1 = label(
      text: "A",
      path: importantPoint1
)

def importantPoint2 = sq.getPoint(2).drawColor("green")
def importantLabel2 =  label(
      text: "B",
      path: importantPoint2,
      rotatedirection: 45*DEGREES
)


def importantPoint3 = sq.getPoint(3).drawColor("blue")
def importantLabel3 =  label(
      text: "C",
      path: importantPoint3,
      rotatedirection: 135*DEGREES
)

scene.add(sq,
      importantPoint1, importantLabel1,
      importantPoint2, importantLabel2,
      importantPoint3, importantLabel3
)

highlight(obj: importantLabel1)                  // scale back and forth
highlight(obj: importantLabel2, type: "twist")   // scale with a twist
highlight(obj: importantLabel3, type: "boxHighlight")
highlight(obj: sq, type: "contour")              // "snake" over the contour

highlightExamples

In plain Groovy: play.highlight(...), play.twistAndScale(...), play.boxHighlight(...) and play.contourHighlight(...) respectively.

Configuring the contour highlight

The contour ("snake") highlight takes a few extra parameters:

def obj = Shape.circle()
scene.add(obj)
highlight(obj: obj, type: "contour",
          amplitude: 0.85,   // max portion of the shape drawn (0–1, default 0.4; 1 draws then undraws all)
          thickness: 15,     // thickness of the "snake"
          color: "violet")   // color of the "snake"
scene.waitSeconds(2)

ContourHighlight1

In plain Groovy: build a ContourHighlight.make(2, obj) and chain .setAmplitude(.85).setThickness(15).setColor("violet"), then play.run(anim).

Highlighting Bounding Boxes

For complex shapes, running the snake over the bounding box is cleaner than over every detail of the outline. Pass box: true:

def obj = LatexMathObject.make("Look here!")
    .center().scale(2)
scene.add(obj)

// Over the full (busy) outline
highlight(obj: obj, type: "contour")

// Over the bounding box instead (gap 0.1 around the box)
highlight(obj: obj, type: "contour", box: true, gap: 0.1, color: "green")
scene.waitSeconds(2)

ContourHighlight2

In plain Groovy: ContourHighlight.make(2, obj) and ContourHighlight.makeBBox(2, .1, obj).

Highlighting Points

You can also highlight Point objects: a circle with radius equal to the point's thickness is drawn. Here we build three highlights with run: false and fire them together with animGroup so they play at the same time:

def P1 = Point.at(-.5, 0)
    .dotStyle(DotStyle.CROSS)
    .thickness(30)
    .drawColor("blue")

def P2 = Point.at(0, 0)
    .dotStyle(DotStyle.TRIANGLE_UP_FILLED)
    .thickness(50)
    .drawColor("tomato")

def P3 = Point.at(.5, 0)
    .dotStyle(DotStyle.CIRCLE)
    .thickness(30)
    .drawColor("black")

scene.add(P1, P2, P3)

def h1 = highlight(obj: P1, type: "contour", color: "gold",  run: false)
def h2 = highlight(obj: P2, type: "contour", color: "blue",  run: false)
def h3 = highlight(obj: P3, type: "contour", color: "green", run: false)
animGroup(anims: [h1, h2, h3])
scene.waitSeconds(1)

ContourHighlight3

In plain Groovy: ContourHighlight.make(2, P1).setColor("gold") (and the same for the others), then play.run(anim1, anim2, anim3).


Aligning Objects

The animAlign(...) command animates the alignment of one object with another (the animated counterpart of the align() method). Here six labels slide onto the edges and centers of a big square. We build each with run: false and fire them together with animGroup:

def upper = LatexMathObject.make("upper")
def lower = LatexMathObject.make("lower")
def left = LatexMathObject.make("left")
def right = LatexMathObject.make("right")
def hcenter = LatexMathObject.make("center")
def vcenter = LatexMathObject.make("vcenter")
def center = Shape.square().scale(3).fillColor("lightblue")

scene.add(center)
camera.adjustToAllObjects()

def a1 = animAlign(obj: left,    dst: center, type: "left",    runtime: 3, run: false)
def a2 = animAlign(obj: right,   dst: center, type: "right",   runtime: 3, run: false)
def a3 = animAlign(obj: upper,   dst: center, type: "upper",   runtime: 3, run: false)
def a4 = animAlign(obj: lower,   dst: center, type: "lower",   runtime: 3, run: false)
def a5 = animAlign(obj: hcenter, dst: center, type: "hcenter", runtime: 3, run: false)
def a6 = animAlign(obj: vcenter, dst: center, type: "vcenter", runtime: 3, run: false)
animGroup(anims: [a1, a2, a3, a4, a5, a6])
scene.waitSeconds(1)

alignAnimation

type accepts left, right, upper, lower, hcenter or vcenter.

In plain Groovy: animAlign(obj: left, dst: center, type: "left", runtime: 3) is Commands.align(3, center, AlignType.LEFT, left); run several at once with play.run(anim1, anim2, ...).

Sibling commands: animStackTo and animSetLayout

Two related commands animate the positioning tools from the Transforming objects chapter: animStackTo (animated stack) and animSetLayout (animated setLayout). Like every anim* command they play immediately unless run: false is given, and always return the animation:

// Stack a square to the right of a circle, with a gap
animStackTo(obj: sq, dst: circle, anchor: "right", gap: 0.1)

// Lay out the elements of a group in a row to the right, but don't play yet;
// store it in a variable and play it later with play.run(anim)
def anim = animSetLayout(obj: g, layout: "right", gap: 0.1, run: false)

Both animStackTo and animSetLayout accept a single object or a List; layout also admits a GroupLayout (as returned by the layout(...) DSL) or an inline spec map like [type: "circular", center: [0, 0], radius: 2].


Moving Along a Path

The MoveAlongPath animation moves an object along a specified path. You can use either a Shape or JMPath object to define the path.

Path Movement Parameters

The animation accepts two important boolean parameters:

  1. Rotation: Whether the object should rotate to match the tangent of the path
  2. Parameterization:
  3. true - Uses arc-length parameterization for constant velocity along the path
  4. false - Uses standard Bézier parameterization (slower at sharp turns)

Example: Comparing Parameterizations

// Create a path shaped like a piece of tangerine
def pathShape = Shape.circle().fillColor("orange")  // A circle (4 Bézier curves)
pathShape.get(2).shift(1, 0)  // Move left point to create a wedge shape
scene.add(pathShape)

// Blue square: moves with Bézier coordinates
def blueSquare = Shape.square()
.scale(.1)
.fillColor("darkblue").fillAlpha(.4)

// Red square: uses arc-length for constant velocity
def redSquare = blueSquare.copy()
    .fillColor("darkred").fillAlpha(.4)

// Animation with constant velocity (arc-length parameterization)
def anim=animMoveAlongPath(
    runtime: 5, // Duration in seconds
    obj: blueSquare, // Object to move
    path: tangerine, // Path to follow
    anchor: "center",// Center of object matches the path
    rotation: 'rotate',  // Rotate object to match tangent
    parametrized: true,// Use arc-length (constant velocity)
    lambda: "linear", // Linear timing function
    run: false //Don't play the animation yet!
)

def anim2=animMoveAlongPath(
    runtime: 5, // Duration in seconds
    obj: blueSquare, // Object to move
    path: tangerine, // Path to follow
    anchor: "center",// Center of object matches the path
    rotation: 'rotate',  // Rotate object to match tangent
    parametrized: false,//  Use Bézier parameterization
    lambda: "linear", // Linear timing function
    run: false //Don't play the animation yet!
)

animGroup(anims: [anim, anim2])   // play both at the same time

moveAlongpath

Note: We specified lamda: "linear"for uniform velocity. Lambda functions are covered in detail in the next chapter.

Tip: Although FunctionGraph is a subclass of Shape and supports MoveAlongPath, it's recommended to use the PointOnFunctionGraph object when animating a point along a function graph.

Tip: Try adding the method camera.registerUpdater(FollowObject.make(blueSquare)) just before the play.run command to keep the camera always centered on the blue square.

The same animations in Groovy syntax can be defined:

def anim = MoveAlongPath.make(
    5,                      // Duration in seconds
    tangerine,              // Path to follow
    blueSquare,             // Object to move
    AnchorType.CENTER,      // Center of object matches the path
    RotationType.ROTATE,    // Rotate object to match tangent
    true                    // Use arc-length (constant velocity)
).setLambda(t -> t)         // Linear timing function

// Animation with Bézier parameterization
def anim2 = MoveAlongPath.make(
    5,                      // Duration in seconds
    tangerine,              // Path to follow
    redSquare,              // Object to move
    AnchorType.LEFT,        // Left side of object matches the path
    RotationType.FIXED,     // Don't rotate object
    false                   // Use Bézier parameterization
).setLambda(t -> t)         // Linear timing function

The ShowCreation Animation

The ShowCreation animation draws an object and adds it to the scene. Different strategies are used depending on the object type, specified in the ShowCreationStrategy enum. The strategy is chosen automatically but can be overridden with setStrategy().

Warning: Forcing a specific strategy may cause errors for incompatible object types.

Example: Creating a Square

Use appear(...) with type: "draw" (aliases: showcreation, sketch):

def sq = Shape.square()
    .fillColor("#87556f").center()

appear(obj: sq, type: "draw", runtime: 2)  // Draws sq in 2 seconds
scene.waitSeconds(1)

showCreation1

For a simple shape like this, the SIMPLE_SHAPE_CREATION strategy is used.

In plain Groovy: play.showCreation(2, sq) (or play.run(ShowCreation.make(2, sq))).

Example: Creating a Math Formula

When creating a MultiShapeObject (like a LaTeX formula), a small delay is added between each shape:

def text = LatexMathObject.make(r"$a^2+b^2=c^2$")
    .center().scale(3)

appear(obj: text, type: "draw", runtime: 2)
scene.waitSeconds(1)

showCreation2

Tip: Try using a longer duration (10 seconds) to see the animation details: the contour is drawn first, then the glyphs are filled.

Creation Strategies

Specific creation strategies exist for different object types: - Axes - Arrows - Delimiters - Simple shapes - Multi-shape objects


The Transform Animation

The Transform class smoothly transforms one Shape object into another. In the DSL it is the morph(...) command (its default type: "auto" picks the best strategy for you).

Basic Transform Example

def circle = Shape.circle()
    .shift(-1, 0).scale(.5)

def pentagon = Shape.regularPolygon(5)
    .shift(.5, -.5).style("solidblue")

morph(from: circle, to: pentagon, runtime: 3)
scene.waitSeconds(3)

transform1

In plain Groovy: morph(from: circle, to: pentagon, runtime: 3) is play.transform(3, circle, pentagon).

Note: The transform animation also interpolates drawing parameters like thickness and color.

Important: When transformation from A to B is complete, A is removed from the scene and B is added. Use object B for any subsequent operations.

Understanding Transform Steps

This example shows intermediate transformation states:

def triangle = shape(
      type: "regularpolygon",
      sides: 3,
      style: [drawColor: 'red', fillColor: 'gold', thickness: 20],
      transform: [rotate: PI/6, scale: [.5, 1]]
)
def pentagon = shape(
      type: "regularpolygon",
      sides: 5,
      style: [drawColor: 'blue', fillColor: 'violet'],
      transform: [rotate: PI/4],
      stack: [to: triangle, destinyanchor: "right", gaps: 5],
)
// Create the transformation animation
def anim = Transform.make(2, triangle, pentagon)
anim.setLambda(t -> t)  // Constant velocity
anim.initialize()

for (t in linspace(0, 1, 6)) {//From 0 to 1 in 6 steps
    // Compute the animation at time t
    anim.doAnim(t)
    // Get the intermediate object
    def intermediate = anim.getIntermediateObject().copy()
    //// Add descriptive text below the intermediate object
    def lat = latex(
          text: "{\\tt t=$t}",
          stack: [to: intermediate, destinyanchor: "lower", relativegaps: .5],
    )
    // Add both elements to the scene
    scene.add(intermediate, lat)
}
// Ensure everything is visible
camera.adjustToObjects(triangle, pentagon)
// Save the result
scene.saveImage("intermediateSteps.png")

intermediateSteps


Transform Strategies

The transformation method depends on the source and destination object types. For example, when both shapes are regular polygons with the same number of sides, a similarity transform is used:

def pentagon = shape(type: "regularPolygon", sides: 5,
                     transform: [scale: 0.5, shift: [-1, -1]],
                     style: "solidOrange")

def pentagonDst = shape(type: "regularPolygon", sides: 5,
                        transform: [scale: 0.8, shift: [0.5, -0.5], rotate: 45*DEGREES],
                        style: "solidBlue")

morph(from: pentagon, to: pentagonDst, runtime: 3)
scene.waitSeconds(1)

transform2

The similarity method ensures the object doesn't get distorted during transformation.

In plain Groovy: morph(from: pentagon, to: pentagonDst, runtime: 3) is play.run(Transform.make(3, pentagon, pentagonDst)).

Available Transform Strategies

You can force a specific strategy using .setTransformMethod(method):

Warning: Forcing an incompatible strategy may cause errors or prevent animation.

Currently implemented strategies:

  1. INTERPOLATE_SIMPLE_SHAPES_BY_POINT - Point-by-point interpolation for simple shapes (single connected component like squares or circles). Allows path optimization for smoother animation.

  2. INTERPOLATE_POINT_BY_POINT - General interpolation converting shapes to canonical form. Works with multiple components (e.g., letter "B" has 3 components).

  3. SIMILARITY_TRANSFORM - Creates a direct similarity between shapes. The first two points of the source transform to the first two points of the destination. (The former ISOMORPHIC_TRANSFORM value still works but is deprecated; prefer SIMILARITY_TRANSFORM.)

  4. ROTATE_AND_SCALEXY_TRANSFORM - Similar to a similarity but with non-uniform scaling. Used for rectangle-to-rectangle transforms to prevent distortion.

  5. FUNCTION_INTERPOLATION - Interpolates between function graphs, x-to-x.

  6. MULTISHAPE_TRANSFORM - For transforming MultiShape objects (like LaTeXMathObject).

  7. GENERAL_AFFINE_TRANSFORM - Like a similarity transform but accepts general affine transformations. First three points of source map to first three points of destination.

  8. ARROW_TRANSFORM - Specialized for transforming arrows, using a similarity transform while properly handling arrow heads.

Comparing Strategies

This example shows why the correct strategy matters:

def sq = Shape.square()
    .center().style("solidRed")

def sq2 = Shape.square()
    .scale(.25, 1).style("solidGreen")
    .rotate(45 * DEGREES)
    .moveTo(Point.at(1, 0))

// Forcing GENERAL_AFFINE_TRANSFORM (not ideal for rectangles)
def tr = Transform.make(10, sq, sq2)  // 10 seconds to see details
tr.setTransformMethod(Transform.TransformMethod.GENERAL_AFFINE_TRANSFORM)
play.run(tr)
scene.waitSeconds(3)

TransformStrategies01

Notice the intermediate steps aren't natural rectangles. This is why ROTATE_AND_SCALEXY_TRANSFORM exists.

Best Practice: Let JMathAnim choose the strategy automatically by removing the setTransformMethod() call:

TransformStrategies02


Flip Transforms

A simpler transformation that works with any MathObject is the flip, reached with morph(..., type: "flip"). It scales the first object to 0 (horizontally, vertically, or both) then scales the second one from 0 to 1, creating a flipping effect.

Flip Orientations

  • horizontal - Flip left-to-right
  • vertical - Flip top-to-bottom
  • both - Flip both directions

Example: Flipping Text

def text = LatexMathObject.make("JMathAnim")
def flips = ["horizontal", "vertical", "both"]

// Center all glyphs on screen
// Note: MultiShapeObject and subclasses are iterable
for (s in text) {
    s.center()
}

camera.zoomToObjects(text)

def previous = null
int index = 0

for (s in text) {
    if (previous != null) {
        morph(from: previous, to: s, type: "flip", orientation: flips[index], runtime: 2)
        index = (index + 1) % 3
    }
    previous = s
}

flipAnimation

In plain Groovy: morph(from: previous, to: s, type: "flip", orientation: "horizontal", runtime: 2) is play.run(FlipTransform.make(2, OrientationType.HORIZONTAL, previous, s)).


Animating Style Changes

Beyond position and shape, you can animate visual properties (style) of objects.

The setColor Animation

Animates color changes. You can specify draw color, fill color, or both. Set to null to leave a color unchanged.

Example: Color Transitions

def circle = shape(
    type: "circle",
    style: [thickness: 8]
)

appear(
    runtime: 1,
    type: "showcreation",
    obj: circle
)

scene.waitSeconds(1)

// Animate fill color to violet (draw color unchanged)
animStyle(
    runtime: 2,
    obj: circle,
    style: [fillcolor: "violet"]
)


scene.waitSeconds(1)

// Animate to the "solidorange" style
animStyle(
    runtime: 2,
    obj: circle,
    style: "solidorange"
)

// Animate to a gradient fill and fixed draw color
def gradient = radialGradient(
    center: [0.25, 0.75],
    radius: 0.5,
    0.0: "white",
    1.0: "brown"
)

animStyle(
    runtime: 2,
    obj: circle,
    style: [drawColor: "steelblue", fillColor: gradient]
)

scene.waitSeconds(3)

setColorAnimation


AffineTransform Related Animations

These animations provide animated versions of the affine transformations discussed in the transforming objects chapter.

Affine Transform

Animates a general affine transformation:

axes(//Create an add axes to the scene
      xRange: [-2, 2],
      yRange: [-2, 2],
      style: [thickness: 6, drawColor: 'darkblue', layer: 1]
)

//Create a grid but don't add it to the scene
def gridAux = grid(
      steps: [1, 1],
      divisions: [2, 2],
      center: [0, 0],
      addToScene: false
)
//Get the lines of the grid as a MathObjectGroup
//so we can transform them
def grid = gridAux.getMathObject()

// Create a "B" glyph
def bigB = latex(
      text: "B",
      transform: [height: 1, center: true],
      style: [name: "solidOrange", fillAlpha: .5],
)

// Define transformation points
def A = Point.at(0, 0).drawColor("blue")
def B = Point.at(1, 0).drawColor("blue")
def C = Point.at(0, 1).drawColor("blue")
def D = Point.at(0, .5).drawColor("red")
def E = Point.at(1, 0).drawColor("red")
def F = Point.at(1, 1).drawColor("red")

scene.add(A, B, C, D, E, F)

// Animate creation
appear(obj: [grid, bigB], type: "draw")
scene.waitSeconds(1)

// Animate the affine transform (A,B,C) → (D,E,F)
animAffine(
      type: "affine",
      runtime: 3,
      obj: [grid, bigB],
      origin: [A, B, C],
      destiny: [D, E, F]
)
scene.waitSeconds(1)

affineAnimation2

This animation interpolates element-by-element from the identity matrix to the transformation matrix. For special cases (reflection, similarity), JMathAnim uses optimized algorithms for better visual results.


Reflection

Animates a reflection that maps point A to point B:

axes(
      xRange: [-2, 2],
      yRange: [-2, 2],
      style: [thickness: 6, drawColor: 'darkblue', layer: 1]
)

//Create a grid but don't add it to the scene
def gridAux = grid(
      steps: [1, 1],
      divisions: [2, 2],
      center: [0, 0],
      addToScene: false
)
//Get the lines of the grid as a MathObjectGroup
def grid = gridAux.getMathObject()

// A pentagon
def reg = shape(
      type: "regularpolygon",
      sides: 5,
      transform: [center: true],
      style: [fillColor: 'steelblue']
)

// Text label
def text=latex(
    text: "Pentagon",
    transform: [center: true, height: .5],
    style: [name: "solidOrange",fillAlpha: .5, layer: 1],
)

// Origin and destination points for reflection
def A = reg.getPoint(0).drawColor("blue").copy()  // Copy of first vertex
def B = Point.at(1, .5).drawColor("red")

// Add everything to the scene
scene.add(A, B, grid, text)

// Define and play the reflection animation
animAffine(
    type: "reflection",
    runtime: 3,
    obj: [reg, text, grid],
    origin: A,
    destiny: B
)
scene.waitSeconds(2)

reflection1Anim

Note: Point A is also transformed since it's part of the shape.

Reflection by Axis

Animates a reflection with a specified axis:

def reg1=shape(
    type: "regularpolygon",
    sides: 6,
    style: "solidred",
    transform: [center: true]
)

def reg2 = reg1.copy()
    .style("solidorange")

scene.add(reg1, reg2)
camera.scale(2)

// Use an edge of the hexagon as the reflection axis
def A = reg1.getPoint(1)
def B = reg1.getPoint(2)
line(
    a: A,
    b: B,
    style: [dashstyle: "dotted"],
    addToScene: true
)
animAffine(
    type: "reflectionByAxis",
    runtime: 3,
    obj: reg2,
    axis: [A,B]
)
scene.waitSeconds(2)

reflection2Anim


Similarity

Direct Similarity

Animates the direct similarity mapping (A,B) → (C,D):

def A = Point.origin().drawColor("blue")
def B = Point.at(1, 0).drawColor("blue")
def C = Point.at(1, .2).drawColor("red")
def D = Point.at(1.8, .6).drawColor("red")

def triangle = shape(
      type: "polygon",
      points: [A.copy(), B.copy(), [0, .5]],
      style: "solidBlue"
)

scene.add(triangle, A, B, C, D)

animAffine(
      runtime: 3,
      type: "similarity",
      obj: triangle,
      origin: [A, B],
      destiny: [C, D]
)
scene.waitSeconds(3)

directIso

How it works: JMathAnim creates the similarity as a composition of shifting, rotating, and uniform scaling, preserving the shape's form (only size changes).

The former Commands.isomorphism(...) method still works but is deprecated; prefer Commands.similarity(...).

Inverse Similarity

Available since version 0.9.9, this includes a reflection:

// Same setup as above, but use inverseSimilarity
animAffine(
      runtime: 3,
      type: "inverseSimilarity",
      obj: triangle,
      origin: [A, B],
      destiny: [C, D]
)

inverseIso

For the similarity and inverseSimilarity types, origin and destiny also accept a Rect, mapping one rectangle to the other. The gui editor has built autocomplete for most DSL commands. You can see a cheatsheet of DSL commands here


TwistTransform

Previous animations aimed for natural-looking intermediate steps when transforming object A into object B. However, when measurements must be preserved (like rectifying a circle arc), point-to-point interpolation won't work. That's why TwistTransform was created.

Purpose and Limitations

Available since version 0.9.12, TwistTransform creates "realistic transforms" that preserve measurements. It has specific requirements:

Requirements:

  • Both A and B must be shapes with straight segments
  • Both must have the same number of vertices
  • Vertices must be properly "aligned" (vertex 0 of A goes to vertex 0 of B, etc.)
  • Shapes should be open paths (closed shapes work, but intermediate steps may not appear closed)

Simple Example: Square to Segment

def sq = shape(
    type: "square",
    style: [drawColor: 'steelblue']
)

// Create a segment with 5 points (square has 5 points when opened)
// Same length as square sides
def seg = shape(
      type: "segment",
      from: [0, 0],
      to: [4, 0],
      numpoints: 5,
      style: [drawColor: 'firebrick']
)

camera.adjustToObjects(sq, seg)

// Twist transform: 5 seconds, from sq to seg, using point 0 as pivot
morph(
    type: "twist",
    runtime: 5,
    from: sq,
    to: seg,
    pivotal: 0
)

twist01

The square unfolds into a segment while preserving side lengths.

Understanding the Pivot Point

The pivot point parameter is crucial and produces different effects. The animation consists of:

  1. Shift - Moves the pivot point of source to the pivot point of destination
  2. Angle adjustment - Progressively modifies angles of segments from pivot point to match destination angles. Segments are lengthened/shortened as needed.
  3. Pre-pivot segments - Same process applied to segments before the pivot point

You can omit the pivot parameter; JMathAnim will use an approximation of the midpoint (size()/2).

Advanced: All three processes can be controlled with custom lambda functions.

Advanced Example: Rectifying a Semicircle

// Semicircle with 50 points (polygonal approximation, not Bézier curves)
def semiCirc = shape(
      type: "arc",
      angle: PI,
      numpoints: 50,
      style: [drawColor: 'steelblue'],
)
PathUtils.rectifyPath(semiCirc.getPath())  // Remove all curvature

// Create segment from (0,0) to (-PI,0)
// Note: (-PI,0) not (PI,0) so first point of segment matches arc's first point
// Using (PI,0) would make the arc "turn around" to match endpoints
def seg = shape(
      type: "segment",
      from: [0, 0],
      to: [-PI, 0],
      numpoints: 50,
      style: [drawColor: 'firebrick'],
      // Position segment below arc
      stack: [to: semiCirc,gaps: .25,destinyanchor: "lower"]
)

camera.centerAtObjects(semiCirc, seg)

morph(
    runtime: 5,
    type: "twist",
    from: semiCirc,
    to: seg
)
scene.waitSeconds(2)

twist02

This demonstrates the classic geometric problem of arc rectification, showing how a curved arc can be "unrolled" into a straight segment while preserving its length.


Transforming Math Expressions

LaTeX math expressions support a specialized animation called TransformMathExpression that allows fine-tuning transformations between LatexMathObject instances. This is covered in detail in the mathematical formulas chapter.


home back