Going 3D
So far every shape has lived on a flat page. manic has a second world: a real 3D space you look at through a camera. The idea is exactly the same — you name a cast of shapes and call them out in a script — but now the shapes have depth, and you can spin the camera around them.
One rule of thumb: everything 3D ends in 3 — cube3, sphere3, move3,
orbit3. That’s how you tell the two worlds apart.
First, a camera
A 3D scene needs a camera — an eye to look through. You say where the eye sits and which point it looks at:
camera3((8, -10, 6), (0, 0, 1), 45); // eye position, look-at point, zoom
Positions are (x, y, z), and here z is up (x and y are the ground). You
add one camera, and you can swing it around later.
The creator-first five
For most explainers, start with the relationship you want rather than camera coordinates or intermediate positions. 3D V2 adds five composition words:
| word | creator intent |
|---|---|
view3(subject, "isometric", 1, smooth, 1.3) | frame one object or tagged group, with margin |
travel3(subject, route, 2, smooth) | move along a line3, arrow3, or curve3 |
attach3(label, subject, (0,0,1)) | position-only follow (compatible default); add rigid to inherit orientation; use none to release |
become3(subject, blueprint, 1, smooth) | keep the subject id while it adopts another shape and style |
turn3(group, pivot, z, 90, 1, smooth) | rigidly turn one object or tagged group around a pivot and axis |
Here is the core pattern. The sensor and wings travel with the ship, release without snapping, then the wings deploy and the ship becomes its final design:
attach3(sensor, ship, (0,0,0.8));
attach3(leftWing, ship, (0,-1.2,0));
attach3(rightWing, ship, (0,1.2,0));
par { draw(route, 2.8, smooth); travel3(ship, route, 2.8, smooth); }
attach3(sensor, none);
attach3(leftWing, none); attach3(rightWing, none);
par {
turn3(wings, ship, z, 90, 1.1, smooth);
become3(ship, finalBlueprint, 1.1, smooth);
}
view3(spacecraft, "isometric", 1.2, smooth, 1.4);
▶ Compact five-word reference:
▶ Vertical creator story:
▶ Production lab (relationships, contours, finishes, OBJ, and variable tube):
Design details that make 3D feel professional
- Give the main subject one persistent id. Declare alternate forms as hidden
blueprints and use
become3; do not swap the whole scene. - Tag parts that should turn or frame together.
view3andturn3both accept a tag, so one spatial rig remains one authoring concept. - Attach before the shared move; release immediately before a part gets its own motion. Release preserves the resolved world position.
- Use
view3(...,"fit")to preserve the current viewing direction, or choosefront,side,top, orisometric. A margin around1.15–1.35is useful for landscape; start around1.45–1.7for vertical video with text above and a social footer below. - Put shot changes at idea boundaries, then let object motion carry the middle of the explanation. Constant camera motion makes depth harder to read.
view3uses transformed group bounds and the active canvas aspect ratio, so the same intent works in landscape and portrait without a second 3D mode. In a creator/quiz scene it also fits the actual media rectangle, not the full canvas behind the heading and social footer.travel3reads the route’s current transform every frame. A route may rotate or move in the sameparblock without pre-baking dozens of coordinates.- Use
attach3(part, body, offset, rigid)for a mechanical assembly. Its offset lives in the body’s local frame, it turns with the body, and release freezes both position and orientation exactly.
▶ See it play:
The 3D cast
| shape | write | draws |
|---|---|---|
| cube3 | cube3(box, (0,0,1), (2,2,2)); | a box (width, depth, height) |
| sphere3 | sphere3(ball, (0,0,1), 0.9); | a ball of that radius |
| point3 | point3(p, (1,1,1)); | a small marker in space |
| line3 / arrow3 | arrow3(v, (0,0,0), (0,0,2)); | a segment / a vector |
| grid3 | grid3(floor, (0,0,0), 5, 1); | a ground grid to sit things on |
| axes3 | axes3(ax, (0,0,0), 3); | labelled x, y, z arrows |
Style and reveal them with words you already know — color, opacity, show,
flash:
cube3(box, (0, 0, 1), (2, 2, 2)); color(box, cyan);
show(box, 0.6);
Labelling a point in space
To put words on a 3D point, make an ordinary 2D text and pin it there with
pin3. As the camera moves, the label sticks to its point:
text(tag, (0, 0), "origin");
pin3(tag, (0, 0, 0));
pin3 deliberately stays the same screen size. When the label should feel as
if it lives in the world, use label3 and give its desired world height:
text(pName, (0,0), "P");
label3(pName, pointP, 0.35); // gets smaller as P moves away
Relationships, projections, and contours
These are live construction words, not snapshots:
point3(p, (2,1,3));
project3(shadow, p, "xy");
link3(drop, p, shadow, 0.06);
surface3(bowl, "x^2+y^2", (-2,2), (-2,2), 24);
contour3(levelOne, bowl, 1);
Move p and both shadow and drop recompute every frame. contour3 accepts
a surface3 height field and extracts the requested z level. Use these words
for geometry, vectors, optimization, fields, and engineering callouts—the
engine owns the relationship while the author owns the story.
Textbook dimension-story series
These portrait stories use the same 3D engine for a textbook-friendly purpose: start with one familiar object, preserve its identity, and let the next dimension arrive through motion. Each gallery page contains the complete source and its video card.
| Story | Dimensional journey | Teaching idea |
|---|---|---|
| The trapped light beam | 1D → 2D → 3D | Distance grows from 5 to 13 to 85 through nested right triangles. |
| How space learned to grow | point → line → plane → room | A line sweeps into a surface; the surface lifts into volume. |
| Length, area, volume | 1D → 2D → 3D | Why measured units become cm, cm², and cm³. |
| A point gets an address | x → (x,y) → (x,y,z) | Each new axis adds one coordinate to the same point. |
| The revolving semicircle | diameter → curve → sphere | A solid can be generated from a lower-dimensional rule. |
| Statistical dimensions | list → scatter → cloud | More variables require a richer coordinate world. |
| Dimension reduction | 3D → 2D → 1D | A sphere reveals a section, then the section reveals a diameter. |
| Watermelon sections | whole → halves → ¼ + ¾ | Perpendicular great-circle cuts create meaningful pieces. |
Creator pattern: keep the mathematical subject persistent, introduce one axis
or section at a time, and hold the settled frame long enough to read the new
formula. Use view3(...,"fit") at dimensional boundaries; use ordinary object
motion inside each explanation.
Curves and surfaces
Draw a wire through space from three formulas of t (a helix, here), or a
surface from a height formula z = f(x, y):
curve3(helix, "cos(t)", "sin(t)", "t*0.2", (0, 12));
surface3(wave, "sin(x)*cos(y)", (-3, 3), (-3, 3));
For shapes a plain height field can’t make — a torus, a Möbius strip —
use param3, which takes three formulas of two parameters, u and v:
param3(torus, "(3 + cos(v))*cos(u)", "(3 + cos(v))*sin(u)", "sin(v)",
(0, 6.28), (0, 6.28));
One formula rule: always put
*between names. Writepi*t, neverpit(manic readspitas one unknown word). Same forv*v, notvv.
Textbook sections: halves, quarters, and the remainder
param3 is also the V2 route for an exact authored section of a curved solid.
Bound one parameter to describe only the required half or quarter of the sphere,
and add a second bounded surface for the exposed cut face. This keeps the lesson
mathematical: the geometry is defined by the section, not hidden by a flat mask.
The watermelon example uses that pattern to compare horizontal and vertical great-circle cuts, then makes two perpendicular cuts and separates a quarter from its three-quarter remainder:
// A sphere quarter: longitude u spans 90°, latitude v spans the full height.
param3(quarter,
"3*cos(v)*cos(u)", "3*cos(v)*sin(u)", "3*sin(v)",
(0, pi/2), (-pi/2, pi/2));
finish3(quarter, "shading=smooth material=matte depth=0.25 shadow=0.18");
▶ Animated textbook reconstruction:
Open the complete, copyable source in the 3D scenes gallery.
Practical authoring tips:
- Keep the outside shell and each exposed section face as separate tagged entities. They can be revealed, shifted, and recoloured as one teaching unit.
- Use a light
papertemplate and restrained face colours when the goal is a textbook diagram; depth should clarify the construction, not dominate it. - Frame each new construction with
view3(tag,"fit",...)after the cast changes. The camera then follows the mathematical subject instead of fixed coordinates. - V2 supports exact authored sections through bounded
param3. A generic verb that cuts any arbitrary solid and automatically creates the resulting pieces is intentionally deferred to V3.
Solids
Build filled, shaded solids:
prism3/pyramid3— n-sided prisms and cones (use many sides for a cylinder or a smooth cone).revolve3— spin a radius profiler(t)around the upright axis (vases, spheres, lathe shapes).extrude3— lift a flat 2D shape (even a boolean cut-out) straight up into a solid.
prism3(hex, (0, 0, 1), 6, 1, 2);
revolve3(vase, (3, 0, 1.5), "0.7 + 0.4*sin(t*2)", (0, 3));
Giving lines some body
A 3D line, arrow, or curve is a thin thread by default. thick turns it into a
rounded tube (arrows grow a solid head):
arrow3(v, (0, 0, 0), (2, 2, 2)); thick(v, 0.04);
For a horn, vessel, nerve, pipe, or any path whose radius changes, use a
normalized radius profile (t=0 start, t=1 end):
curve3(spine, "4*t-2", "sin(6*t)*0.2", "0", (0,1));
tube3(horn, spine, "0.06 + 0.28*t", 14);
One optional render finish
The default is still Manic’s restrained, template-aware diagram rendering.
When an object needs a different surface treatment, finish3 keeps the choice
in one bounded string:
finish3(globe, "shading=smooth material=metal depth=0.2 shadow=0.2");
finish3(terrain, "mesh=0.25 texture=checker scale=3");
finish3(shell, "material=glass shading=smooth");
- Start with
shading=smoothfor spheres/organic surfaces and leave boxes flat. - A little
meshclarifies topology;1is intentionally strong. depthandshadoware subtle readability controls from0to1, not a replacement for authored lighting.checkerandstripesare procedural and deterministic; Manic does not load texture scripts or arbitrary shaders.
Controlled OBJ models
model3(mark, "asset:models/manic-pyramid.obj", (0,0,1), 1.4);
finish3(mark, "shading=smooth material=metal mesh=0.12");
model3 reads geometry only: OBJ vertices, polygon faces (triangulated), and
lines. It ignores material/script features and enforces file/geometry limits.
An asset: URI selects a file packaged with Manic, so it works from the CLI,
Docker image, or production backend without a launch-directory assumption or
extra flag. An ordinary path such as uploads/my-model.obj still works for a
user-owned model, but the UI/backend must provision that file. Use built-in
solids when they express the same idea—they remain the lightest option.
Available bundled 3D assets
| Stable URI | What it is | Good for |
|---|---|---|
asset:models/manic-pyramid.obj | Small generic pyramid OBJ | Learning model3, a beacon, marker, monument, or placeholder model |
Bundled names are intentionally few and predictable. See the full
Bundled assets catalog and do not invent an asset: name that is
not listed. To add one to Manic itself, place the
geometry-only file under assets/models/, document its URI here and in
assets/README.md, and add a checked example. The release, Docker, EC2, and
playground pipelines copy the complete assets/ directory automatically, so
future catalog entries need no per-file deployment rule.
Moving in 3D
Same rhythm as the 2D verbs, with the 3 on the end:
par {
rotate3(box, (0, 0, 360), 4, linear); // spin the box
orbit3(70, 25, 12, 4, smooth); // orbit the camera around it
roll3(-20, 4, smooth); // bank around the view direction
}
move3/shift3— move to / by a pointrotate3— turn it (degrees around x, y, z)grow3— stretch a line or arrow’s tip to a new pointorbit3— swing the camera (angle around, angle up, distance)roll3— bank the camera around its viewing direction; it can run besideorbit3inpar, including through stable overhead/underside viewslook3— aim the camera at a new point
These remain the precise controls. Use them when exact coordinates or a specific orbit are part of the explanation; use the creator-first five for camera composition and relationship choreography.
Check the transitions, not only the last frame
manic check examples/three-d-v2-lab.manic --canvas portrait
The publishing audit samples camera transitions between named steps. It warns
when projected 3D bounds leave the creator media rectangle, an orbit/zoom reads
as a shock, the eye enters geometry, or a live spatial relationship has lost
its source. It also checks the settled frame of every step. The most useful
repair is usually semantic: tag the shot’s subject and call view3(tag,"fit")
after the cast changes, or give a camera beat more duration.
Morphing one shape into another
morph3 sets a shape up to become another; then to(..., morph, ...) blends
between them. It works for curves, surfaces, and solids — even a cube turning
into a sphere:
cube3(a, (0, 0, 1), (2, 2, 2));
sphere3(b, (0, 0, 0), 1.2); hidden(b);
morph3(a, b);
to(a, morph, 1, 2.5, smooth); // a cube melts into a ball
Which words work in 3D?
3D shapes speak most of the same vocabulary — color, opacity, hidden,
untraced, tag, and the verbs show, fade, draw, flash, pulse,
scale. A handful of words are 2D-only and will politely refuse on a 3D
shape (with a message that names the 3D replacement):
| if you reach for… | on a 3D shape, use… |
|---|---|
hue | color with a palette name |
stroke | thick |
move / rotate / spin | move3 / rotate3 |
cam / zoom | camera3 / orbit3 |
morph | morph3 |
Now see it all in motion in the 3D scenes gallery.