Coming from Manim
Motion Script covers the same ground as Manim's graphs, formulas and 3D scenes,
through @motionscript/math. The ideas carry over; the way a scene is written
does not. This page maps one to the other.
npm install @motionscript/math @motionscript/sdk
What changes
A scene is data, not a program. In Manim, construct runs once and each
self.play call renders as it goes. Here a scene is a document: the things in
it (nodes) and what happens to them (commands, each placed at a time). Nothing
runs in order. A frame is worked out from the time alone, so any frame can be
shown without playing the ones before it, and an editor can scrub, cut and
export any stretch of it.
A self.play is a command with an at. Where Manim waits for one
animation to end before the next starts, a command says when it starts and how
long it lasts. Two commands at the same at run together; self.wait(1) is a
second with no command in it.
An updater is a binding. Manim keeps a value in a ValueTracker and
recomputes what depends on it every frame. Here the value is a document
variable, a prop that depends on it is written as a formula ("=[t]"), and
variableTo drives it. There is no frame-to-frame state, so the result is the
same however you get to a frame.
A coordinate system holds what is drawn on it. axes.plot(...) and
axes.c2p(...) become children: a functionPlot placed inside an axes2d is
drawn in its graph units, and so is the x/y of any other node put inside
it, a point, a label or an image.
LaTeX needs no TeX install. MathJax is bundled, and every glyph of a formula is a path that can be coloured, written on and morphed.
A scene side by side
A dot sliding along a parabola to its lowest point. In Manim:
class Lowest(Scene):
def construct(self):
axes = Axes(x_range=[0, 10], y_range=[0, 100, 10])
curve = axes.plot(lambda x: 2 * (x - 5) ** 2)
t = ValueTracker(0)
dot = always_redraw(lambda: Dot(axes.i2gp(t.get_value(), curve)))
self.add(axes, curve, dot)
self.play(t.animate.set_value(5))
As a document:
{
kind: 'composition',
variables: [{ id: 't', value: 0 }],
nodes: [
{ id: 'axes', type: 'axes2d', parentId: null, layerOrder: 0,
props: { width: 760, height: 420, xRange: [0, 10], yRange: [0, 100], yStep: 10 } },
{ id: 'curve', type: 'functionPlot', parentId: 'axes', layerOrder: 0,
props: { expression: '2(x - 5)^2' } },
// Inside the plot, a point rides it: its x says where, its height is the curve's.
{ id: 'dot', type: 'plotPoint', parentId: 'curve', layerOrder: 0,
props: { x: '=[t]' } },
],
commands: [
{ id: 'slide', type: 'variableTo', targetId: 't', at: 0, duration: 1.5, args: { value: 5 } },
],
}
Classes
| Manim | Motion Script |
|---|---|
NumberLine, UnitInterval | numberLine |
Axes | axes2d |
NumberPlane, ComplexPlane | numberPlane |
PolarPlane | polarPlane |
ThreeDAxes | axes3d, a 3D graph with its own camera |
axes.plot, ParametricFunction, ImplicitFunction, polar graphs | functionPlot: how the equation is written decides which |
axes.plot_line_graph, Polygon on axes | pointsPlot |
Surface, axes.plot_surface | surfacePlot3d |
ParametricFunction in 3D | curvePlot3d |
Dot, LabeledDot, axes.i2gp | plotPoint (inside a plot, it rides it) |
Dot3D | plotPoint3d |
Arrow, Vector, DoubleArrow, Line on axes | vectorArrow; its head is the stroke's endCap |
Arrow3D | vectorArrow3d |
axes.get_area | curveArea |
axes.get_riemann_rectangles | riemannSum |
TangentLine, axes.get_secant_slope_group | tangentLine (dx above zero for a secant) |
axes.get_vertical_line, get_lines_to_point | dropLines |
ArrowVectorField | vectorField |
StreamLines | streamLines |
TracedPath | tracedPath |
MathTex, Tex | latex |
Matrix, IntegerMatrix | latexMatrix |
Brace, BraceLabel, BraceBetweenPoints | curlyBrace |
Angle, RightAngle | angleMark |
SurroundingRectangle, Underline, Cross | emphasisMark |
Graph, DiGraph | networkGraph |
SampleSpace | sampleSpace |
DecimalNumber, Variable | core's number, bound to a variable |
Table | rectangularGrid in @motionscript/grids |
BarChart | barChart in @motionscript/charts |
Animations
| Manim | Motion Script |
|---|---|
Create, DrawBorderThenFill | progressTo (start, end) |
Write | writeTo on a latex |
FadeIn, FadeOut | fadeTo |
Transform, ReplacementTransform | morphTo on a plot, a formula or a field |
TransformMatchingTex | morphTo on a latex: shared terms travel |
Indicate | pulse |
Wiggle | wiggle |
ShowPassingFlash | flash |
Circumscribe | an emphasisMark, drawn on with progressTo |
MoveAlongPath | moveTo with a path |
Rotate, ScaleInPlace | rotateTo, scaleTo |
obj.animate.set_color | fillTo, strokeTo; partFillTo for one part of a formula or a graph |
ValueTracker.animate.set_value | variableTo |
axes range changes | rangeTo |
ApplyMatrix, LinearTransformationScene | transformTo on a numberPlane |
ApplyPointwiseFunction, prepare_for_nonlinear_transform | warpTo on a numberPlane |
graph.change_layout | arrangeTo on a networkGraph |
move_camera, begin_ambient_camera_rotation | orbitTo on an axes3d, a canvas3d or a camera3d |
self.camera.frame.animate.move_to(dot) | a camera with follow naming the dot, and zoomTo |
Where it differs
- Angles in equations are radians and trig takes bare names (
sin(x)); a name an equation does not know is a value of yours, set inparamsand animated withparamsTo. - 3D graphs are z up and right-handed, as in Manim; the scene itself is y
up, so an
axes3d's default camera already shows the textbook view. - A morph between two equations blends their points, so any kind of curve bends into any other. An implicit curve has no points to blend and changes half way.
- Lengths that are not graph values are pixels in 2D and scene units in 3D: a point's radius, a stroke's weight, a label's size. They keep their size when the axes' range changes.
Every component above has its own page; the Math overview lists them by group.