ghome back

Examples gallery

The animations in this channel are mostly made with JMathAnim, except for some simple things made with Powerpoint. Kdenlive was used for video editing. In Spanish with English subtitles: El Taxista de Hardy

Function and its points with zero slope

A simple animation illustrating the updateable object PointOnFunctionGraph :

   def axes = Axes.make(-2, 2)//Axes with unit marks from -2 to 2, both vertically and horizontally
scene.add(axes)

//The function
def cat = FunctionGraph.make(x -> (x - 1) * (x + 1) * (x - .5))
cat.thickness(3).drawColor("darkblue")
//The roots of f'(x)
double root1 = -0.4342585459107
double root2 = 0.767591879244

double xStart = -1
def A = PointOnFunctionGraph.make(xStart, cat)
A.style("redcircle")

def tangentLine = Line.make(A, A.getSlopePointRight())
    .drawColor("darkgreen")

//First, we animate the creation of the objects
play.showCreation(cat)
play.fadeIn(A)
play.showCreation(tangentLine)

//Now we shift the point A to the first root and fadeIn a illustrative text and a marker point
play.shift(5, root1 - xStart, 0, A)
def marker1 = A.copy().dotStyle(DotStyle.CROSS)
    .drawColor("darkblue")

def text1 = LatexMathObject.make(r"$$f'(x)=0$$")
    .scale(.5)
    .stack()
    .withGaps(.1)
    .withDestinyAnchor(AnchorType.UPPER)
    .toObject(A)

play.fadeIn(2, text1, marker1)

//We do the same with the second root
play.shift(5, root2 - root1, 0, A)
def marker2 = A.copy()
    .dotStyle(DotStyle.CROSS)
    .drawColor("darkblue")

def text2 = LatexMathObject.make(r"$$f'(x)=0$$")
    .scale(.5)
    .stack()
    .withGaps(.1)
    .withDestinyAnchor(AnchorType.LOWER)
    .toObject(A)

play.fadeIn(2, text2, marker2)
play.shift(5, 1, 0, A)
scene.waitSeconds(3)

You can see the video here

Taylor expansion of sin(x)

//Function definitions
public int factorial(int n) {
    int resul = 1
    for (int i = 1; i <= n; i++) {
        resul *= i
    }
    return resul
}

final DoubleUnaryOperator TaylorExpansionSin(int order) {
    return { double x ->
        double resul = 0d
        double potX = x
        int sign = 1
        for (int n = 0; n < order; n++) {
            int k = 2 * n + 1
            resul += potX / factorial(k) * sign
            sign = -sign
            potX *= x * x
        }
        return resul
    } as DoubleUnaryOperator
}

//Main script
int orderTaylor = 8
Axes axes = Axes.make()
axes.addXTicksLegend(r"$\pi$", PI, TickType.PRIMARY, 0)
axes.addXTicksLegend(r"$-\pi$", -PI, TickType.PRIMARY, 0)
axes.addXTicksLegend(r"$2\pi$", 2 * PI, TickType.PRIMARY, 0)
axes.addXTicksLegend(r"$-2\pi$", -2 * PI, TickType.PRIMARY, 0)
axes.generatePrimaryYTicks(-4, 4, 1)
scene.add(axes)
final double xmin = -2 * PI - .2
final double xmax = 2 * PI + .2
def sinFunction = FunctionGraph.make(t -> Math.sin(t), xmin, xmax)
    .thickness(25).drawColor("#682c0e")

camera.adjustToObjects(sinFunction)

play.showCreation(sinFunction)
scene.waitSeconds(1)

def taylor = new FunctionGraph[orderTaylor]
def texts = new LatexMathObject[orderTaylor]
for (int n = 1; n < orderTaylor; n++) {
    taylor[n] = FunctionGraph.make(TaylorExpansionSin(n), xmin, xmax)
        .drawColor("#153e90").thickness(15)
    texts[n] = LatexMathObject.make("Taylor order " + (2*n-1))
        .scale(3)
        .stack().withGaps(.2).toScreen(ScreenAnchor.UPPER_LEFT)
    texts[n]
        .color("#153e90")
        .layer(2)
}
final Rect r = texts[1].getBoundingBox().addGap(.4, .1)
Shape box = Shape.rectangle(r)
    .fillColor("white")
    .thickness(3)
    .layer(1)

play.showCreation(taylor[1], texts[1], box)
for (int n = 2; n < orderTaylor; n++) {
    scene.add(taylor[n - 1].copy()
            .thickness(1)
            .drawColor("gray")
    )
    Transform transformFunction = Transform.make(2, taylor[n - 1], taylor[n])
    Transform transformText = Transform.make(2, texts[n - 1], texts[n])
    play.run(transformFunction, transformText)
}
scene.waitSeconds(5)

You can see the video here.

Trifolium into Freeth's Nephroid (and back)

Two parametric curves expressed in polar coordinates. Note that we made a copy of the first one as it becomes altered after the first transform.

scene.add(Axes.make())
def trifolium = ParametricCurve.makePolar(t -> 2 * Math.cos(t) * (4 * Math.sin(t) * Math.sin(t) - 1), t -> t, 0, PI)
def nephroid = ParametricCurve.makePolar(t -> 1 + 2 * Math.sin(t / 2), t -> t, 0, 4 * PI)

trifolium
    .drawColor("#153e90")
    .thickness(15)

nephroid
    .drawColor("#a05344")
    .thickness(15)

def trifoliumCopy = trifolium.copy()

camera.adjustToObjects(trifolium, nephroid)
play.transform(5, trifolium, nephroid)
play.transform(5, nephroid, trifoliumCopy)

Here you have a GIF from the movie generated:

Trifollium

Trail curve derived from Trifolium

For each point of the Trifolium, we add the derivative vector rotated 90 degrees clockwise, and draw the trail. The animation is done with the advanceFrame() method, and we register an instance of CameraAlwaysAdjusting class to adjust the camera to all objects in the scene.

def trifolium = ParametricCurve.makePolar(t -> 2 * Math.cos(t) * (4 * Math.sin(t) * Math.sin(t) - 1), t -> t, 0, PI)

//The original coordinates of the points are irrelevant as they will be updated
//prior to drawing the first frame.
def pointOnCurve = Point.at(0, 0)
    .drawColor("darkblue")
Point pointToTrail = Point.at(0, 0)

def arrow = Arrow.make(pointOnCurve, pointToTrail, ArrowType.ARROW1)
    .drawColor("darkblue").layer(1)
scene.add(trifolium.thickness(15), pointOnCurve, arrow)

def derivedCurve = Trail.make(pointToTrail)
    .drawColor("darkred")
    .dashStyle(DashStyle.DASHED)

scene.add(derivedCurve)
camera.scale(2.5)
dt=scene.getDt()
double time = 10
for (double t = 0; t < time; t += dt) {
    final double t0 = PI * t / time
    Vec v = trifolium.getFunctionValue(t0)
    pointOnCurve.moveTo(v)
    Vec deriv = trifolium.getTangentVector(t0, 1).normalize()
    Vec normal = deriv.copy().rotate(-90 * DEGREES)
    pointToTrail.copyStateFrom(pointOnCurve.add(normal))
    scene.advanceFrame()
}

Here you have a GIF from the movie generated:

Trifollium

The trail of the vertices of a square rotating over a line

def sq = Shape.square()
    .scale(.2)
    .stack()
    .withOriginAnchor(AnchorType.LOWER_AND_ALIGNED_LEFT)
    .toScreen(ScreenAnchor.LEFT)
scene.add(Line.XAxis(), sq)
int n = 1
scene.add(Trail.make(sq.getPoint(0)).drawColor("red").thickness(15)
    , Trail.make(sq.getPoint(1)).drawColor("blue").thickness(15)
    , Trail.make(sq.getPoint(2)).drawColor("green").thickness(15)
    , Trail.make(sq.getPoint(3)).drawColor("gray").thickness(15)
)
for (int k = 0; k < 30; k++) {
    play.rotate(.5, sq.getPoint(n), -90 * DEGREES, sq)
    n++;
}
scene.waitSeconds(1)

Here is a GIF from the movie generated:

rotatinSquare

The Koch curve

//Function definition
private  getNextKochIteration(previousShape) {
    //A new iteration of the Koch curve is composed of 4 copies of the previous iteration
    //scaled 1/3.
    def s1 = previousShape.copy()
    def s1Path=s1.getPath()
    s1Path.scale(previousShape.get(0), 1d / 3, 1d / 3)

    def s2 = s1Path.copy() //A copy of s1...
        .rotate(s1.get(0), PI / 3)//rotated 60 degrees around left point of s1...
        .shift(s1.get(0).to(s1.get(-1)))//and moved to start at right point of s1...

    def s3 = s1Path.copy()//A copy of s1...
        .rotate(s1.get(0), -PI / 3)//rotated -60 degress around left point of s1...
        .shift(s1.get(0).to(s2.get(-1)))//and moved to start at right point of s2...

    def s4 = s1Path.copy()//A copy of s1...
        .shift(s1.get(0).to(s3.get(-1)));//moved to start at right point of s3..

    s1.merge(s2, true, false).merge(s3, true, false).merge(s4, true, false)//merge all
    return s1;
}

//Main script
int numIters = 6
def koch = new Shape[numIters]
koch[0] = Shape.segment(Point.origin(), Point.unitX()).thickness(1)
for (int n = 1; n < numIters; n++) {
    koch[n] = getNextKochIteration(koch[n - 1])
}
camera.zoomToObjects(koch[numIters - 1])
for (int n = 1; n < numIters; n++) {
    play.transform(3, koch[n - 1], koch[n])
}
scene.waitSeconds(5)

You can see the video here.

The Hilbert curve

//Function definition
private getNextHilbert(box, previous) {
    //Next iteration of hilbert is done scaling the previous to 50%,
    //making 4 copies and merging them properly rotated and positioned

    Shape nextHilbert = previous.copy()
    nextHilbert.getPath().scale(box.get(1), .5, .5)
    //Creates a reflection about a diagonal of s1 and apply to it
    AffineJTransform reflection1 = AffineJTransform.createReflectionByAxis(
        nextHilbert.getBoundingBox().getUpperRight(),
        nextHilbert.getBoundingBox().getLowerLeft(),
        1);
    reflection1.applyTransform(nextHilbert);

    JMPath s2 = previous.getPath().copy().scale(box.get(2), .5, .5)
    JMPath s3 = previous.getPath().copy().scale(box.get(3), .5, .5)
    JMPath s4 = previous.getPath().copy().scale(box.get(0), .5, .5)

    //Creates a reflection about the other diagonal of s4 and apply to it
    final AffineJTransform reflection2 = AffineJTransform.createReflectionByAxis(
        s4.getBoundingBox().getLowerRight(),
        s4.getBoundingBox().getUpperLeft(),
        1)
    s4.applyAffineTransform(reflection2)

    //Now merge everything
    nextHilbert.merge(s2, true, false).merge(s3, true, false).merge(s4, true, false)
    return nextHilbert
}

//Main script
int numIters = 7
def unitSquare = Shape.square().center().scale(-1, 1)
unitSquare.thickness(1).drawColor("#153e90").fillColor("#FDFDFD")

def hilbert = unitSquare.copy().scale(.5).rotate(-90 * DEGREES)
hilbert.get(0).setSegmentToThisPointVisible(false)

camera.setGaps(.1, .1)
camera.zoomToObjects(unitSquare)

def previous = hilbert
scene.add(unitSquare)
for (int n = 0; n < numIters; n++) {
    hilbert = getNextHilbert(unitSquare, hilbert)
    play.transform(2, previous, hilbert)
    previous = hilbert
}
scene.waitSeconds(3)

You can see the video here.

The Tusi couple

int numPoints = 10//Number of points/diameters to show

//The outer circle
def outerCircle = Shape.circle()
    .thickness(15)
    .drawColor("blue")

//This shape (outerCircle scaled 50%) is the path that will follow the rotatingCircle group
def path = outerCircle.copy().scale(.5)

//Add the diameters
for (int n = 0; n < numPoints * 2; n++) {
    Point p1 = Point.at(outerCircle.getVecAt(.5d * n / numPoints))
    Point p2 = Point.at(outerCircle.getVecAt(.5d * n / numPoints + .5))
    scene.add(Shape.segment(p1, p2).drawColor("gray").thickness(5))
}

//A group containing the rotating circle and the marker points
def rotatingCircle = MathObjectGroup.make()
//Circle that moves inside
def circleSmall = Shape.circle()
    .scale(.5).shift(0, .5) //Scale, position...
    .drawColor("olive").thickness(10)//Style...

rotatingCircle.add(circleSmall)//Add to group
for (int n = 0; n < numPoints; n++) {
    def p = Point.at(circleSmall.getVecAt(1d * n / numPoints)).style("redCircle")
    rotatingCircle.add(p)//Add to group
}

scene.add(outerCircle, rotatingCircle)
def mov = MoveAlongPath.make(10, path, rotatingCircle, AnchorType.CENTER, RotationType.FIXED, false)
def rot = Commands.rotate(10, -2 * PI, rotatingCircle).setUseObjectState(false)
mov.setLambda(t -> t)//Uniform movement
rot.setLambda(t -> t)//Uniform rotation
play.run(mov, rot)

Here you have a GIF from the generated movie :

Tusi

The distributive property

//Two rectangles
def sq1 = Shape.square()
    .scale(2.5, 1).style("solidblue")

def sq2 = Shape.square()
    .scale(1.75, 1).style("solidred")

sq2.stack()
    .withGaps(.5)
    .withDestinyAnchor(AnchorType.RIGHT)
    .toObject(sq1)
scene.add(sq1, sq2)

//The colors chosen to the symbols a, b and c
def colA = JMColor.parse("#34403C");
def colB = JMColor.parse("#961A4C");
def colC = JMColor.parse("#6A2A5C");
def colArea = JMColor.parse("#B73A1C");

//Brace delimiters
def del1X = Delimiter.makeStacked(sq1, AnchorType.LOWER, DelimiterType.BRACE, .1).addLabelTip(r"$b$")
def del1Y = Delimiter.makeStacked(sq1, AnchorType.LEFT, DelimiterType.BRACE, .1).addLabelTip(r"$a$")
def del2X = Delimiter.makeStacked(sq2, AnchorType.LOWER, DelimiterType.BRACE, .1).addLabelTip(r"$c$")
def del2Y = Delimiter.makeStacked(sq2, AnchorType.RIGHT, DelimiterType.BRACE, .1).addLabelTip(r"$a$")

//Need to declare this so that labels do not appear rotated (vertical delimiters)
del1Y.setRotationType(RotationType.FIXED)
del2Y.setRotationType(RotationType.FIXED)

//Set the colors for labels only
del1X.getLabelTip().color(colB)
del1Y.getLabelTip().color(colA)
del2X.getLabelTip().color(colC)
del2Y.getLabelTip().color(colA)
def textBC = LatexMathObject.make(r"$b+c$")
textBC.setColorToIndices(colB, 0)//"b" glyph
textBC.setColorToIndices(colC, 2)//"c" glyph
def del12X = Delimiter.makeStacked(
    MathObjectGroup.make(sq1, sq2), //We group these 2 rectangles so the brace adjust to the 2 combined
    AnchorType.UPPER, DelimiterType.BRACE, .1)
    .addLabelTip(textBC)

//The upper formula  Area=a*b+a*c
def formula1 = LatexMathObject.make(r"Area=$a\cdot b+a\cdot c$").scale(3)
//Position formula .5 units above sq2...we will center it horizontally later
formula1.stack()
    .withGaps(.5)
    .withDestinyAnchor(AnchorType.UPPER)
    .toObject(sq2)
//Apply colors
formula1.setColorToIndices(colArea, 0, 1, 2, 3)//"Area"
formula1.setColorToIndices(colA, 5, 9)//The "a" glyphs
formula1.setColorToIndices(colB, 7)//The "b" glyph
formula1.setColorToIndices(colC, 11)//The "c" glyph

//The upper formula  Area=a*(b+c)
def formula2 = LatexMathObject.make(r"Area=$a\cdot(b+c)$").scale(3)
//Apply colors
formula2.setColorToIndices(colArea, 0, 1, 2, 3)//"Area"
formula2.setColorToIndices(colA, 5)//The "a" glyphs
formula2.setColorToIndices(colB, 8)//The "b" glyph
formula2.setColorToIndices(colC, 10)//The "c" glyph

scene.add(del1X, del2X, del1Y, del2Y, formula1)
camera.adjustToAllObjects()

//Align formula1 horizontally and formula2 with it. Note that we have to do this after adjusting the camera
formula1.hCenter()
formula2.alignCenter(4, formula1, 4)//Align "=" sign of formula2 with that of the formula1

//We make a copy of formula1 to make the inverse animation later
def formula3 = formula1.copy()

scene.waitSeconds(2)
def shiftSquareAnim = Commands.shift(3, .5, 0, sq1)
def fadeBraceAnim = Commands.fadeIn(3, del12X)

def changeFormulaAnim = TransformMathExpression.make(3, formula1, formula2)
changeFormulaAnim.mapRange(0, 6, 0)//"Área=a\cdot"
changeFormulaAnim.map(7, 8)//b -> b
changeFormulaAnim.map(8, 9)//+ -> +
changeFormulaAnim.map(9, 5).addJumpEffect(.5)//Second a -> a
changeFormulaAnim.map(11, 10)//c -> c

play.run(shiftSquareAnim, fadeBraceAnim, changeFormulaAnim)
scene.waitSeconds(3)

//Inverse animation
shiftSquareAnim = Commands.shift(3, -.5, 0, sq1)
fadeBraceAnim = Commands.fadeOut(3, del12X)

changeFormulaAnim = TransformMathExpression.make(3, formula2, formula3)
changeFormulaAnim.mapRange(0, 6, 0)
changeFormulaAnim.map(8, 7)
changeFormulaAnim.map(9, 8)
changeFormulaAnim.map(5, 9).addJumpEffect(-.5)
changeFormulaAnim.map(10, 11)
play.run(shiftSquareAnim, fadeBraceAnim, changeFormulaAnim)
scene.waitSeconds(3)

distributive

The Clock

This example shows how combining simple animations we can have complex ones. The total animation is done with a single playAnimation call.

double runtime = 5

//This shape hold the points where the numbers will lie
def destiny = Shape.circle().scale(.75).rotate(90 * DEGREES)
def wholeAnim = AnimationGroup.make()
for (int n = 1; n <= 12; n++) {
    //Take 12 points equally spaced inside the path 0, 1/12, 2/12,...11/12
    //We take them in reverse as the circle is built counterclockwise
    Vec sv = destiny.getVecAt(1d*(12 - n) / 12);
    //Each number begins centered at (0,0) and moves to a point of shape destiny, with a combined animation
    //of growing, shifting and rotating...
    def sq = LatexMathObject.make("" + n).center()
    def singleNumberAnim = AnimationGroup.make()
    singleNumberAnim.add(Commands.shift(runtime , sv, sq))
    singleNumberAnim.add(Commands.growIn(runtime ,  sq).setUseObjectState(false))
    singleNumberAnim.add(Commands.rotate(runtime, sv.getAngle() - PI / 2, sq).setUseObjectState(false))
    singleNumberAnim.setLambda(UsefulLambdas.smooth())
    wholeAnim.add(singleNumberAnim)
}
wholeAnim.addDelayEffect(.2)
//You can experiment changing the lambda, for example
//wholeAnim.setLambda(UsefulLambdas.bounce2())
play.run(wholeAnim)

Here you have a GIF from the movie generated:

Clock

A Pythagoras Theorem proof

An animation with a background image and the shadow effect

Link to Youtube

Sum of the first odd numbers

Visual proof of the sum of the 9 first odd numbers Link to Youtube

And here with the 40 first odd numbers... Link to Youtube

A property of powers

The map parameters may seem confusing to read, but the creation was pretty straightforward, using the formulaHelper method before to show the indices of each LatexMathObject instance:

def t1 = LatexMathObject.make(r"$4^{2+3}=4^2\cdot 4^3$")
def t2 = LatexMathObject.make(r"$4^{7+3}=4^7\cdot 4^3$").alignCenter(4, t1, 4)
def t3 = LatexMathObject.make(r"$4^{7+1}=4^7\cdot 4^1$").alignCenter(4, t1, 4)
def t4 = LatexMathObject.make(r"$4^{7-2}=4^7\cdot 4^{-2}$").alignCenter(4, t1, 4)
def t5 = LatexMathObject.make(r"$4^{ {1\over2}-2}=4^{1\over2}\cdot 4^{-2}$").alignCenter(6, t1, 4)
def t6 = LatexMathObject.make(r"$a^{b+c}=a^{b}\cdot a^{c}$").alignCenter(4, t1, 4)

//Colors
def colA = JMColor.parse("darkgreen")
def colB = JMColor.parse("#84142d")
def colC = JMColor.parse("#1f4068")
t1.setColorToIndices(colA, 0, 5, 8)
    .setColorToIndices(colB, 1, 6)
    .setColorToIndices(colC, 3, 9)
t2.setColorToIndices(colA, 0, 5, 8)
    .setColorToIndices(colB, 1, 6)
    .setColorToIndices(colC, 3, 9)
t3.setColorToIndices(colA, 0, 5, 8)
    .setColorToIndices(colB, 1, 6)
    .setColorToIndices(colC, 3, 9)
t4.setColorToIndices(colA, 0, 5, 8)
    .setColorToIndices(colB, 1, 6)
    .setColorToIndices(colC, 2, 3, 9, 10)
t5.setColorToIndices(colA, 0, 7, 12)
    .setColorToIndices(colB, 1, 2, 3, 8, 9, 10)
    .setColorToIndices(colC, 4, 5, 13, 14)
t6.setColorToIndices(colA, 0, 5, 8)
    .setColorToIndices(colB, 1, 6)
    .setColorToIndices(colC, 3, 9)

double runtime = 2
camera.setGaps(.1, .1)
camera.zoomToObjects(t1, t2, t3, t4, t5, t6)
def tr = TransformMathExpression.make(runtime, t1, t2)
tr.mapRange(0, 9, 0)
play.run(tr)
scene.waitSeconds(1)

tr = TransformMathExpression.make(runtime, t2, t3)
tr.mapRange(0, 9, 0)//t2 and t3 have the same number of shapes, one-to-one correspondence
play.run(tr)
scene.waitSeconds(1)

tr = TransformMathExpression.make(runtime, t3, t4)
tr.mapRange(0, 8, 0)
tr.defineDstGroup("minus8", 9, 10)
tr.map(9, "minus8")//The "3" maps into the 2 shapes "-8"
play.run(tr)
scene.waitSeconds(1)

tr = TransformMathExpression.make(runtime, t4, t5)
tr.map(0, 0)
tr.defineDstGroup("frac1", 1, 2, 3)
tr.map(1, "frac1").setTransformStyle(MathTransformStyle.FLIP_VERTICALLY)
tr.mapRange(2, 5, 4)
tr.defineDstGroup("frac2", 8, 9, 10)
tr.map(6, "frac2").setTransformStyle(MathTransformStyle.FLIP_VERTICALLY)
tr.mapRange(7, 10, 11)
play.run(tr)
scene.waitSeconds(1)

tr = TransformMathExpression.make(runtime, t5, t6)
tr.map(0, 0)
tr.defineOrigGroup("frac1", 1, 2, 3)
tr.map("frac1", 1)//The 3 shapes "1 / 2" maps into the shape "b"
tr.mapRange(4, 7, 2)
tr.defineOrigGroup("frac2", 8, 9, 10)
tr.map("frac2", 6)
tr.mapRange(11, 12, 7)
tr.defineOrigGroup("minus2", 13, 14)
tr.map("minus2", 9)
play.run(tr)
scene.waitSeconds(1)

//Highlight the "b"
play.run(Commands.highlight(1, t6.get(1)),
    Commands.highlight(1, t6.get(6)))
//Highlight the "c"
play.run(Commands.highlight(1, t6.get(3)),
    Commands.highlight(1, t6.get(9)))
scene.waitSeconds(3)

Here is a gif from the movie generated:

powerProperty1

Moving 3 coins

A solution for a simple puzzle. Can you invert the triangle moving only 3 coins?

def coinBase = Shape.circle().fillColor("gold").thickness(10)
def coin = new Shape[10]
for (int n = 0; n < 10; n++) {
    coin[n] = coinBase.copy()
}
double vertGap = Math.sqrt(3) - 2;//This negative gap is computed so that circles are tangent
def pyramid=MathObjectGroup.make(coin)
pyramid.setLayout(PascalLayout.make(Vec.to(0,0),0, vertGap))
scene.add(pyramid)
camera.adjustToAllObjects()
camera.scale(2)
//The number of coins:
//   0
//  1 2
// 3 4 5
//6 7 8 9
//To invert the pyramid, we need to:
//Rotate coin 6 around coin 3,
//rotate coin 9 around coin 8, and
//rotate coin 0 around coin 2
def anim1 = Commands.rotate(2, coin[3].getCenter(), -120 * DEGREES, coin[6])
def anim2 = Commands.rotate(2, coin[8].getCenter(), -120 * DEGREES, coin[9])
def anim3 = Commands.rotate(2, coin[2].getCenter(), -120 * DEGREES, coin[0])

play.run(anim1,anim2,anim3)
//Now they are arranged in this way:
//6 1 2 0
// 3 4 5
//  7 8 
//   9
//So, we need to:
//Rotate coin 6 around coin 1,
//rotate coin 9 around 7, and
//rotate coin 0 around coin 5
//to invert the pyramid again

scene.waitSeconds(3);//Take a breath...

anim1 = Commands.rotate(2, coin[1].getCenter(), -120 * DEGREES, coin[6])
anim2 = Commands.rotate(2, coin[7].getCenter(), -120 * DEGREES, coin[9])
anim3 = Commands.rotate(2, coin[5].getCenter(), -120 * DEGREES, coin[0])

play.run(anim1, anim2, anim3)

scene.waitSeconds(3)

Here is a gif from the movie generated:

coins

A declaration of love

This example illustrates the transform animation between two multishape objects.

def text = LatexMathObject.make(r"I$\heartsuit$pentagons").center()
text.setColorToIndices(JMColor.parse("darkred"), 1);//Heart must be dark red

def pentagons = MultiShapeObject.make()
for (LatexShape sh : text) {//Iterates over the shapes
    //Creates a pentagon rotated with a random angle
    Shape pentagon = Shape.regularPolygon(5)
    pentagon.rotate(Math.random() * 2 * PI)
    AffineJTransform tr = AffineJTransform
        .createAffineTransformation(pentagon.getBoundingBox(), sh.getBoundingBox(), 1)
    //Transforms the pentagon so that fits in the bounding box of the current letter
    tr.applyTransform(pentagon)
    //Pentagon will be drawn and filled with a random color
    JMColor col = JMColor.random()
    pentagon.fillColor(col)
        .fillAlpha(.5)
        .drawColor(col)
    pentagons.add(pentagon)
}

camera.zoomToObjects(text)
play.transform(3, pentagons, text)
play.transform(3,  text, pentagons)

pentagons

One thousand candies falling

This example explores the use of MathObjectGroupclass to handle large sets of objects

def candy = Shape.circle().scale(.03).thickness(2) // base shape for candy
def numCandies = 1000 // Number of candies to fall
def refPoint = Point.origin()

// Locates the refPoint at the bottom of the screen, leaving a margin the size of a candy
refPoint.stack().withGaps(candy.getHeight()).toScreen(ScreenAnchor.LOWER)

// Creates the group of candies
def candyHeap = MathObjectGroup.make()
for (def n = 0; n < numCandies; n++) {
    candyHeap.add(candy.copy().fillColor(JMColor.random()))
}

// Now we will layout this using a PascalLayout. We need a triangular number
// of candies to get a perfect triangular heap, so we will use an auxiliar group
// with the candies and fill it to get a triangular number
def triangularNumber = 0
def k = 0
while (triangularNumber < numCandies) {
    triangularNumber = k * (k + 1) / 2
    k++
}

def auxCandyHeap = MathObjectGroup.make()

// add the candies
auxCandyHeap.getObjects().addAll(candyHeap.getObjects())

// Now add the "virtual candies"
for (def n = 0; n < triangularNumber - numCandies; n++) {
    auxCandyHeap.add(candy.copy()) // Virtual candies, these will be discarded later
}

// Reverse the order, so that the virtual candies will be on top of the Pascal Layout
Collections.reverse(auxCandyHeap.getObjects())

def radius = .5 * candy.getHeight() // radius of a candy
def vgap = radius * (Math.sqrt(3) - 2) // negative gap so that the circles will stick together

// The reference point here is meaningless, we will align the group later
def layout = PascalLayout.make(Point.origin(), 0, vgap)
auxCandyHeap.setLayout(layout)
auxCandyHeap.stack().withDestinyAnchor(AnchorType.UPPER).toObject(refPoint)

// Note that we used auxCandyHeap only to set the layout of all elements of group candyHeap
// Now perform animations. We will shuffle all elements in each row
def anim = AnimationGroup.make()
def rows = layout.getRowGroups(auxCandyHeap)

Collections.reverse(rows.getObjects()) // Reverse the order of rows

def counter = 0
def rowCounter = 0

for (def r : rows) {
    def row = (MathObjectGroup) r
    Collections.shuffle(row.getObjects())

    // Now for each candy of the shuffled row, add a fall animation
    for (def candyToFall : row) {
        if (candyHeap.getObjects().contains(candyToFall)) { // if not an auxiliar candy...
            def conc = Concatenate.make()
            final def wt = 2d * counter / candyHeap.size()
            conc.add(WaitAnimation.make(wt))
            conc.add(
                Commands.moveIn(
                    1 - .9 * rowCounter / rows.size(),
                    ScreenAnchor.UPPER,
                    candyToFall
                ).setLambda(t -> t * t)
            )
            anim.add(conc)
            counter++
        }
    }
    rowCounter++
}

scene.playAnimation(anim)
scene.waitSeconds(3)

You can see a video of the animation here

Showing the divisibility of a number with candies

The animation of the candies allocating in the band is done with a single setLayout animation, with a jump effect with semicircular path and a delay effect of 0.5

You can see a video of the animation here (note: the title of the video should say "7" instead of "9").

Concentric circles that appear and disappear

This animation show how using the MathObjectGroup class can achieve interesting effects easily.

def numberSmallSquares = 24 // 24 squares in each circle
def numberConcentricCircles = 15 // 15 concentric circles

// This is the side length of a regular polygon
// circumscribed in a unit circle
def scaleSmallSquare = 2 * Math.tan(PI / numberSmallSquares)

// Creates a base shape (a square, that can be changed)
// and put it to the right of the point (1,0)
def baseShape = Shape.square().scale(scaleSmallSquare)
baseShape.stack().withOriginAnchor(AnchorType.LEFT).toPoint(1, 0)

// Colors to alternate in base shape
def col1 = JMColor.parse("#512D6D")
def col2 = JMColor.parse("#F8485E")

// Each concentric circle is a MathObjectGroup
def circles = new MathObjectGroup[numberConcentricCircles]

// Create the first MathObjectGroup, the outer circle
def delta = 2 * PI / numberSmallSquares
circles[0] = MathObjectGroup.make()

def useFirstColor = false
for (def n = 0; n < numberSmallSquares; n++) {
    def smallSquare = baseShape.copy().fillColor(useFirstColor ? col1 : col2)
    smallSquare.rotate(Point.origin(), delta * n)
    circles[0].add(smallSquare)
    useFirstColor = !useFirstColor
}

// Create the next MathObjectGroups recursively, scaling appropriately
def scale = 2d
for (def k = 1; k < circles.length; k++) {
    circles[k] = circles[k - 1].copy().scale(scale / circles[k - 1].getWidth())
    scale *= scale / circles[k - 1].getWidth()
}

// Add everything to the scene. If you add an array, each element of this array
// will be added to the scene
scene.add(circles)
camera.adjustToAllObjects()

// Time to create animations, each one a rotation with alternating direction
def ag = AnimationGroup.make()
def reverse = false

for (def k = 0; k < circles.length; k++) {
    ag.add(
        Commands.rotate(
            1, // 1 second
            Vec.to(0, 0), // Rotation center
            (reverse ? -delta : delta), // angle
            circles[k] // Object to rotate
        )
    )
    reverse = !reverse
}

ag.setLambda(t -> t) // Linear movement
play.run(ag) // Play...
scene.waitSeconds(1) // Wait...
play.run(ag)
scene.waitSeconds(1) // Wait...and done!

Here is a GIF from the movie generated:

OpticalIllusion

A Truchet tiling

An example showing how to generate a simple MathObjectGroup (a Truchet tile) and use the setLayout method to tile the plane. Add a zoom out and rotate effect to generate the optical illusion that the image is still rotating after the animation is finished.

private def makeTruchet(boolean turn) {
    def truchetTile = MathObjectGroup.make()

    // This commented line added the outline of the tile. You can uncomment it
    // to see the difference
    // truchetTile.add(Shape.square().drawColor("violet").fillColor("white"))

    // A quarter circle, with radius 1
    def a = Shape.arc(.5 * PI)

    // 2 copies, scaled around center of the arc
    def width = .375
    def inner = a.copy().scale(Point.origin(), .5 - width * .5, .5 - width * .5)
    def outer = a.copy().scale(Point.origin(), .5 + width * .5, .5 + width * .5)

    // Merge inner and reversed outer shape, connecting both ends
    inner.merge(outer.reverse(), true, true)

    truchetTile.add(
        inner,
        inner.copy().rotate(Point.at(.5, .5), PI)
    )

    if (turn) { // Rotate the tile 90 degrees
        truchetTile.rotate(.5 * PI)
    }

    return truchetTile
}


def truchets = MathObjectGroup.make()
def size = 44 // A square of 44 rows/columns
final def numTiles = size * size
final def rowSize = size

for (def k = 0; k < numTiles; k++) {
    def rand = (Math.random() < .5)
    truchets.add(makeTruchet(rand))
}

truchets.setLayout(BoxLayout.make(rowSize)).center()
scene.add(truchets)

// A radial gradient, centered at origin and relative to screen
def grad = JMRadialGradient.make(
    truchets.getCenter(),
    truchets.getHeight() * .5
)

grad.setRelativeToShape(false)
grad.add(0, JMColor.parse("#EEEEEE"))
grad.add(1, JMColor.parse("#FF5722"))

// Both draw and fill colors with this gradient
truchets.fillColor(grad).drawColor(grad)

camera.setMathXY(-1, 1, 0)

play.run(
    Commands.rotate(20, 4 * PI, truchets).setLambda(t -> t),
    Commands.cameraScale(20, camera, 20).setLambda(t -> t)
)
scene.waitSeconds(5)// In this 5 seconds you will think that the picture is still moving :-)

You can see the generated video here

home back