Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The language layer

Everything so far has been static text. manic also has a small computation layer that runs before the animation — variables, arithmetic, loops, and macros. It lets one rule draw a hundred shapes.

These are resolved at build time. By the time the animation plays, they’ve expanded into plain calls — so they cost nothing at render.

Variables — let

let r = 120;
let gap = r * 2 + 40;
circle(a, (cx - gap, cy), r);
circle(b, (cx + gap, cy), r);

Arithmetic is what you’d expect: + - * / ^, parentheses, and functions like sin, cos, sqrt. Constants pi, tau, e are built in, as are the canvas vars w, h, cx, cy.

Put a * between names. A number can hug a name (2r, 3(x+1)), but two names glued together read as one word — write r*x, i*dx, pi*t, never rx / idx / pit. This is the most common slip in loops and formulas.

Loops — for

for i in 0..5 {
  dot(p{i}, (200 + i*180, cy), 8);   // p0 … p4
}

p{i} is id interpolation{expr} glued to a name makes each entity unique. Use i in the body to compute positions, sizes, hues…

// one for-loop paints a full rainbow ring.
title("One loop");  canvas("16:9");
text(t, (cx, 90), "one loop, 24 dots, every hue");  color(t, cyan);  size(t, 28);  hidden(t);

let n = 24;
for i in 0..n {
  dot(p{i}, (cx + 300*cos(tau*i/n), cy + 300*sin(tau*i/n)), 14);
  hue(p{i}, 360*i/n);      // colour by angle -> a rainbow
  hidden(p{i});
}

show(t, 0.5);
stagger(0.05) { for i in 0..n { show(p{i}); } }
wait(1.4);

▶ See it play:

Conditionals — if

let n = 5;
if n > 3 {
  circle(big, (cx, cy), 120);
}

Macros — def

A def is a reusable rule. Its parameters are numbers; build ids inside with interpolation. It can even call itself (recursion) — that’s how the fractal tree in the gallery is one page of code:

def branch(k, x, y, ang, len, depth) {
  if depth > 0 && len > 2 {
    let x2 = x + len*cos(ang);
    let y2 = y - len*sin(ang);
    line(seg{k}, (x, y), (x2, y2));
    branch(2*k,     x2, y2, ang + 0.4, len*0.72, depth - 1);
    branch(2*k + 1, x2, y2, ang - 0.4, len*0.72, depth - 1);
  }
}
branch(1, cx, h - 40, 1.5708, 150, 12);

Curve L-systems — one rule, one path

For deterministic textbook curves, use lsystem instead of manually recursing into thousands of separate lines:

lsystem(carpet, (cx,cy), 620,
  "F+F+F+F",
  "F=FF+F+F+F+FF",
  "angle=90 heading=0 iterations=4");

gradient(carpet, cyan, magenta, gold);
stroke(carpet, 2);
untraced(carpet);
draw(carpet, 3, smooth);

The six arguments are:

  1. id, centre, and fitted square size;
  2. the starting string (axiom);
  3. semicolon-separated rewrite rules such as "X=...;Y=...";
  4. one short options string.

In the generated program, + turns left by angle, - turns right, | turns around, and every symbol listed by draw=F moves forward while drawing. heading sets the first direction, iterations controls detail, and padding keeps the result away from its fitted edge.

Use closed=true fill=true when the grammar describes a closed boundary; Manic triangulates it into one concave region. Keep fill=false for line curves. The constructor deliberately does not interpret branch brackets: use a recursive def or tree3 for branching plants. This separation keeps curve stories simple and makes invalid input easy to repair.

Practical tips

  • Increase iterations one step at a time: rewrite systems grow exponentially.
  • Keep the curve as one entity, then use ordinary draw, gradient, move, become, step, and camera verbs.
  • For a lesson, show generations 0 → 1 → 2 before revealing the detailed curve.
  • See lsystem-asymptote-curves.manic for four canonical grammars and creator-lsystem-fractal-curve.manic for a complete creator Short.

Repeated motifs — one design, many placements

Use repeat when the visual idea is “this motif forms a field,” not “write a loop that manually copies every part.”

polygon(tile, (cx-12,cy+10), (cx+12,cy+10), (cx,cy-14));
repeat(field, tile, "layout=hex rings=4 spacing=42 rotate=30 scale=0.8");
untraced(field);
draw(field, 2, smooth);

The motif can be one entity or a tag containing several entities. Choose:

  • layout=hex rings=N spacing=S;
  • layout=grid rows=R cols=C spacing=S (or separate gapx / gapy);
  • layout=radial count=N radius=R face=same|out.

rotate turns the arrangement and scale sizes each instance. Every output is tagged with the destination id; stable instances are {id}.i0, {id}.i1, … The output of one repeat can itself be the motif of another, which is how nested textbook tilings stay short. Use normal color, gradient, mask, untraced, draw, and stagger afterward.

See asymptote-tiling-reference.manic for hex/grid/radial/nested coverage and creator-one-tile-pattern-story.manic for the creator treatment.

Reductions

Fold a range into one number with sum, prod, min, max:

let n = 6;
let total = sum(i in 1..n : i);   // 1 + 2 + … + (n-1)

That’s the whole language. The rest is kits — bundles of higher-level figures → Kits.