Monocurl
Download

lesson 04

Animation Basics

Monocurl animations are built from state changes. You mutate leaders; play statements synchronize followers to those leaders with a chosen strategy.

Leader And Follower

mesh x = value creates two things:

  • A leader — the variable your code reads and writes, initialized to value
  • A follower — what the viewport actually draws, initialized to [] (empty)

Changing the leader in code is instant and invisible. Only a play statement makes the follower catch up to the leader, and the animation chosen for play controls *how* it gets there.

The viewport in the editor shows follower state at any point in time. When you scrub the timeline, you are asking: "what did the followers look like at this moment in execution?"

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

mesh ball =
    center{1.6l}
    fill{soft(BLUE)}
    stroke{BLUE, 2}
    Circle(0.45)

slide "Leader And Follower"
    play Wait(0.5)

    # This assignment changes the leader immediately.
    # At this point viewport still shows the follower which hasn't caught up yet.
    ball =
        center{1.6r}
        fill{soft(ORANGE)}
        stroke{ORANGE, 2}
        Circle(0.45)

    play Wait(0.5)

    # Now Set() syncs the follower to the leader — the ball jumps.
    play Set()
    play Wait(0.8)

Set

Set copies every dirty leader to its follower instantly. Use it for jump cuts or to reveal a new state without a transition.

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

mesh ball =
    center{1.4l + 0.15d}
    fill{soft(GREEN)}
    stroke{GREEN, 2}
    Circle(0.45)

slide "Set"
    play Wait(1.0)

    ball = center{1.4r} fill{soft(MAGENTA)} stroke{MAGENTA, 2} Circle(0.55)
    play Set()
    play Wait(1.0)

In general, animations can infer the meshes you want to animate by seeing which one has changed. In more complex scenarios, you can explicitly say the variables you want with play Set([&ball]) syntax.

Lerp

Lerp interpolates compatible values over time. "Compatible" usually means the leader and follower are the same function call with the same structure, so arguments can be interpolated individually. The exact definition can be found in docs.

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

let Ball = |pos, radius, col|
    center{pos} fill{soft(col)} stroke{col, 2} Circle(radius)

mesh ball = Ball(pos: 1.4l, radius: 0.35, col: BLUE)

slide "Lerp"
    play Wait(0.6)

    ball.pos = 1.4r
    ball.radius = 0.6
    ball.col = ORANGE
    play Lerp(1.5)
    play Wait(0.8)

Because ball is defined as a labeled call to Ball, you can mutate individual fields (ball.pos, ball.radius, ball.col) and then Lerp interpolates each argument independently from its old value to its new value.

Lerp and other animations accepts an optional rate modifier for easing:

play Lerp(1.5, smooth)     # default: smooth S-curve
play Lerp(1.5, ease_out)   # decelerates
play Lerp(1.5, linear)     # constant speed

Intro And Outro Animations

Some animations are meant for meshes that are appearing or disappearing. They compare the current follower state with the new leader state and animate the difference.

Write — traces new contours as if drawn by hand. Best for curves and text.

video03-animation.mcl
slide "Write"
    mesh curve = stroke{BLUE, 3} ExplicitFunc(|x| sin(x * TAU), [-1.5, 1.5, 100])
mesh label = center{1.2d} color{BLUE} Text("sin(2πx)", 0.55)
    play Write(1.2)
    play Wait(0.8)

Fade — fades meshes in or out. Grow — expands geometry from its center outward. Good for shapes appearing.

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

slide "Grow And Fade"
    mesh shapes = [
        center{1.3l} fill{soft(BLUE)} stroke{BLUE, 2} Circle(0.42),
        center{0r} fill{soft(ORANGE)} stroke{ORANGE, 2} Square(0.7),
        center{1.3r} fill{soft(GREEN)} stroke{GREEN, 2} RegularPolygon(5, 0.44)
    ]
    play Grow(1.0)
    
    mesh label = center{1.1d} color{GRAY} Text("three shapes", 0.55)
    play Fade(0.8)
    play Wait(0.6)

    # can also be used for hiding!
    shapes = []
    label = []
    play Fade(0.9)

Trans And TagTrans

Trans is the general-purpose mesh transformation. It morphs the current follower into the current leader by matching contours using a cost function. It can be used when Lerp is not possible (because of different leader and follower structures).

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

mesh shape = fill{soft(CYAN)} stroke{CYAN, 2} Triangle(1.2l, 1.2r, 1.0u)

slide "Trans"
    play Wait(0.8)

    shape = fill{soft(ORANGE)} stroke{ORANGE, 2} Circle(0.75)
    play Trans(1.2)
    play Wait(0.8)

    shape = Rect([2.0, 1.2])
    play Trans(1.2)
    play Wait(0.6)

TagTrans is a specialization of Trans that restricts matching to fragments with the same tag. This gives you fine-grained control over the contour-matching process.

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

mesh pair = [
    tag{1} center{1.1l} fill{soft(BLUE)} stroke{BLUE, 2} Square(0.55),
    tag{2} center{1.1r} fill{soft(ORANGE)} stroke{ORANGE, 2} Circle(0.45)
]

slide "TagTrans"
    play Wait(0.8)

    # Without TagTrans, Trans might match the square to the right circle
    # and the circle to the left square. TagTrans forces tag-1 to tag-1
    # and tag-2 to tag-2, so each shape moves and transforms independently.
    pair = [
        tag{2} center{1.1l} fill{soft(ORANGE)} stroke{ORANGE, 2} Circle(0.5),
        tag{1} center{1.1r} fill{soft(BLUE)} stroke{BLUE, 2} Circle(0.5)
    ]
    play TagTrans(1.4)
    play Wait(0.8)

Lerp With Operators

Lerp is not just for labeled function arguments. Operator-based expressions can also be interpolated, because operators know their own identity state.

For example, interpolating from x to rotate{angle} x makes the mesh rotate smoothly.

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

mesh tri =
    center{ORIGIN}
    fill{soft(BLUE)}
    stroke{BLUE, 2}
    Triangle(0.8l, 0.8r, 0.9u)

slide "Rotate With Lerp"
    play Set()
    play Wait(0.5)

    tri = rotate{PI} tri
    play Lerp(1.8)
    play Wait(0.8)

The same works for shift, scale, color, and most other operators. The general pattern is: mesh = operator{args} mesh sets the leader to the operated version, and Lerp interpolates from the follower (un-operated) to the leader (operated).

You can also chain this to animate a sequence of operator applications:

video03-animation.mcl
let soft = |col| lerp(WHITE, col, 0.22)

mesh box =
    center{1.5l}
    fill{soft(BLUE)}
    stroke{BLUE, 2}
    Square(0.55)

slide "Shift And Rotate"
    play Set()
    play Wait(0.5)

    box = shift{3r} rotate{PI / 3} box
    play Lerp(1.8)
    play Wait(0.6)

Choosing An Animation

  • Set — instant snap; use for jump cuts and initial reveals
  • Lerp — smooth interpolation; use when leader and follower have the same structure
  • Write — tracing; use for new curves, paths, and text
  • Grow — expansion; use for new filled shapes
  • Fade — opacity; use for appearing or disappearing geometry
  • Trans — general morphing; use when structure changes significantly
  • TagTrans — tagged morphing; use when multiple independent pieces each need their own counterpart