5. Lerp
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: "Monocurl" needs to be written
- Track 2: The graph and color grid
- Track 3: The camera motion
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.