Identity rate function: animation progress equals time progress.
Use this when a motion should have constant speed or when another helper already encodes easing.
let linear = |t| tplay rate{linear} Lerp(1, [&dot])
Monocurl
std.anim
Reference for the symbols exported by anim.mcl.
Identity rate function: animation progress equals time progress.
Use this when a motion should have constant speed or when another helper already encodes easing.
let linear = |t| tplay rate{linear} Lerp(1, [&dot])Default ease-in/ease-out rate for most animations.
A monotone fifth-order smoothstep-style curve, symmetric around the midpoint.
let smooth = |t| {
let s = 1 - t
return t * t * t * (10 * s * s + 5 * s * t + t * t)
}play Lerp(1, [&dot], smooth)Rate that starts slowly and accelerates.
let smooth_in = |t| 2 * smooth(0.5 * t)play rate{smooth_in} Lerp(1, [&item])Rate that starts quickly and settles slowly.
let smooth_out = |t| 2 * smooth(0.5 * (t + 1)) - 1play rate{smooth_out} Lerp(1, [&item])Fifth-order smoothstep rate.
let smoother = |t| t * t * t * (t * (t * 6 - 15) + 10)Cosine ease-in/ease-out rate.
let cosine = |t| ...Power ease-in rate; larger p makes the start slower and the end sharper.
let ease_in = |t, p = 3| ...p | power, default 3 |
Power ease-out rate; larger p makes the start sharper and the end slower.
let ease_out = |t, p = 3| ...p | power, default 3 |
Symmetric power ease-in/ease-out rate.
let ease_in_out = |t, p = 3| {
if (t < 0.5) { return ease_in(2 * t, p) / 2 }
return 1 - ease_in(2 * (1 - t), p) / 2
}p | power, default 3 |
Exponential ease-out rate.
let exponential = |t| ...Bounce-out rate with decaying rebounds near the end.
let bounce = |t| ...Elastic-out rate that overshoots and rings into place.
Because it overshoots, it can produce values outside [0, 1]; use with geometry that tolerates extrapolation.
let elastic = |t| ...Reserve time on the current slide without changing any followers.
Wait advances the timeline but does not sync leaders to followers.
let Wait = |time = 1| ...play Wait(0.5)Snap selected followers to their current leader values.
Passing [] asks Monocurl to snap every dirty leader. Pass explicit references in larger scenes to avoid syncing unrelated state.
let Set = |vars = []| ...background = [0.035, 0.045, 0.060, 1]
mesh dot = fill{CYAN} Circle(0.2)
"set"
play Set([&dot])
play Wait(1)
dot = fill{ORANGE} Square(0.4)
play Set([&dot])Interpolate selected followers toward their leaders using value interpolation.
Lerp recursively interpolates normal values: equal values stay fixed, numbers and complex numbers blend linearly, same-length lists and same-key maps lerp elementwise, matching labeled function calls lerp only their labeled arguments and rerun the function, and matching operator chains lerp operands plus labeled operator arguments. Unlabeled call arguments must already match exactly. Use Trans when the visible mesh topology changes instead of merely the arguments of one live expression.
let Lerp = |time = 1, &vars = [], rate = smooth| PrimitiveAnim(time, &vars, nil, nil, rate)vars | optional leader references; [] asks Monocurl to find dirty leaders |
rate | timing function, default smooth |
background = [0.035, 0.045, 0.060, 1]
mesh ball = center{center: 1.2l} fill{color: BLUE} Circle(radius: 0.35)
"labeled lerp"
play Set()
ball.center = 1.2r
ball.color = ORANGE
ball.radius = 0.6
play Lerp(1)background = [0.035, 0.045, 0.060, 1]
mesh blue = center{center: 1.2l + 0.4u} fill{color: BLUE} Circle(radius: 0.25)
mesh gold = center{center: 1.2l + 0.4d} fill{color: YELLOW} Circle(radius: 0.25)
"selected leaders"
play Set()
blue.center = 1.2r + 0.4u
gold.center = 1.2r + 0.4d
play Lerp(1, [&blue])
play Set([&gold])background = [0.035, 0.045, 0.060, 1]
mesh ball = center{center: 1.2l} fill{color: ORANGE} Circle(radius: 0.25)
"rate"
play Set()
ball.center = 1.2r
play rate{bounce} Lerp(1.2, [&ball])Low-level primitive animation wrapper used by higher-level animations.
embed can normalize follower/leader values into interpolation endpoints plus state; lerp can then use that state at every frame. Most scene code should use Lerp, Trans, Write, Grow, Fade, or CameraLerp instead.
let PrimitiveAnim = |time = 1, &vars = [], embed = nil, lerp = nil, rate = smooth| ...embed | optional function mapping follower and leader into interpolation endpoints |
lerp | optional interpolation function |
rate | timing function |
Fade newly introduced content in and removed content out.
Fade compares the current follower with the destination leader. Meshes introduced by the leader fade in; meshes deleted from the leader fade out. delta adds an entrance/exit offset, so content can fade while sliding.
let Fade = |time = 1, &meshes = [], delta = [0, 0, 0], rate = smooth| ...meshes | optional leader references |
delta | optional entrance/exit offset |
background = [0.035, 0.045, 0.060, 1]
mesh note = center{center: 0.8l} fill{color: CYAN} Circle(radius: 0.3)
"fade"
play Fade(0.6, [¬e], 0.2d)
note = []
play Fade(0.6, [¬e], 0.2d)Draw stroke and text contours progressively.
Write compares the current follower with the destination leader. Stroke/text contours introduced by the leader are traced in; contours deleted from the leader are traced out. It is most useful for strokes, Text, Tex, and Latex.
let Write = |time = 1, &meshes = [], rate = smooth| ...meshes | optional leader references |
background = [0.035, 0.045, 0.060, 1]
mesh title = Text("Monocurl", 0.8)
"write"
play Write(1, [&title])
title = []
play Write(0.7, [&title])Morph between different mesh geometry by building a point/contour/surface matching plan.
Trans is the general-purpose geometry morph. It may split, reorder, tessellate, and match mesh components so unrelated shapes can still transform, which is flexible but can occasionally pick a surprising correspondence. Use TagTrans when logical identities should control which pieces match.
let Trans = |time = 1, &meshes = [], path_arc = 0, rate = smooth, similar_topo_hint = 0| ...meshes | optional leader references; [] asks Monocurl to find dirty mesh leaders |
path_arc | 0 for straight motion, or a 3-vector whose direction chooses the arc plane and whose length is the arc angle in radians |
rate | timing function |
similar_topo_hint | when truthy, first try to match start and end meshes vertex-by-vertex using the same triangle graph; useful when you authored matching topology and want control over the correspondence instead of the general matcher |
background = [0.035, 0.045, 0.060, 1]
mesh shape = fill{color: alpha{0.14} CYAN} stroke{color: CYAN} Triangle(1.2l, 1.2r, 1u)
"trans"
play Set()
shape = fill{color: alpha{0.14} ORANGE} stroke{color: ORANGE} Circle(radius: 1)
play Trans(1.2)Morph meshes by matching tagged subparts before matching geometry.
Use TagTrans when a scene has logical identities, such as terms in an equation or pieces in a proof. Tags decide which submesh is paired first; the underlying Trans matcher then handles geometry inside each pair and any remaining content.
let TagTrans = |time = 1, &meshes = [], path_arc = 0, rate = smooth, similar_topo_hint = 0, tag_map = nil| ...meshes | optional leader references; [] asks Monocurl to find dirty mesh leaders |
path_arc | same circular-arc control as Trans |
similar_topo_hint | when truthy, first try vertex-by-vertex interpolation for tagged pairs with the same triangle graph |
tag_map | optional function that remaps source tags before matching |
background = [0.035, 0.045, 0.060, 1]
mesh pair = [
tag{1} center{1.1l} fill{alpha{0.22} BLUE} stroke{BLUE} Circle(0.35),
tag{2} center{1.1r} fill{alpha{0.22} ORANGE} stroke{ORANGE} Square(0.55)
]
"tag trans"
play Set()
pair = [
tag{2} center{1.1l} fill{alpha{0.22} ORANGE} stroke{ORANGE} Square(0.55),
tag{1} center{1.1r} fill{alpha{0.22} BLUE} stroke{BLUE} Circle(0.35)
]
play TagTrans(1.2, [&pair])Interpolate a camera leader with camera-specific motion.
Prefer CameraLerp over plain Lerp for camera motion. It linearly moves the camera position and forward direction, but spherical-interpolates the up vector, which avoids the roll/twist artifacts a structural value lerp can produce.
let CameraLerp = |&camera, time = 1, rate = smooth| ...camera | camera leader reference, passed with &camera |
camera = DEFAULT_CAMERA
play Set([&camera])
camera = Camera([2, 1, 5], ORIGIN, UP)
play CameraLerp(&camera, 1)Reveal newly introduced mesh content by growing it outward from its center.
Grow compares the current follower with the destination leader. Meshes introduced by the leader start collapsed at their centroid and expand into place; meshes deleted from the leader shrink back to their centroid. Best for appearing objects; use Fade for opacity-only entrances.
let Grow = |time = 1, &meshes = [], rate = smooth| ...meshes | optional leader references; [] asks Monocurl to infer dirty mesh leaders |
background = [0.035, 0.045, 0.060, 1]
mesh ring = stroke{CYAN, 3} Circle(1)
"grow"
play Grow(0.8, [&ring])
ring = []
play Grow(0.6, [&ring])Morph line-based geometry by carrying local edge rotations through the path.
Bend is best for curves, polylines, and stroke-like shapes whose local direction should rotate smoothly. Unlike Trans, it does not run the full topology matcher; use Trans for unrelated filled shapes and Bend when the path structure itself is meaningful.
let Bend = |time = 1, &meshes = [], rate = smooth| ...meshes | optional leader references |
background = [0.035, 0.045, 0.060, 1]
mesh path = stroke{color: CYAN, stroke_width: 4} Polyline([1.4l, 0.4l + 0.7u, 0.4r + 0.4d, 1.4r])
"bend"
play Set()
path = stroke{color: ORANGE, stroke_width: 4} Polyline([1.4l, 0.8l + 0.6d, 0.7r + 0.7u, 1.4r])
play Bend(1.2)Bend morph that pairs tagged subparts before matching geometry.
Use this for tagged stroke/path content when each tagged piece should bend independently.
let TagBend = |time = 1, &meshes = [], rate = smooth| ...play TagBend(1, [&paths])Temporarily change a mesh leader to a highlight color and then restore it.
Highlight is a progressor: it mutates the referenced leader to the highlighted value and then back to the original value.
let Highlight = |&value, color, time = 1, rate = smooth| anim ...value | mesh leader reference |
play Highlight(&equation, YELLOW)Sweep a moving write-window over a mesh and then restore it.
Flash uses two phase functions: the leading edge and trailing edge of the visible window. By default the window opens, sweeps over the stroke/text contours, and closes.
let Flash = |&value, time = 1, lead = nil, trail = nil| ...value | mesh leader reference |
lead | optional leading edge rate |
trail | optional trailing edge rate |
play Flash(&equation, 0.8)Move all mesh content from one leader into another and clear the source.
This is a state-changing helper, not a visual morph by itself; it snaps both leaders with Set.
let Transfer = |&from, &into| anim {
into = [into, from]
from = []
play Set([&from, &into])
}play Transfer(&old_group, &new_group)Move only the tagged subset matching a filter into another leader.
The filter receives the full tag list. Non-matching content stays in from.
let TransferSubset = |&from, &into, filter = nil| anim ...filter | tag filter for content to move |
play TransferSubset(&source, &sink, |tags| 2 in tags)Copy the tagged subset matching a filter into another leader.
Matching content is appended to into; from is left unchanged.
let CopySubset = |&from, &into, filter = nil| anim ...filter | tag filter for content to copy |
Copy all mesh content from one leader into another without clearing the source.
Snaps only the destination follower after appending the source content.
let Copy = |&from, &into| anim {
into = [into, from]
play Set([&into])
}Move a tagged subset into an auxiliary target, morph it, then transfer it into another leader.
This is the workhorse for equation/proof animations: select tagged source pieces, morph them into matching destination geometry, then append them into into.
let TransSubsetTo = |&from, filter, target, &into, time = 1, tagged = 0, path_arc = 0, rate = smooth, similar_topo_hint = 0| anim {
mesh aux = []
play TransferSubset(&from, &aux, filter)
aux = target
if (tagged) {
play TagTrans(time, &aux, path_arc, rate, similar_topo_hint)
} else {
play Trans(time, &aux, path_arc, rate, similar_topo_hint)
}
play Transfer(&aux, &into)
}filter | tag filter for content to move |
target | target mesh for the subset |
time | morph duration |
tagged | use TagTrans when truthy, Trans otherwise |
path_arc | arc control passed through to Trans/TagTrans |
similar_topo_hint | when truthy, first try vertex-by-vertex interpolation for subset pairs with the same triangle graph |
play TransSubsetTo(&labels, |tags| 2 in tags, tag_filter{|tags| 2 in tags} formula, &formula, 1, 1)Copy a tagged subset into an auxiliary target, morph it, then transfer it into another leader.
Same as TransSubsetTo, but leaves the source content in place.
let TransSubsetCopy = |&from, filter, target, &into, time = 1, tagged = 0, path_arc = 0, rate = smooth, similar_topo_hint = 0| anim {
mesh aux = []
play CopySubset(&from, &aux, filter)
aux = target
if (tagged) {
play TagTrans(time, &aux, path_arc, rate, similar_topo_hint)
} else {
play Trans(time, &aux, path_arc, rate, similar_topo_hint)
}
play Transfer(&aux, &into)
}filter | tag filter for content to copy |
target | target mesh for the copied subset |
time | morph duration |
tagged | use TagTrans when truthy, Trans otherwise |
path_arc | arc control passed through to Trans/TagTrans |
similar_topo_hint | when truthy, first try vertex-by-vertex interpolation for subset pairs with the same triangle graph |
Run a list of animations with staggered offsets.
Starts each animation after an offset distributed by unit_map, then waits for all branches to finish. Good for cascaded Grow, Write, or per-item progressors.
let LaggedMap = |anims, average_offset = 0.1, unit_map = smooth| ...average_offset | average delay between neighboring starts |
unit_map | rate used to distribute offsets |
play LaggedMap(map(dots, |&d| Grow(0.4, [&d])), 0.08)Replace the rate function on a primitive animation.
This affects primitive animations and wrappers that return primitive animations. It does not rewrite arbitrary anim { ... } blocks.
let rate = operator |target, rate| ...play rate{bounce} Lerp(1, [&ball])Slow an animation by a multiplicative factor.
slow{2} doubles the target duration.
let slow = operator |target, factor = 2| ...play slow{2} Write(1, [&path])Speed an animation up by a multiplicative factor.
fast{2} halves the target duration.
let fast = operator |target, factor = 2| ...Delay an animation block by playing Wait before it.
Useful inside parallel play [...] lists when one branch should start later.
let delay = operator |target, time = 1| [target, anim {
play Wait(time)
play target
}]play [Write(1, [&a]), delay{0.2} Write(1, [&b])]