5. Lerp

Accompanying Video

5.1 Functor Interpolation

DEFINITION For two functors of the same type, functor interpolation is defined to be the recursive interpolation of its arguments. That is, a new functor is created of the same type where each argument to the functor is just the interpolation from the associated argument of the start functor to that of the end functor.

Here's an example:

var A = Circle:
    center: {0,0,0}
    radius: 1
    tag: {}
    stroke: BLACK

var B = Circle:
    center: {1,0,0}
    radius: 3
    tag: {}
    stroke: BLACK

var C = lerp(A, B, 0.25)

In this case, this is the same thing as saying:

C = Circle:
    center: {0.25, 0, 0}
    radius: 1.5
    tag: {}
    stroke: BLACK

Because the center is interpolated from {0,0,0} to {1,0,0}, and the radius is interpolated from 1 to 3.

For full semantics, read the documentation of lerp.

5.2 Lerp

Now, what if we can have a follower animation that interpolates between the follower and iterator value using functor interpolation?

IMPORTANT This brings us to Lerp. It does exactly what we want, interpolating a screen variable between two functors. In particular, at each time t, Lerp sets its target variable equal to the functor interpolation between the starting value (follower) and ending value (iterator), with the blend factor being time dependent.

Heres an example. Notice that its important that the type of the follower and iterator match.

tree trans = Square:
    center: RIGHT
    width: 0.5
    tag: {}
    stroke: BLACK

tree lerps = Square:
    center: LEFT
    width: 0.5
    tag: {}
    stroke: BLACK

/* Important: we have to do make sure that when the Lerp is run */
/* the follower is of the same type as the iterator. */
/* Thus we do a Rotated: 0 so that it's visually the same, */
/* but also now the follower and iterator both will be Rotated functors.  */
/* That is, the Lerp will lerp the rotation field from 0 to 5pi/4, */
/* which is what we want. */
lerps = Rotated:
    mesh: lerps
    rotation: 0
p += Set:
    vars&: lerps

lerps.rotation = 5 * PI / 4
/* linearly interpolates the rotation factor from 0 to 5pi / 4 */
/* this is because the follower and iterator are both of the same type */
/* and the only attribute they have thats different is rotation */
p += Lerp:
    vars&: lerps
    time: 1

/* transform example */
p += Set:
    vars&: trans

trans = Rotated:
    mesh: trans
    rotation: 5 * PI / 4
p += Transform:
    meshes&: trans
    time: 1

This is the result (slightly modified for easy comparison). Notice how Transform takes the shortest path, whereas Lerp performs an actual rotation since we are interpolating the rotation factor.

5.3 User Defined Functors and More Examples

The true power of Lerp comes when we use user defined types.

For instance, here we interpolate several factors of the Weierstrass function. In general, Lerp is good for demonstrating how several parameters influence an object.

let n = 50

func ws(a,b,x) = identity:
    var s = 0
    for i in 0:<n
        s += a ** i * cos((b ** i) * PI * x)
    element: s

func Weierstrass(a,b) = ExplicitFunc:
    start: -4
    stop: 4
    samples: 1024
    f(x): ws(a,b,x)
    tag: {}
    stroke: RED

tree w = Weierstrass:
    a: 0.5
    b: 0.1
tree axis = Axis2d:
    center: ORIGIN
    x_unit: 1
    x_rad: 4
    x_label: "x"
    y_unit: 1
    y_rad: 3
    y_label: "y"
    grid: off
    tag: {}
    color: BLACK

p += Set:
    vars&: {w, axis}
w.b = 4
p += Lerp:
    vars&: w
    time: 3

Lerping between variables that are meant to be integers wont leave the interpolated variable as an integer, but that doesn't matter a good amount of times. Take a look:

func RootsOfUnity(N) = identity:
    let n = round(N)

    var dots = {}
    for i in 0 :< n
        let theta = TAU * i / n
        let c = cos(theta)
        let s = sin(theta)
        dots += Dot:
            point: {c,s,0}
            tag: {}
            dot: BLACK

    element: dots

tree ring = Circle:
    center: ORIGIN
    radius: 1
    tag: {}
    stroke: RED

tree roots = RootsOfUnity:
    N: 0
p += Set:
    vars&: {roots, ring}

roots.N = 64
p += Lerp:
    vars&: roots
    time: 3
    unit_map(u): smooth_in(u)

5.4 Type Elision

IMPORTANT With Lerp, it's important that the follower and iterator are of the same type, thus the following example does not work, even though the starting variable "looks like" a square, it's type is different and thus we cannot adequately interpolate.

tree curr = Polygon:
    var verts = {}
    verts += ORIGIN
    verts += {1, 0, 0}
    verts += {1, 1, 0}
    verts += {0, 1, 0}
    vertices: verts
    tag: {}
    stroke: BLACK

p += Set:
    vars&: curr

curr = Square:
    center: ORIGIN
    width: 1
    tag: {}
    color: default
/* cannot interpolate from a Polygon to a Square */
/* even if it visually looks on screen they are different types */
p += Lerp:
    vars&: curr
    time: 1

This was perhaps not that hard to see, however there are circumstances in which type must be elided where it's not so obvious. Many operations dealing with subsets necessarily have to elide type (basically the functor attributes are lost in some operations so you cannot adequately interpolate them anymore). Also, in certain cases some Transfer operations have to elide type when the destination variable is not empty. In either case, using Set to "restore" type is common.

5.5 (Optional) Monocurl Intro Video Animation

Also, we can now create the Monocurl intro video animation! Lets break it down.

In fact, there isn't that much going on. It does involve a camera animation which we haven't explicitly seen, but it's the same general idea.

Track 1 is pretty easy, we simply just need to use a Write animation. Track 3 is also not that bad, we use CameraLerp to move the camera from its default position to some offset target. Note that lerp would work as well, but the rotation is slightly off.

Track 2 might seem a bit difficult, but really it's just two simple animations. We first have a blank ColorGrid, then we change its color, and then we add elevation to it via PointMapped. Both interpolations can be done via Transform. Note Track 2 and Track 3 should be in parallel.

Lets see the code!

let step = 0.05
func q(x,y) = 1.2 * ((x-0.5)**2 + (y-0.5)**2)
func col(x,y) = identity:
    let val = q(x,-y)
    let colors = {0:BLUE,0.15:YELLOW,0.25:ORANGE,0.5:RED}
    let color = keyframe_lerp(colors, val)
    element: color

/* meshes */
tree tex = Text:
    text: "Monocurl"
    scale: 1.5
    stroke: BLACK
    fill: BLACK
tex = Centered:
    mesh: tex
    at: {0,1,0}

tree grid = ColorGrid:
    x_min: 0
    x_max: 1
    y_min: -1
    y_max: 0
    x_step: step
    y_step: step
    tag: {}
    color_at(pos): BLACK
    stroke: BLACK

tree axis = Axis3d:
    center: {0,0,-0.01}
    pos_x_axis: {1,0,0}
    pos_y_axis: {0,-1,0}
    pos_z_axis: {0,0,1}
    x_unit: 1
    x_min: 0
    x_max: 1
    x_label: "x"
    y_unit: 1
    y_min: 0
    y_max: 1
    y_label: "y"
    z_unit: 1
    z_min: 0
    z_max: 1
    z_label: "z"
    grid: on
    tag: {}
    color: BLACK

/* intro */
p += Write:
    meshes&: tex
    time: 1
p += sticky Fade:
    meshes&: {axis, grid}
    time: 1

/* main animation */
var grid_anim = {}
grid_anim += Transform:
    grid = ColorGrid:
        x_min: 0
        x_max: 1
        y_min: -1
        y_max: 0
        x_step: step
        y_step: step
        tag: {}
        color_at(pos): col(pos[0], pos[1])
        stroke: BLACK
    meshes&: grid
    time: 1

grid_anim += Transform:
    grid = PointMapped:
        mesh: grid
        point_map(point): {point[0], point[1], q(point[0], point[1] + 1)}
    meshes&: grid
    time: 2
p += grid_anim

/* camera movement (sticky just means to run in parallel with */
/* previous animation, we'll cover sticky more later) */
p += sticky CameraLerp:
    camera.origin = {2,-2,1.4}
    camera.up = {0,0,1}

    camera&: camera
    time: 3

p += Wait(1)

And of course, the result:

Notice that the main animation honestly isn't that complex. Hopefully you now feel like you could have come up with the sequence.

Sections

5.1 Functor Interpolation5.2 Lerp5.3 User Defined Functors and More Examples5.4 Type Elision5.5 (Optional) Monocurl Intro Video Animation