home back

Advanced topics

A grab bag of tools that don't fit anywhere else but will save your day sooner or later: skipping already-tested parts of a long video, objects that update themselves, trails, sounds, and more. The first section alone is worth bookmarking.

Disabling and enabling animations

Suppose you are writing a rather long animation. Usually, this process involves several test runs to check if everything goes as planned. If you are fine tuning the last part of the animation, you don't need to run it all the way from the beginning to do this. Instead, you can add these methods to your code:

disableAnimations();
//...animation code that is already tested and don't need to see it again before generating the final movie
enableAnimations();
//...animation code that I want to preview

The disableAnimations() and enableAnimations() methods allow you to temporarily disable animations and frame generation. Updating and object creation are done, but the non-essential parts, like drawing, writing to a movie, or performing the animations, are omitted, dramatically increasing speed. You can also use this to generate a movie with only specific parts of the sketch.

Updaters

An updater is an object that automatically recomputes the state of a MathObject on every frame, based on other objects it depends on. Updaters are instances of the abstract class Updater, and they are attached to the object they modify with the MathObject methods registerUpdater(updater) and unregisterUpdater(updater).

An Updater subclass must implement the following methods:

//The objects this updater depends on. The scene uses this list to work out
//the correct update order, so that dependencies are always updated first.
public List<Versionable> getDependencies();

//Applied before the object's own update pass. Return true if something changed.
public abstract boolean applyBeforeUpdate();

//Applied after the object's own update pass. Return true if something changed.
public abstract boolean applyAfterUpdate();

The object being modified is available inside the updater through the getMathObject() method. Most updaters do their work in applyAfterUpdate() and simply return false from applyBeforeUpdate().

Returning the correct dependency list from getDependencies() is what keeps everything consistent: JMathAnim uses it to order the update queue, so an updater that reads from an object B is always run after B itself has been updated.

For example, let's suppose we have the following simple animation, where a Point object named A moves from the point (1, .5) to (-1, .5):

add(Axes.make(),Shape.circle());
Point A = Point.at(1, .5);
play.shift(3,-2,0,A);
waitSeconds(3);

We want a second point that automatically locates itself at the normalized coordinates of point A, that is, the projection of A onto the unit circle. We write an Updater that reads A and repositions its own object accordingly:

class NormalizeUpdater extends Updater {

    private final Point sourcePoint;

    public NormalizeUpdater(Point sourcePoint) {
        this.sourcePoint = sourcePoint;
    }

    @Override
    public List<Versionable> getDependencies() {
        //Declare that we read from sourcePoint, so that it is updated before us
        return Collections.singletonList(sourcePoint);
    }

    @Override
    public boolean applyBeforeUpdate() {
        return false;
    }

    @Override
    public boolean applyAfterUpdate() {
        double norm = sourcePoint.v.norm();
        if (norm == 0) return false;
        //Move our object to the normalized coordinates of sourcePoint
        getMathObject().moveTo(sourcePoint.v.copy().scale(1 / norm));
        return true;
    }
}

and modify the scene, attaching an instance of this updater to a new point:

add(Axes.make(), Shape.circle());
Point A = Point.at(1, .5);
Point B = Point.origin().drawColor("red");
B.registerUpdater(new NormalizeUpdater(A));
add(B);
play.shift(3, -2, 0, A);
waitSeconds(3);

Generates the following animation:

Updater01

Predefined updaters

JMathAnim has some built-in updaters that maybe useful:

Camera always adjusted to objects

With the AlwaysAdjusted updater, you can force the camera to show all objects in the scene. The camera will zoom out when needed, but not zoom in. It is registered on the camera itself, and admits the horizontal and vertical gaps, plus an optional varargs of the objects to keep visible (if none is given, all objects in the scene are considered). For example:

camera.registerUpdater(AlwaysAdjusted.make(.1, .1));

See the Cameras chapter for more details on camera updaters.

Stacks permanently an object to another

Shape circ1=Shape.circle().scale(.3).fillColor("red").thickness(8);
Shape circ2=circ1.copy();
Shape circ3=circ1.copy();
Shape circ4=circ1.copy();
Shape sq=Shape.square().center().thickness(8);
add(sq,circ1,circ2,circ3,circ4);

//Stacks permanently the LEFT of circ1 with the RIGHT of sq
circ1.registerUpdater(StackToUpdater.make(sq).withDestinyAnchor(AnchorType.RIGHT));

//Stacks permanently the RIGHT of circ2 with the LEFT of sq
circ2.registerUpdater(StackToUpdater.make(sq).withDestinyAnchor(AnchorType.LEFT));

//Stacks permanently the LOWER of circ3 with the UPPER of sq
circ3.registerUpdater(StackToUpdater.make(sq).withDestinyAnchor(AnchorType.UPPER));

//Stacks permanently the UPPER of circ4 with the LOWER of sq
circ4.registerUpdater(StackToUpdater.make(sq).withDestinyAnchor(AnchorType.LOWER));

play.rotate(3, 90*DEGREES, sq);
waitSeconds(3);

Updater02

Trail

A trail is a Shape subclass that updates every frame, adding the position of a marker point. Let's draw a cycloid using a combined shift and rotate animation:

double circleRadius = .25;
Shape circle = Shape.circle()
    .scale(circleRadius)
    .fillColor("royalblue")
    .stack()
    .toScreen(ScreenAnchor.LEFT)
    .rotate(-90 * DEGREES);//Rotate it so that point 0 touches the floor

//By default a circle shape has 4 point, so point 0 and 2 make a diameter
Shape diameter = Shape.segment(circle.getPoint(0), circle.getPoint(2)).layer(1).thickness(3);
//Note that, as diameter is created with point instances of the Shape circle, we don't need to animate diameter, only circle

//The "floor". An horizontal line that we put right under the circle
Line floor = Line.XAxis()
    .stack()
    .withDestinyAnchor(AnchorType.LOWER)
    .toObject(circle);
add(floor, diameter);//Add everyhing (no need to add circle because it will automatically added with the shift and rotate animation)

Trail trail = Trail.make(circle.getPoint(0));//The Trail object
trail.layer(1)
    .thickness(6)
    .drawColor(JMColor.parse("tomato"));
add(trail);
//Ok, time to move this!
Animation shift = Commands.shift(10, 4 * PI * circleRadius, 0, circle).setLambda(t -> t);
Animation rotate = Commands.rotate(10, -4 * PI, circle).setUseObjectState(false).setLambda(t -> t);
playAnimation(shift, rotate);
waitSeconds(1);
trail01

The addOnce method

A useful method when creating procedural animations is the addOnce(obj) method. This method adds the specified object(s) to the scene but removes them after they are drawn, so they only "live" for a frame. This method may be useful when you need to create an object for every frame, draw it, and remove it because you will use another object in the next frame.

Current status of methods implemented to MathObjects

Not all MathObject and Animation combinations are compatible. Below is a table that shows, at the current version of the library, what you can and cannot do:

MathObject Affine transforms related: Shift, scale, rotate , grow in, shrink out, highlight ShowCreation animation Transform animation
Point Yes Yes (fadeIn is used) No
Shape Yes Yes Yes
Line Yes Yes Yes
Axes No Yes No
LatexMathObject Yes Yes Yes (also you can use the specialized TransformMathExpression method)
Arrow Yes Yes Yes (delegates in the similarity transform)
Delimiter No (you have the transform the anchor points instead) Yes No (transform anchor points instead)

Sounds

Since version 0.9.7-SNAPSHOT, JMathAnim can add sounds to created videos. To do so, an external ffmpeg executable is needed. You can define the path where this executable is at the setupSketch() method with the command config.setFfmpegExecutable(path) where path is a String with the full path to the ffmpeg executable, like C:\ffmpeg\bin\ffmpeg.exe in Windows or /usr/bin/ffmpeg in Linux.

To add a sound to a specific moment of the animation, you can use the command playSound. For example

playSound("pop.wav");

Will add the given sound at the current frame. Note that adding a sound doesn't stop the animations. They are simply added to the current frame.

The sound file is loaded using the ResourceLoader class, so usual conventions are used. In this case, JMathAnim will look for the file pop.wav in the directory project_dir/resources/sounds. Remember that you can use the "!" modifier to specify an absolute path.

As ffmpeg is used as an external command to process the sound files, all the most common formats are supported, like wav, mp3, ogg or flac.

When adding a sound to the animation, and after the video is created, JMathAnim will process all the added sounds and merge them into the created video, so extra time will be spent. If you don't want to add any sound at all to the animation, you can disable it with the config command:

config.setSoundsEnabled(false);

Another method to add a sound is with the animation PlaySoundAt. This is useful when you want to play a sound at a specific time in an animation. For example, suppose you have this animation of a square moving and rotating from the previous chapters, where rotating happens between 40% and 60% of the animation.

Shape sq = Shape.square().scale(.5).style("solidblue").moveTo(Point.at(-1, 0));
AnimationGroup ag = AnimationGroup.make(
    Commands.shift(6, 2, 0, sq),
    Commands.rotate(6, PI * .5, sq)
            .setUseObjectState(false)
            .setLambda(UsefulLambdas.smooth().compose(UsefulLambdas.allocateTo(.4, .6)))
);
playAnimation(ag);

Suppose you want the square to translate quietly, but the rotation makes a rotationSound.mp3 , which is located at the home_project/resources/sounds directory. You can achieve this if you define this animation and play it with the original one:

 playAnimation(ag, PlaySoundAt.make(6, .4, "rotationSound.mp3"));

Another (wrong) way of achieving this may be using lambdas, using the following definition:

PlaySoundAt.make(6, 0, "rotationSound.mp3").setLambda(UsefulLambdas.allocateTo(.4, .6));

but if you create the animation, the sound will be played at the start of the animation. What happened here? Well, the PlaySoundAt.make defines an animation that will play the sound when runtime is greater or equal to the given time, in this case, 0. The allocate function evaluated at any t<.4 will return 0, so the animation will play the sound at the first animation frame.

To prevent this, the makeStrict method creates an animation where the sound will be played after the runtime parameter is strictly greater than a given one. So, if you want to make the following code right, you should use:

PlaySoundAt.makeStrict(6, 0, "rotationSound.mp3").setLambda(UsefulLambdas.allocateTo(.4, .6));

home back