Shapes

Eight methods declare geometry. Each takes one options object:

new Graphics()
  .rect({ width: 200, height: 120, cornerRadius: 16 })
  .ellipse({ x: 140, y: 0, width: 80, height: 80 })
  .fill('royalblue');

Shapes recorded before a paint call are combined into one surface and painted together. See Painting.

Props every shape takes

PropTypeDefaultDescription
x / ynumber0Centre of the shape. y is up
width / heightnumber0Size of the shape's box
rotationnumber0Degrees, applied to this shape only
scalenumber1Applied to this shape only
pivotVector2 | AnchorKeycentrePivot for this shape's rotation and scale
opacitynumber1Alpha for this shape
blendBlendMode'pass-through'How this shape mixes with what is under it
start / endnumber0 / 1Trim the outline, for draw-on animation

rotation and scale here affect one shape. To turn the whole figure at once, use the graphics-level modifiers.

Positioning by anchor

x and y place the shape's centre. Instead you can pass one of the nine anchors, whose value is the point that anchor lands on:

// Top-right corner sits at (200, 150)
new Graphics().rect({ topRight: { x: 200, y: 150 }, width: 120, height: 60 });

The anchors are center, topLeft, topCenter, topRight, centerLeft, centerRight, bottomLeft, bottomCenter, bottomRight.

Pass at most one, and never together with x, y or pivot. Both mistakes throw rather than being ignored.

An anchor also sets the shape's pivot, so a rotation turns about the pinned corner:

.rect({ bottomLeft: { x: -200, y: -100 }, width: 100, height: 100, rotation: 15 })

rect

.rect({ width: 200, height: 120, cornerRadius: 16, cornerStyle: 'rounded' })
PropTypeDefaultDescription
cornerRadiusnumber | RectCornerRadius0One radius, or one per corner
cornerStyleRectCornerStyle'rounded'How the corner is cut

Corner values take the same forms as the cornerRadius attribute.

ellipse

.ellipse({ width: 200, height: 200 })              // circle
.ellipse({ width: 240, height: 120 })              // oval
.ellipse({ width: 200, height: 200, sweep: 90 })   // quarter pie
PropTypeDefaultDescription
sweepnumber360Arc length in degrees
startAnglenumber0Where the arc begins, in degrees. 0 is 3 o'clock
rationumber1Inner radius as a fraction of the outer, so the size of the hole

ratio makes a ring without cutting one circle out of another:

.ellipse({ width: 300, height: 300, ratio: 0 })               // solid disk
.ellipse({ width: 300, height: 300, ratio: 0.7 })             // donut
.ellipse({ width: 300, height: 300, ratio: 0.7, sweep: 120 }) // ring segment
.ellipse({ width: 300, height: 300, ratio: 0, sweep: 120 })   // pie wedge

Both ends of the range give a solid ellipse: at 0 there is no hole, and at the default 1 the hole reaches the outer edge. The useful values are in between, and animating ratio opens the hole smoothly.

For a progress ring, set startAngle: -90 so the arc starts at 12 o'clock.

polygon and polygram

Regular polygons and stars.

.polygon({ width: 160, height: 160, sides: 6, cornerRadius: 8 })  // hexagon
.polygram({ width: 160, height: 160, sides: 5, ratio: 0.5 })      // 5-point star
PropTypeDefaultDescription
sidesnumber5Number of points
rationumber0.5Polygram only. Inner radius as a fraction of the outer
cornerRadiusnumber0Rounds the vertices
cornerStyleCornerStyle'rounded'How each vertex is cut

line

A polyline through explicit points.

.line({
  points: [{ x: -200, y: -60 }, { x: -60, y: 40 }, { x: 80, y: -20 }, { x: 200, y: 90 }],
  radius: 12,
})
.stroke({ weight: 6, fill: '#6990DD' })
PropTypeDefaultDescription
pointsVector2[][]The path, in local coordinates
radiusnumber0Corner rounding at each joint
closedbooleanfalseJoin the last point back to the first

A line is an open outline, so it is normally painted with .stroke(). Set closed: true to make it fillable.

line does not take the anchor props, since its geometry is already explicit points.

path

Vector paths, from an SVG d string, a command array, or a PathBuilder.

import { Graphics, PathBuilder } from 'motion-script';

// From an SVG string, straight out of Figma or Illustrator
.path({ data: 'M 0 -60 L 52 40 L -52 40 Z' })

// Built in code
.path(
  new PathBuilder()
    .moveTo(-100, 0)
    .bezierCurveTo(-100, 80, 100, 80, 100, 0)
    .close(),
)

PathBuilder has moveTo, lineTo, bezierCurveTo, quadraticCurveTo, arc and close. Pass the builder itself to .path() and it is converted for you.

PropTypeDefaultDescription
datastring | PathCommand[]''SVG path data, or typed commands
centerBounds[minX, minY, maxX, maxY]Frame to centre the path against

A path positions itself from its own data rather than from x and y. By default it centres on its own bounding box, which is right for a lone path and wrong when several paths belong to one figure, since each would centre separately. Give them all the same centerBounds and they share one frame:

const frame: [number, number, number, number] = [-300, -300, 300, 300];

new Graphics()
  .path(bodyPath.toPathState({ centerBounds: frame }))
  .path(wingPath.toPathState({ centerBounds: frame }))
  .fill('tomato');

PathBuilder.toPathState(props) attaches the frame, or any other shape prop, to a built path.

text and richText

Text as part of a drawing rather than as a node. Use it for axis labels, legends and callouts, where a Text node would be more machinery than the job needs.

.text({ text: '0.5', x: 120, y: -40, fontSize: 24 })
.fill('white/70')
PropTypeDefaultDescription
textstring''The string to draw
fontSizenumber | 'autofit'16Size in px
fontFamilystring'Arial'Family. It must be in the project's font manifest
fontWeightnumber400Weight
fontStyle'normal' | 'italic''normal'Style
letterSpacingnumber0Extra tracking
lineHeightnumber0Line box height
textAlignTextAlign'center'Anchor within width
wrapbooleanfalseWrap to width
pathPathDatanullWrap the glyphs along a path

richText takes spans instead of text, for runs with mixed styling. It is the drawn form of the RichText node.

Anything that needs layout, measurement, or animation with .to() should be a Text node.

Inherited defaults

Leave a typography field off and the drawing inherits it, the same way a Text node does — from the nearest enclosing <DefaultTextStyle>, falling back to the project theme's typography.default preset. So a label drawn inside a custom node picks up the document's face without the node having to take a fontFamily prop and thread it through:

<DefaultTextStyle fontFamily="Inter" fontSize={24}>
  <Chart />   {/* its .text({ text: '0.5' }) axis labels shape in Inter at 24 */}
</DefaultTextStyle>

Only the shaping fields inherit: fontFamily, fontSize, fontWeight, fontStyle, letterSpacing, lineHeight and textAlign. fill, stroke and shadow don't, because in a Graphics those are group-scoped paint ops rather than per-shape slots — .text(…).rect(…).fill('red') paints both shapes together, and a shape with no paint op after it draws nothing at all. Paint your drawn text explicitly.

A node whose text is its own vocabulary rather than the document's opts out for everything it draws:

protected onRender(ctx: RenderContext): void {
  ctx.pushTextStyle(null);
  try { super.onRender(ctx); } finally { ctx.popTextStyle(); }
}

The Code node does exactly this: its token positions are measured against a monospaced face, so a document-wide display family would take the block apart column by column.

Drawing on with start and end

Every shape takes start and end, the same outline trim the shape nodes have:

new Graphics()
  .path({ data: signature, end: progress })
  .stroke({ weight: 8, fill: 'white' });

progress is just a number read while building, so it can come from a signal or a node property.