Ollin

Drawing

One of p5's strengths is that you can learn its whole drawing surface in an afternoon. Ollin keeps its own surface small and its names familiar for the same reason. You call these methods bare inside draw(), and they forward to the Drawer.

These calls take the point and rectangle types Vector2 and Rectangle, which are documented in Geometry. Color is documented in Color.

Contents

Background and style

background

background(_ color: Color)

Clear the frame to color. The renderer clears every frame, so call this first in draw(). It drops what the canvas has drawn so far this frame, and a layer drawn before it keeps its marks. Inside a withTarget block it clears that layer instead (see Effects).

background(.white)

background(.clear), or any color with an alpha under 1, makes the canvas see-through. The window paints it over black, and every export keeps the coverage as the file's alpha, so the work lands over another layer. See Transparent output.

fill / noFill

fill(_ color: Color)
fill(_ gradient: Gradient)
fill(_ paint: Paint)
noFill()

Set the paint used by filled shapes, either a flat color or a gradient. noFill() turns the fill off.

fill(.red)
drawCircle(width / 2, height / 2, 80)   // solid red disc
fill(.radial(center: center, radius: 80, [.white, .red]))
drawCircle(width / 2, height / 2, 80)   // shaded disc
noFill()                                // following shapes are outline-only

Gradients (linear, radial, or along-path) are covered in Color → Gradient paint.

stroke / noStroke

stroke(_ color: Color)
stroke(_ gradient: Gradient)
stroke(_ paint: Paint)
noStroke()

Set the outline paint, either a flat color or a gradient. noStroke() turns the outline off. An .alongPath gradient runs from start to end along lines, curves, and stroked paths (see Color → Gradient paint).

stroke(.black)
drawRect(40, 40, 120, 80)
stroke(.alongPath([.red, .blue]))
drawLine(Vector2(40, 160), Vector2(160, 160))   // red fading to blue
noStroke()                              // following shapes have no outline

strokeWeight

strokeWeight(_ weight: Double, in units: StrokeUnits = .screen)

Outline thickness in points. units matters only to a line drawn with 3D points: .screen keeps it the same width at every distance, .world reads the weight in the scene's units so the line thins as it recedes.

stroke(.black)
strokeWeight(3)
drawCircle(width / 2, height / 2, 120)

strokeAlign

strokeAlign(_ align: StrokeAlign)   // .center (default), .inside, .outside

Where the stroke sits relative to a shape's outline. The default, .center, straddles the edge with half the weight inside and half outside, which is what p5 and Processing do. .inside keeps the whole stroke within the shape, so the footprint does not change as the weight grows. That helps in tiled grids, where an outward border would overlap its neighbors. .outside puts the stroke entirely beyond the edge. This is state, like strokeWeight, so it holds until you change it.

Three labeled rows. strokeAlign: the same heavy circle outline straddling the marked shape edge, held entirely inside it, and pushed entirely outside it. strokeJoin: the same bend turned with a sharp miter, a cut-off bevel, and a rounded arc. strokeCap: the same segment ended flat at its marked endpoints, rounded past them, and squared past them
fill(.gray); stroke(.black); strokeWeight(20)
strokeAlign(.inside)
drawCircle(width / 2, height / 2, 120)   // outline grows inward; radius-120 footprint kept

It applies to the analytic shapes: circles, ellipses, rectangles, the polygon and star family, and the rest of the SDF catalog. For those, the inset or outset is a geometrically exact offset of the outline. Shapes with no inside and outside keep a centered stroke. That covers lines, point markers, the open drawArc, and the tessellated drawPolyline / drawPolygon / drawShape. With hollow, the band already has two edges to stroke, so alignment does not apply there.

strokeJoin

strokeJoin(_ join: StrokeJoin)   // .miter (default), .bevel, .round

How a stroked path turns its corners. .miter extends the two outer edges until they meet at a sharp point, which keeps a chevron or a star's tips crisp. When a corner is acute enough that the point would run out into a long spike, .miter falls back to a flat bevel. .bevel always cuts the corner off with a straight edge, and .round fills it with an arc for a smooth bend. This is state, like strokeWeight, so it holds until you change it.

stroke(.black); strokeWeight(20); strokeJoin(.round)
drawPolyline([Vector2(120, 360), Vector2(540, 120), Vector2(960, 360)])   // a rounded peak

It applies to the stroked paths with interior corners: drawPolyline, the drawPolygon outline, drawShape contours, and the flattened drawBezier. The analytic SDF shapes draw their own outlines, and drawLine is a single segment, so neither has a join to style.

strokeCap

strokeCap(_ cap: StrokeCap)   // .butt (default), .round, .square

How the open ends of a stroked path are finished. .butt ends the stroke flat at the endpoint, so its footprint stops exactly where the path does. .round adds a half-disk over each end, a rounded tip. .square adds a flat extension past the endpoint, half the stroke weight long. Both .round and .square therefore reach half the weight beyond the path's end. This is state, like strokeWeight, so it holds until you change it.

stroke(.black); strokeWeight(24); strokeCap(.round)
drawPolyline([Vector2(300, 540), Vector2(780, 540)])   // rounded tips past each end

It applies to the open stroked paths: drawLine, drawBezier, drawPolyline, and any open drawShape contour. Closed outlines have no ends to cap, which covers the drawPolygon outline and any closed contour.

strokeProfile / noStrokeProfile

strokeProfile(_ profile: StrokeProfile)         // .uniform (default), .taper, .ramp, .nib, .values
strokeProfile { t in ... }                      // width by hand, over the path fraction
noStrokeProfile()

How the stroke width varies as the path travels, which is the difference between a drawn line and a made mark. A profile is a multiplier, not a width. strokeWeight still sets how wide the mark gets, and the profile sets what fraction of that width it uses at each point. This is state, like strokeWeight, so it holds until you change it, and withState { } saves and restores it.

The named profiles cover the common marks:

ProfileThe mark
.uniformOne width the whole way. The default.
.taper(start:end:)Thin at the ends and full in the middle, like a brush pressed down and lifted. start and end are the multipliers at the two ends, both 0 by default, so .taper(start: 1) keeps a blunt start and lifts off at the end.
.ramp(from:to:)A straight wedge from one width to another.
.nib(angle:thinness:)A flat calligraphy pen held at angle. The mark is widest where the path runs across the nib, and a hairline where it runs along it. thinness is how much width the thinnest direction keeps.
.values([...])Evenly spaced multipliers along the path, interpolated between them. Use it for a width curve you write by hand, or one recorded from an input.

The closure form is a StrokeProfile(_:) over the path fraction, and StrokeProfile(directional:) sees the heading at that point as well, which is how .nib knows which way the pen is running. A profile answers one point at a time, so calling it (profile(0.5), or profile(0.5, heading)) is how a sketch asks for the multiplier at a place along the path.

The same S-curve drawn three ways at one stroke weight: an even line, a taper that swells in the middle and vanishes at both ends, and a calligraphic nib that thickens and thins as the curve turns
strokeWeight(12)
strokeProfile(.taper())
drawBezier(Vector2(120, 700), Vector2(540, 120), Vector2(960, 700))   // thin, fat, thin

It applies wherever a path is expanded into a stroke: drawLine, drawBezier, drawPolyline, drawCurve, and the outlines of drawShape and drawPolygon. The analytic shapes carry a single width by construction. That means drawCircle, drawRect, drawStar, and the rest of the SDF catalog keep drawing at strokeWeight and say so once on the console.

Two things to know:

  • Width is read at every point of the path, as a fraction from 0 at the start to 1 at the end. The fraction is measured along the path's own length. A path with only a handful of points changes width in visible steps. Sample a curve densely enough for the profile to have somewhere to go. Long segments are split automatically, but the profile can only follow the points it is given.
  • A closure profile runs where the stroke is expanded, not on the sketch, so it cannot reach time or a @Param directly. Copy what it needs into a local first, as in let clock = time, and capture that. The named profiles take their animation as an argument, so .nib(angle: time) needs nothing special.

A profiled stroke stays vector on the way out. --export-svg and --export-pdf write the region the mark covers as a filled outline, not as a stroked path with one width. A plotted or printed mark then matches the screen. See Export.

A profile shapes a stroke by where you are along a finished path. A mark being drawn right now has no finished path to take a fraction of, so see Marks instead. A StrokeMark measures how fast the pointer is traveling and how hard it is pressed, then drives width and opacity from that. The two multiply, so a dynamic mark can still take a profiled lift-off.

strokeDash / noStrokeDash

strokeDash(_ lengths: [Double], phase: Double = 0)   // dash, gap, dash, gap, in points along the path
strokeDash(_ dash: StrokeDash)                       // .dashes(_:gap:), .dots(spacing:), or StrokeDash([12, 8], phase: 0)
noStrokeDash()

Cut every stroke into dashes. The lengths alternate dash and gap, measured along the path in its own units, so scale scales the dashes along with the weight. An odd count repeats itself, the way SVG reads one, so [8, 4, 2] is [8, 4, 2, 8, 4, 2]. This is state, like strokeWeight, so it holds until you change it, and withState { } saves and restores it.

stroke(.black); strokeWeight(4)
strokeDash([12, 8])
drawPolyline(points)                     // 12 on, 8 off, along the whole path
strokeDash([12, 8], phase: time * 60)    // the same, marching forward

phase slides the pattern along the path. A positive phase carries the dashes forward, so a phase that grows with time is a marching outline, and a phase of one period (the sum of the lengths) brings the pattern back to where it started.

Every dash is a stroke of its own. strokeCap finishes both ends of each one, and a corner inside a dash keeps its strokeJoin. That is what makes a dotted line one more line. .dots(spacing:) lays down zero-length dashes, which are round dots under strokeCap(.round), squares under .square, and nothing at all under .butt.

strokeCap(.round); strokeWeight(6)
strokeDash(.dots(spacing: 12))
drawCurve(points)
The same S-curve dashed three ways at one stroke weight: an even dash, a row of dots, and a taper whose dashes shrink together toward both ends

The cut happens before the path is expanded, so the other stroke tools keep working on the whole path rather than on each dash. A strokeProfile reads its place on the whole, so a taper tapers across the gaps instead of starting over at every dash. A brush stamps each dash and skips the gaps. An along-path gradient runs on through them. On a closed path the pattern starts at the first point and runs the closing segment too, and a dash that crosses that seam is one dash, so the corner there is still a corner.

It applies wherever a path is expanded into a stroke: drawLine, drawBezier, drawPolyline, drawCurve, drawArc, the outlines of drawShape and drawPolygon, and stroked text. While a dash is on, drawCircle, drawEllipse, drawRect, drawTriangle, drawNgon, drawStar, drawTrapezoid, and drawParallelogram hand their outline to that same path, so they dash too, and a profile or a brush reaches them for as long as they do. The rest of the analytic catalog keeps its continuous outline and says so once on the console. A pattern too fine for its path (more than twenty thousand dashes on one stroke) draws the path whole and says so too.

StrokeDash is a plain value: lengths and phase, plus period and isSolid for a pattern that leaves the path whole. Contour.dashed(_:) and Shape.dashed(_:) make the same cut as geometry, handing back the dashes as open contours to draw, offset, or export however you like.

A dashed stroke stays vector on the way out. --export-svg, --export-pdf, and the G-code writer carry every dash as a subpath of its own, so a plotter lifts the pen at each gap and lays down exactly the dashes the screen showed, and a filled shape under a dashed outline keeps its fill whole. See Export.

hollow / solid

hollow(_ width: Double)
solid()

Draw region shapes (circle, rect, star, triangle, heart, …) as a constant-width band along their outline instead of a solid interior. It is what drawRing is to a circle, applied to every shape. The fill color paints the band, and an active stroke borders both of its edges. That lets you frame a hollow shape in a second color. A stroke alone cannot do that, because it would be the only band. width is the band thickness, centered on the edge. This is state, like fill and stroke, and solid() returns to filled shapes. Points, lines, and drawRing, which is already a band, ignore it.

The same five-point star drawn twice: solid, a filled shape with one stroked outline, and under hollow(16), a constant-width band hugging the outline with the fill painting the band and the stroke running along both of its edges
hollow(16)
fill(.cyan)
stroke(.black)
strokeWeight(2)
drawStar(width / 2, height / 2, 140, 70, points: 5)   // cyan ring-star, black edges
solid()                                                // back to solid fills

pointSize

pointSize(_ size: Double)

Diameter of a drawPoint dot, in points. This is state, like strokeWeight, so set it once and it holds. drawPoint takes a per-call override.

pointSize(4)
drawPoint(width / 2, height / 2)

pointMarker

pointMarker(_ marker: PointMarker)

The glyph drawPoint stamps: .circle, .square, .diamond, .cross, or .x. The default is .circle, .cross is a plus sign, and .x is a diagonal cross. This is state, like pointSize, so set it once and it holds. Every marker is sized by its on-screen diameter, so the footprint stays the same when you switch glyphs. Every marker takes the current fill color and ignores stroke.

pointMarker(.cross)
pointSize(10)
for p in cloud { drawPoint(at: p) }          // a scatter of plus signs

blendMode

blendMode(_ mode: BlendMode)   // .normal (default), .add, .subtract, .multiply, .screen, .lightest, .darkest

How following shapes combine with what is already on the canvas. The default, .normal, lays each shape over the previous ones, so a translucent shape shows what is beneath. The other modes combine the new color with the destination. The main one is .add, which sums colors as light, so overlapping marks brighten toward white instead of the topmost one winning. Light-field and particle sketches want that additive accumulation, and the "sandpainting" depth of field in Rendering/DepthOfField is built from it.

blendMode(.add)                          // overlaps brighten, best on a dark background
noStroke()
for p in particles {
    fill(Color(hue: p.hue, saturation: 0.7, brightness: 1, alpha: 0.1))
    drawCircle(p.x, p.y, p.r)            // a thousand faint dots pile into a glow
}

.add sums as light and lightens. .screen also lightens, more softly. .multiply stacks ink and darkens. .subtract darkens by removing light. .lightest and .darkest keep the lighter or the darker of the two colors, channel by channel. Blend mode applies to every primitive, including the SDF shapes, the tessellated paths, images, and text. Like other style, it is saved and restored by withState { }. You can scope an additive field that way and leave the rest of the frame normal.

One known limit affects .darkest. The analytic shapes darken their whole covering rectangle, not just the shape, and that covers drawCircle, drawRect, and the rest of the SDF catalog. The reason is that the GPU's min blend cannot ignore a quad's empty pixels. Until that is fixed, draw .darkest shapes on the triangle path, using drawPolygon with a many-sided outline, or draw them through a layer mask.

The canvas blends in linear light, the gamma-correct pipeline, so .add sums physically and two half-bright lights make a full-bright one. Against a dark background that reads as glowing accumulation. See Examples/Rendering/ToneMapping, where the flat .normal mode is the control case.

Basic shapes

Six panels: a filled circle, rectangle, and line on top, with markers showing that a circle's x, y is its center while a rectangle's is its top-left corner; an outlined circle, a filled-and-stroked rectangle, and a thick line below

drawPoint

drawPoint(_ x: Double, _ y: Double)
drawPoint(_ x: Double, _ y: Double, _ size: Double)
drawPoint(_ p: Vector2)
drawPoint(_ p: Vector2, size: Double)

A filled marker in the current fill color. It ignores stroke, so noFill() draws nothing. The glyph is the current pointMarker, a round dot by default. size is the on-screen diameter, and without it the marker uses the current pointSize. Each point is a single SDF instance, so a field of thousands stays cheap.

A row of round dots stepping down from bold to a faint sub-pixel speck, each labeled with its size, beside a block of tiny points reading as an even gray wash
fill(.black)
pointSize(3)
for p in cloud { drawPoint(at: p) }          // a scatter of dots
drawPoint(width / 2, height / 2, 12)     // one bigger dot

[!TIP] Sizes go all the way down. Round points, circles, lines, and shape outlines stay smooth at sub-pixel sizes. A dot, a line, or a strokeWeight thinner than a pixel fades by ink. It does not pop in, snap to a 1px floor, or flicker as it moves. A field of tiny points, a hairline drawLine, or a barely visible rectangle outline reads as a soft, even wash rather than hard speckle. Draw at whatever size and stroke weight the sketch wants, down to a fraction of a pixel.

drawLine

drawLine(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double)
drawLine(_ a: Vector2, _ b: Vector2)

A stroked line segment between two points. It honors strokeCap, which is butt by default, and takes solid, translucent, or gradient stroke paint. It renders through the high-quality stroke path, which expands the edges into triangles and adds a ~1px anti-aliasing fringe. The line therefore stays crisp and even at any angle and resolution, down to sub-pixel widths.

Straight lines at stroke weights from 14 down to a half-pixel hairline, at several angles, the thickest with accent dots where its flat butt ends stop exactly at the endpoints
stroke(.black)
drawLine(0, 0, width, height)           // corner to corner

drawArrow

drawArrow(from: Vector2, to: Vector2, headLength: Double? = nil, headWidth: Double? = nil)
drawArrow(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double,
          headLength: Double? = nil, headWidth: Double? = nil)

An arrow, drawn as a stroked shaft ending in a solid triangular head whose tip is exactly to. The whole mark, head included, takes the current stroke, so one stroke(...) call colors it. Use strokeWeight to thicken the shaft, and head measurements you leave out scale with it. Use it for a diagram's pointer, a vector field's glyph, or a force made visible.

An arrow with dots at its two anchor points, a thicker arrow whose head has grown with the stroke weight, and a ring of thin arrows pointing outward from a shared center
stroke(.crimson)
strokeWeight(3)
drawArrow(from: center, to: mouse)
drawArrow(from: p, to: p + force, headLength: 12)

The shaft stops at the head's base, so translucent arrows lay one even coat of ink. The translucent ring in the Shapes/Arrows example shows that.

drawCircle

drawCircle(_ x: Double, _ y: Double, _ radius: Double)
drawCircle(center: Vector2, radius: Double)
drawCircle(_ circle: Circle)

A circle. Give it a scalar center (positional x, y, radius), a Vector2 center:, or a Circle value.

A circle with its center dotted and an accent ray labeled radius reaching the rim, beside three concentric circles sharing one dotted center
drawCircle(width / 2, height / 2, 120)
drawCircle(center: Vector2(200, 200), radius: 60)
drawCircle(Circle(x: 200, y: 200, radius: 60))

To draw a whole array at once, see batches.

drawEllipse

drawEllipse(_ x: Double, _ y: Double, _ radiusX: Double, _ radiusY: Double)
drawEllipse(center: Vector2, radiusX: Double, radiusY: Double)

An ellipse. Give it a scalar center (positional x, y, rx, ry) or a Vector2 center:. As with drawCircle, you give the size as radii (rx, ry), not diameters, so equal radii draw a circle.

An ellipse with accent rays from its dotted center labeled rx across and ry down, beside one drawn with equal radii that reads as a circle
drawEllipse(width / 2, height / 2, 160, 90)
drawEllipse(center: Vector2(200, 200), radiusX: 60, radiusY: 90)

drawRect

drawRect(_ x: Double, _ y: Double, _ width: Double, _ height: Double, cornerRadius: Double = 0)
drawRect(corner: Vector2, width: Double, height: Double, cornerRadius: Double = 0)
drawRect(center: Vector2, width: Double, height: Double, cornerRadius: Double = 0)
drawRect(_ rectangle: Rectangle, cornerRadius: Double = 0)
drawRect(_ x: Double, _ y: Double, _ width: Double, _ height: Double, cornerRadii: CornerRadii)
drawRect(corner: Vector2, width: Double, height: Double, cornerRadii: CornerRadii)
drawRect(center: Vector2, width: Double, height: Double, cornerRadii: CornerRadii)
drawRect(_ rectangle: Rectangle, cornerRadii: CornerRadii)

A rectangle, anchored by its top-left corner or by its center, or built from a Rectangle value. The center form matches p5's rectMode(CENTER). cornerRadius rounds the corners and is clamped to half the shorter side. The default of 0 is a sharp rectangle.

cornerRadii takes a CornerRadii, one radius per corner. CornerRadii(topLeft:topRight:bottomRight:bottomLeft:) names all four, as they sit on the canvas; .top(r), .bottom(r), .left(r), and .right(r) round one pair and leave the other square, for a tab or a card that meets a straight edge; .all(r) is the one-radius form, and uniformRadius reads that one radius back, or nil when the corners differ. Two radii that meet along a side and add up past it are scaled down together, all four by the same factor, so the shape is kept and only its size gives; fitted(width:height:) is that rule as a call, for a sketch that wants the radii as they will draw. Four equal radii draw exactly as cornerRadius: does, on the same analytic path, and the vector exports write the outline as a path with an arc at each rounded corner.

Four rectangles: one anchored by an accent dot at its top-left corner, one by a dot at its center, one with its corners rounded by a cornerRadius of 26, and one with a different radius on each corner, the top-left the largest and the bottom-left square
drawRect(40, 40, 120, 80)                                   // top-left corner
drawRect(center: center, width: 200, height: 120)
drawRect(40, 40, 120, 80, cornerRadius: 16)                 // rounded corners
drawRect(40, 140, 120, 40, cornerRadii: .top(16))           // a tab
drawRect(40, 200, 120, 80, cornerRadii: CornerRadii(topLeft: 40, topRight: 8,
                                                     bottomRight: 24, bottomLeft: 0))

drawOrientedBox

drawOrientedBox(_ a: Vector2, _ b: Vector2, thickness: Double)
drawOrientedBox(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double, thickness: Double)

A rectangle placed by its two centerline endpoints a and b, with the given thickness across it. It draws a thick bar between two points with square ends, not round ones. drawRect is axis-aligned, and you would rotate it about its own center. This shape is placed by both of its ends instead. Connecting a pair of moving points, such as a linkage, a truss, or an edge between nodes, then takes one call and no trigonometry. It is a filled region, so it takes fill, an outline stroke, strokeAlign, and hollow, while drawLine is a stroke with no interior. A zero-length bar (a == b) or a non-positive thickness draws nothing.

A thick square-ended bar laid between two accent-dotted endpoints labeled a and b, its thickness marked straight across it, beside three such bars joining three dots into a truss
fill(.black)
drawOrientedBox(Vector2(100, 100), Vector2(400, 260), thickness: 40)   // bar between two points
drawOrientedBox(100, 100, 400, 260, thickness: 40)                     // same, scalar form

drawTriangle

drawTriangle(_ x: Double, _ y: Double, _ radius: Double)
drawTriangle(center: Vector2, radius: Double)
drawTriangle(_ x: Double, _ y: Double, _ base: Double, _ height: Double)
drawTriangle(apex: Vector2, base: Double, height: Double)
drawTriangle(_ a: Vector2, _ b: Vector2, _ c: Vector2)
drawTriangle(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double, _ x3: Double, _ y3: Double)

A triangle, in three forms. The three-argument form is an equilateral triangle centered at (x, y), point-up, with circumradius radius. That radius is the center-to-vertex distance, like drawCircle's radius, so rotating the triangle spins it about that center. The four-argument form is an isosceles triangle whose apex (tip) is at (x, y). It opens toward +y (downward) by height with the given base width, so rotating it sweeps it about the apex. The three-point form places the corners directly, so any triangle is one call. The corners may be in any winding order, and a zero-area triangle draws nothing. All three are analytic SDF shapes, crisp at any size and effectively free per triangle, and they honor strokeAlign and hollow. Aim them with the transform stack.

Three triangles in a row, each with its anchor dotted in orange: an equilateral centered on its dotted middle, an isosceles hanging from its dotted apex and opening downward, and a scalene with all three corners dotted
drawTriangle(width / 2, height / 2, 120)              // equilateral, centered, point-up
drawTriangle(width / 2, 100, 160, 240)               // isosceles, tip at (w/2, 100)
drawTriangle(Vector2(80, 360), Vector2(300, 280), Vector2(180, 120))  // any three corners
withState { translate(300, 300); rotate(time); drawTriangle(0, 0, 80, 200) }  // wedge spinning on its tip

drawArc

drawArc(_ x: Double, _ y: Double, _ radiusX: Double, _ radiusY: Double, start: Double, stop: Double, mode: ArcMode = .open)
drawArc(center: Vector2, radiusX: Double, radiusY: Double, start: Double, stop: Double, mode: ArcMode = .open)

An elliptical arc sweeping from start to stop. The angles are in radians, measured from the positive x-axis and increasing clockwise. mode decides how the ends close, which sets both the stroked outline and the filled region:

  • .open strokes the curve only. A fill paints the segment cut off by the chord, and the chord itself is not stroked.
  • .chord closes with a straight chord between the endpoints, so the stroke traces it and the fill is that segment.
  • .pie closes through the center like a pie slice, so the stroke traces both radii and the fill is the wedge.

The outline is stroked like any other path, so strokeJoin shapes its corners and strokeCap finishes an open arc's two ends.

The same three-quarter arc sweep under the three modes: open with only the curve stroked, chord closed by a straight edge, and pie closed through the center as a wedge. The first is annotated with the zero angle on the positive x-axis and an arrow showing angles increasing clockwise
drawArc(width / 2, height / 2, 160, 90, start: 0, stop: .pi)              // open half-arc
drawArc(width / 2, height / 2, 120, 120, start: 0, stop: .pi / 2, mode: .pie)

drawBezier

drawBezier(_ start: Vector2, _ control: Vector2, _ end: Vector2)
drawBezier(_ x1: Double, _ y1: Double, _ cx: Double, _ cy: Double, _ x2: Double, _ y2: Double)

A quadratic Bézier curve, stroked from start to end and bending toward the single control point control. It takes the current stroke paint, which may be solid, translucent, or gradient, and the current strokeWeight. A curve has no interior, so there is no fill. It honors strokeCap on its ends, butt by default. The curve flattens to a polyline and renders through the high-quality stroke path, which expands the edges into triangles and adds a ~1px anti-aliasing fringe. It stays crisp and even at any angle and resolution, down to sub-pixel widths.

One thick curve from a dotted start to a dotted end, bending up toward an orange control point above it, the two straight control legs ghosted behind the stroke, labeled leaned toward, never reached

For a cubic curve (two control points) or a chain of joined curves, sample the curve into a Shape contour and use drawShape. That call takes any number of points and can be filled.

stroke(.black); strokeWeight(4)
drawBezier(Vector2(100, 400), Vector2(width / 2, 80), Vector2(width - 100, 400))

More shapes

drawNgon

drawNgon(_ x: Double, _ y: Double, _ radius: Double, sides: Int)
drawNgon(center: Vector2, radius: Double, sides: Int)

A regular polygon centered at (x, y), with sides equal-length edges (3 or more) and one vertex pointing up. radius is the circumradius, the center-to-vertex distance, like drawCircle's radius. It is an analytic SDF shape, crisp at any size and effectively free per shape. Rotate it about its center with the transform stack. For an arbitrary, non-regular polygon, use drawPolygon.

A pentagon, hexagon, octagon, and twelve-sided polygon in a row, each with one vertex up and its center dotted, the first with an orange line from center to top vertex labeled radius
drawNgon(width / 2, height / 2, 120, sides: 6)            // a hexagon
withState { translate(300, 300); rotate(time); drawNgon(0, 0, 90, sides: 5) }  // a spinning pentagon

The polygons people name often have their own helpers: drawPentagon, drawHexagon, drawHeptagon, and drawOctagon. Each one is drawNgon with its side count fixed, the way drawCircle reads better than an equal-radii drawEllipse. They take the same (x, y, radius) and (center:, radius:) forms. For any other side count, use drawNgon.

drawHexagon(width / 2, height / 2, 120)                   // same as drawNgon(…, sides: 6)
drawOctagon(center: middle, radius: 90)

drawStar

drawStar(_ x: Double, _ y: Double, _ outerRadius: Double, _ innerRadius: Double, points: Int)
drawStar(center: Vector2, outerRadius: Double, innerRadius: Double, points: Int)

A star centered at (x, y) with points tips (3 or more) and one tip pointing up. The outline alternates between outerRadius at the tips and innerRadius at the valleys. innerRadius runs 0...outerRadius, and a smaller value is spikier. At the apothem the points flatten into a regular polygon's edges, which is exactly how drawNgon is built. It is an analytic SDF shape, crisp at any size and effectively free. Rotate it about its center with the transform stack.

Three stars with dotted centers: five points spiky at a small inner radius, the same five points gentle at a large one, and an eight-point burst
drawStar(width / 2, height / 2, 160, 70, points: 5)       // a classic five-point star
drawStar(width / 2, height / 2, 120, 96, points: 8)       // a gentler eight-point burst

drawRhombus

drawRhombus(_ x: Double, _ y: Double, _ width: Double, _ height: Double, cornerRadius: Double = 0)
drawRhombus(center: Vector2, width: Double, height: Double, cornerRadius: Double = 0)

A rhombus (diamond) centered at (x, y), width by height. Those two measurements are the full diagonals, and there is a vertex at each end of them. cornerRadius rounds the corners while keeping the footprint, so a large value rounds the diamond toward a circle. It is an analytic SDF shape, crisp at any size. Rotate it about its center with the transform stack.

Three diamonds: a tall one with its two diagonals drawn in orange as its width and height, a wide one with its center dotted, and one rounded toward a circle by a corner radius
drawRhombus(width / 2, height / 2, 160, 220)                 // a tall diamond
drawRhombus(width / 2, height / 2, 180, 180, cornerRadius: 40)  // soft, near-circular

drawVesica

drawVesica(_ x: Double, _ y: Double, _ width: Double, _ height: Double, cornerRadius: Double = 0)
drawVesica(center: Vector2, width: Double, height: Double, cornerRadius: Double = 0)

A vesica, meaning a pointed lens formed by the overlap of two circles, centered at (x, y) and width by height. The two tips lie along the longer axis, so a tall lens points up and down while a wide one points left and right. cornerRadius rounds the tips and eases the lens toward an ellipse. It also enlarges the lens slightly, as it does in drawMoon. It is an analytic SDF shape, so rotate it with the transform stack for in-between angles.

Three pointed lenses: a tall one with its two tips dotted top and bottom, a wide one with its tips dotted left and right, and the wide lens again with its tips softened by a corner radius
drawVesica(width / 2, height / 2, 120, 240)                 // a vertical lens (points up/down)
drawVesica(width / 2, height / 2, 240, 120, cornerRadius: 30)  // wide, with softened tips

drawOrientedVesica

drawOrientedVesica(_ a: Vector2, _ b: Vector2, width: Double)
drawOrientedVesica(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double, width: Double)

A vesica placed by its two tip points a and b, bulging to width across the middle. It is the oriented version of drawVesica, the way drawOrientedBox is the oriented version of drawRect. You place both tips rather than a center plus a rotation, so spanning a moving pair of points takes one call with no trigonometry. It is a filled region, so fill, an outline stroke, strokeAlign, and hollow all apply. width is the full waist width. Keep it below the tip distance for a lens, or equal to the tip distance for a circle. A zero-length span (a == b) or a non-positive width draws nothing.

Three lenses each spanning two marked tip points: a broad lens, a narrow sliver, and one whose waist equals the tip distance, closing into a circle
fill(.black)
drawOrientedVesica(Vector2(100, 200), Vector2(400, 260), width: 90)   // lens between two points
drawOrientedVesica(100, 200, 400, 260, width: 90)                     // same, scalar form

drawMoon

drawMoon(_ x: Double, _ y: Double, _ outerRadius: Double, _ innerRadius: Double, _ offset: Double, cornerRadius: Double = 0)
drawMoon(center: Vector2, outerRadius: Double, innerRadius: Double, offset: Double, cornerRadius: Double = 0)

A crescent moon at (x, y), drawn as the disk of outerRadius with a disk of innerRadius removed. The cut disk is shifted offset toward +x. For a classic crescent, keep innerRadius near outerRadius and the offset modest, because a larger offset opens the shape toward a half-moon. cornerRadius rounds the two cusps. It is an analytic SDF shape, so rotate it with the transform stack to face the crescent any direction.

Three crescents from the same marked center: a slim classic crescent, one opened toward a half-moon by a larger offset, and one with its two cusps rounded
drawMoon(width / 2, height / 2, 140, 130, 90)               // a fat crescent, opening right
withState { translate(width / 2, height / 2); rotate(time); drawMoon(0, 0, 120, 120, 80) }  // a slowly turning crescent

drawCross

drawCross(_ x: Double, _ y: Double, _ length: Double, _ thickness: Double, cornerRadius: Double = 0)
drawCross(center: Vector2, length: Double, thickness: Double, cornerRadius: Double = 0)

A plus-sign cross centered at (x, y), spanning length tip-to-tip on both axes, with arms thickness wide. cornerRadius rounds the outer corners for the usual rounded-plus look, and the inner notches stay sharp. It is an analytic SDF shape, so rotate it 45° with the transform stack for an ✕.

Three crosses: a sharp plus, the same plus with rounded outer corners, and the same shape rotated an eighth of a turn into an x
drawCross(width / 2, height / 2, 200, 70, cornerRadius: 16)  // a rounded plus
withState { translate(width / 2, height / 2); rotate(.pi / 4); drawCross(0, 0, 200, 60) }  // an ✕

drawRing

drawRing(_ x: Double, _ y: Double, _ innerRadius: Double, _ outerRadius: Double)
drawRing(center: Vector2, innerRadius: Double, outerRadius: Double)

A filled ring centered at (x, y), between innerRadius and outerRadius. It takes the current fill, not the stroke. For two outlined circles instead, call drawCircle twice with noFill. It is an analytic SDF shape, crisp at any size.

Three filled rings of one outer radius: a thin band, a thick band around a small hole, and a fine ring, each with its center marked
drawRing(width / 2, height / 2, 80, 120)                    // a fairly thin ring
drawRing(width / 2, height / 2, 20, 120)                    // a thick one (small hole)

drawTrapezoid

drawTrapezoid(_ x: Double, _ y: Double, _ topWidth: Double, _ bottomWidth: Double, _ height: Double)
drawTrapezoid(center: Vector2, topWidth: Double, bottomWidth: Double, height: Double)

An isosceles trapezoid centered at (x, y), topWidth across the top edge and bottomWidth across the bottom, height tall. Equal widths give a rectangle, and a zero width gives a triangle. It is an analytic SDF shape, crisp at any size. Rotate it about its center with the transform stack.

Three trapezoids: a narrow top over a wide base, equal widths making a rectangle, and a zero-width top making a triangle
drawTrapezoid(width / 2, height / 2, 120, 220, 160)         // narrow top, wide base
drawTrapezoid(width / 2, height / 2, 200, 200, 120)         // equal widths, a rectangle

drawParallelogram

drawParallelogram(_ x: Double, _ y: Double, _ width: Double, _ height: Double, _ skew: Double)
drawParallelogram(center: Vector2, width: Double, height: Double, skew: Double)

A parallelogram centered at (x, y), width wide and height tall. The top edge is sheared skew points along +x relative to the bottom. A skew of 0 is a rectangle, and a negative value leans the other way. It is an analytic SDF shape. Rotate it about its center with the transform stack.

Three parallelograms: leaning right under a positive skew, upright as a rectangle at zero, and leaning left under a negative skew
drawParallelogram(width / 2, height / 2, 240, 160, 80)      // a right-leaning slab
drawParallelogram(width / 2, height / 2, 240, 160, -80)     // leaning the other way

drawEgg

drawEgg(_ x: Double, _ y: Double, _ bottomRadius: Double, _ topRadius: Double)
drawEgg(center: Vector2, bottomRadius: Double, topRadius: Double)

An egg centered at (x, y) and pointing up. It is drawn as a circle of bottomRadius at the fat lower end, tapering to a rounded tip of topRadius at the top. bottomRadius must be at least topRadius, and equal radii give a circle. It is an analytic SDF shape. Rotate it about its center with the transform stack to tip it over.

Three eggs on their marked centers: the classic taper, a narrower sharper tip, and the equal-radii case, which is a circle
drawEgg(width / 2, height / 2, 120, 60)                     // a classic egg
withState { translate(width / 2, height / 2); rotate(time); drawEgg(0, 0, 120, 40) }  // tumbling

drawHeart

drawHeart(_ x: Double, _ y: Double, _ size: Double)
drawHeart(center: Vector2, size: Double)

A heart centered at (x, y), size points wide, with the lobes up and the point down. It is a little shorter than it is wide. It is an analytic SDF shape, so rotate it with the transform stack: 180° points it up, and 45° tips it like a playing-card suit.

Three hearts on their marked centers: lobes up and point down as drawn, pointed up by a half turn, and tipped like a playing-card suit by an eighth of a turn
drawHeart(width / 2, height / 2, 220)
withState { translate(width / 2, height / 2); rotate(sin(time) * 0.2); drawHeart(0, 0, 200) }  // a gentle wobble

drawCutDisk

drawCutDisk(_ x: Double, _ y: Double, _ radius: Double, _ cut: Double)
drawCutDisk(center: Vector2, radius: Double, cut: Double)

A disk of radius centered at (x, y) with a straight horizontal slice removed. The result is a dome with the flat edge down and the bulge up. cut is the signed offset of the flat edge from the center, in -radius...radius. At 0 you get a half disk. A positive cut raises the flat edge toward the dome and keeps a smaller cap, and a negative one keeps more than half. It is an analytic SDF shape, so rotate it with the transform stack to aim the flat edge.

Three domes cut from the same disk, on their marked centers: the half disk at cut zero, a smaller cap at a positive cut, and more than half the disk at a negative one
drawCutDisk(width / 2, height / 2, 140, 0)                  // a half disk
drawCutDisk(width / 2, height / 2, 140, -40)               // a bit more than half

drawUnevenCapsule

drawUnevenCapsule(_ a: Vector2, _ b: Vector2, _ ra: Double, _ rb: Double)
drawUnevenCapsule(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double, _ ra: Double, _ rb: Double)

A tapered capsule, meaning a round-capped bar with unequal end radii. It runs from a, with radius ra, to b, with radius rb, and it takes fill and stroke like any shape. The end-to-end distance must be at least |ra − rb|, or the smaller cap is swallowed. It is an analytic SDF shape, crisp at any size.

A capsule tapering from a fat round end to a small one between its two marked endpoints, beside the equal-radii case, a plain capsule
drawUnevenCapsule(Vector2(200, 200), Vector2(880, 880), 90, 24)   // a long taper
drawUnevenCapsule(300, 540, 780, 540, 70, 70)                     // equal radii, a plain capsule

Novelty shapes

These are less-common analytic forms, including curves, cut-outs, and a couple of pure doodles. They use the same instanced-SDF path as everything above, so they stay crisp at any size, cost effectively nothing, and rotate through the transform stack.

drawHorseshoe

drawHorseshoe(_ x: Double, _ y: Double, _ radius: Double, _ thickness: Double, gap: Double)
drawHorseshoe(center: Vector2, radius: Double, thickness: Double, gap: Double)

A horseshoe, meaning a thick arc with a gap, centered at (x, y). It is a band at mid-radius radius, thickness thick, with an opening that spans gap radians. gap is the full angular gap, so a smaller gap is more nearly a closed ring. Rotate it with the transform stack to aim the opening.

Three horseshoes on their marked centers, the gap widening from nearly a closed ring through the classic open shoe to a wide horseshoe barely past a half ring
drawHorseshoe(width / 2, height / 2, 160, 70, gap: 1.4)        // a classic open horseshoe
withState { translate(width / 2, height / 2); rotate(time); drawHorseshoe(0, 0, 150, 60, gap: 1.0) }  // a turning, nearly-closed ring

drawParabola

drawParabola(_ x: Double, _ y: Double, _ width: Double, _ height: Double)
drawParabola(center: Vector2, width: Double, height: Double)

A filled parabolic arch centered at (x, y), width across the flat base and height tall, with the curve peaking at the top. It is an exact parabola, with no tessellation. Rotate it with the transform stack.

Three filled arches on their marked centers: a rounded arch, a low wide one, and a narrow tall one, each peaking at the top over a flat base
drawParabola(width / 2, height / 2, 240, 240)                  // a rounded arch

drawRoundedX

drawRoundedX(_ x: Double, _ y: Double, _ length: Double, _ thickness: Double)
drawRoundedX(center: Vector2, length: Double, thickness: Double)

An X (saltire) centered at (x, y), length tip-to-tip along each axis, with round-capped arms thickness wide. It is drawCross turned 45°, with rounded ends. Rotate it with the transform stack.

The round-capped X three times: with chunky arms, with thin arms, and turned off-axis, each with its center marked
drawRoundedX(width / 2, height / 2, 240, 56)                   // a chunky rounded X

drawBlobbyCross

drawBlobbyCross(_ x: Double, _ y: Double, _ radius: Double, blobbiness: Double = 0.5)
drawBlobbyCross(center: Vector2, radius: Double, blobbiness: Double = 0.5)

A four-armed cross with concave, inward-curving sides, its tips reaching radius along each axis. blobbiness, in 0...1, sets how pinched the waist is, so a larger value is more bulbous and a smaller one is spikier. Rotate it with the transform stack, where 45° gives a diagonal four-point pinwheel.

Three four-armed crosses with concave sides: bulbous at blobbiness 0.8, the default 0.5, and spiky at 0.2, the first with the center-to-tip radius marked
drawBlobbyCross(width / 2, height / 2, 150)                    // the default waist
drawBlobbyCross(width / 2, height / 2, 150, blobbiness: 0.35)  // spikier arms

drawTunnel

drawTunnel(_ x: Double, _ y: Double, _ width: Double, _ height: Double)
drawTunnel(center: Vector2, width: Double, height: Double)

A tunnel or archway centered at (x, y), width wide and height tall overall. It has vertical walls and a flat base under a semicircular top. The arch radius is half the width, so height must be at least width / 2. Rotate it with the transform stack to aim the opening.

Three archways: a tall doorway, a squat tunnel whose height is exactly half its width, and the doorway turned on its side with the transform stack
drawTunnel(width / 2, height / 2, 200, 260)                    // a doorway

drawStairs

drawStairs(_ x: Double, _ y: Double, _ stepWidth: Double, _ stepHeight: Double, steps: Int)
drawStairs(center: Vector2, stepWidth: Double, stepHeight: Double, steps: Int)

A staircase centered at (x, y), with steps steps ascending to the right, each one stepWidth wide and stepHeight tall. The whole flight spans stepWidth · steps by stepHeight · steps. Rotate it with the transform stack.

Three staircases ascending to the right: four square steps, eight smaller ones, and four wide shallow treads, each centered on its marked anchor
drawStairs(width / 2, height / 2, 60, 60, steps: 4)            // a four-step flight

drawCoolS

drawCoolS(_ x: Double, _ y: Double, _ size: Double)
drawCoolS(center: Vector2, size: Double)

The hand-drawn "S" off the back of every school notebook, centered at (x, y) and size points tall, drawn as its filled silhouette. Add a stroke to trace its outline, or rotate it with the transform stack.

The pointed notebook S twice: as a solid ink silhouette, and washed pale with a stroke tracing its outline
drawCoolS(width / 2, height / 2, 360)                          // the doodle, filled

Paths & custom shapes

drawPolyline

drawPolyline(_ points: [Vector2], closed: Bool = false)

A connected path through points, stroked. It is open by default, and closed: true joins it back to its first point. Its corners follow strokeJoin, including a closed path's seam, and its open ends follow strokeCap.

The same seven dotted points twice: stroked as an open path with its two ends marked, and joined back to the first point with closed true
let wave = stride(from: 0.0, through: width, by: 8).map { x in
    Vector2(x, height / 2 + sin(x * 0.02 + time) * 60)
}
stroke(.black)
drawPolyline(wave)

drawPolygon

drawPolygon(_ points: [Vector2])

A filled convex polygon through points, plus a stroked outline. The fan fill works only on convex shapes, so use drawShape for concave outlines or holes.

Five orange dots joined by a faint outline on the left, and on the right the filled, outlined pentagon drawPolygon draws through the same five points
let triangle = [Vector2(200, 120), Vector2(280, 280), Vector2(120, 280)]
fill(.black)
drawPolygon(triangle)

drawShape

drawShape(_ shape: Shape)

A vector Shape, which is a filled region that may be concave and may have holes, plus a stroked outline of each contour. The fill is triangulated with even-odd winding, so nested contours become holes. Open contours are stroked but not filled.

A filled square frame whose nested inner square has emptied into a hole, its corners dotted, beside a closed curved outline traced through four dotted points and filled
// A square with a square hole, a frame.
let outer = [Vector2(60, 60), Vector2(260, 60), Vector2(260, 260), Vector2(60, 260)]
let hole  = [Vector2(120, 120), Vector2(200, 120), Vector2(200, 200), Vector2(120, 200)]
fill(.black)
drawShape(Shape(outer: outer, holes: [hole]))

There is also a closure form that builds a curved or straight outline inline with a Path. Trace it with the pen methods move, line, curve, quadCurve, cubicCurve, and close. It then strokes like any Shape, and it fills too if you close it:

drawShape { p in
    p.move(to: Vector2(200, 600))
    p.curve(to: Vector2(400, 480))     // smooth through the points
    p.curve(to: Vector2(600, 560))
    p.quadCurve(to: Vector2(820, 360), control: Vector2(720, 620))
    p.close()
}

A shape you keep draws faster the next time. Most of what drawShape costs is cutting the fill into triangles and building the stroke's outline. A Shape you hold on to and draw again unchanged remembers both from its second draw on. Later frames only copy them, in whatever fill and stroke color the frame asks for. Two hundred traced outlines, kept on screen and recolored every frame, took about 6 ms a frame on an M2 in a release build. Worked out afresh each time, they took 124 ms.

  • The memory goes with the value. A copy of the shape shares it, and an edit to its points or its winding starts over.
  • A stroke is remembered at one width, join, cap, and scale. Drawn at another, it is worked out again.
  • A stroke with a dash, a brush, a width profile, a fading opacity, or a gradient is worked out every time.
  • The closure form builds a new shape on every call, and so does a Shape made inside draw(). Keep the shape in a property to get the saving.
  • A kept shape holds its triangles and stroke as long as it lives, several hundred bytes for each point of a filled and stroked outline.

drawCurve

drawCurve(_ points: [Vector2], closed: Bool = false, spline: Spline = .catmullRom)

A smooth curve through points. By default it is a Catmull-Rom spline that passes through each point, with tangents derived from its neighbors. You draw a "wiggle" straight from a list of points, with no control points to place. closed: false, the default, is an open stroked line, and closed: true makes a closed, fillable loop. It is sugar over Shape(curveThrough:closed:spline:).

spline: .hobby fits the whole run at once, so the bend flows evenly from point to point. The default flattens between far-apart points and swells between close ones. .hobby(tension:curl:) tunes the fit. The typed form, with the Béziers it chose, is HobbySpline.

One set of six dotted points drawn twice: open, a smooth stroked line threading through every dot, and closed, the same spline looped back into a filled ring
let pts = (0...8).map { i in
    Vector2(120 + Double(i) * 100, height / 2 + sin(time + Double(i)) * 80)
}
stroke(.black)
strokeWeight(3)
drawCurve(pts)            // a smooth wave that animates with `time`

Batches

drawCircles(_ circles: [Circle])
drawCircles(_ centers: [Vector2], radius: Double)
drawPoints(_ points: [Vector2])
drawPoints(_ points: [Vector2], size: Double)
drawRects(_ rectangles: [Rectangle], cornerRadius: Double = 0)
drawRects(_ rectangles: [Rectangle], cornerRadii: CornerRadii)

These are collection-call sugar, where one Swift call draws a whole array. The current fill, stroke, and transform apply to every shape in the array. Use them when the shapes share a style, as a point cloud, a scatter, or a grid does. Each shape is still its own instanced quad, so a batch costs the same as the loop it replaces. The call site gets shorter, not the GPU work.

let seeds = (0..<800).map { i -> Circle in
    let a = Double(i) * 2.39996                 // the golden angle
    let r = 8 * Double(i).squareRoot()
    return Circle(x: cos(a) * r, y: sin(a) * r, radius: 4)
}
fill(.white)
noStroke()
withState {
    translate(width / 2, height / 2)
    drawCircles(seeds)                          // a sunflower, in one call
}

To vary the style per shape, giving each its own color or radius, use the single-shape call in a loop, as in for c in circles { fill(...); drawCircle(c) }. The batch forms are for when one style covers the whole array.

Transforms and state

Transforms move, turn, and stretch the coordinate system, not the shapes you have already drawn. Every draw call after one is measured in the new frame. They stack, so each builds on the previous one, and they reset every frame, so draw() always starts from the top-left origin. Wrap them in withState { } to keep a transform local. Order matters. translate then rotate moves out and spins in place, like a top, while rotate then translate moves along the already-tilted axes, like an orbit.

Four panels drawing the same flag with the same call: untransformed at the origin, then translated, then rotated a twelfth of a turn, then scaled up

translate

translate(_ x: Double, _ y: Double)
translate(_ offset: Vector2)

Shift the origin. Part of the per-frame transform stack.

translate(width / 2, height / 2)        // origin now at the center
drawCircle(0, 0, 60)

rotate

rotate(_ radians: Double)

Rotate the coordinate system. The turn is clockwise, because y points down.

translate(width / 2, height / 2)
rotate(time)                            // spin over time
drawRect(center: .zero, width: 160, height: 40)

scale

scale(_ amount: Double)
scale(_ x: Double, _ y: Double)

Scale the coordinate system, uniformly or per axis. This is the transform, which is not the read-only scale property in Canvas.

translate(width / 2, height / 2)
scale(1.5)                              // everything after is 1.5×
drawCircle(0, 0, 80)

symmetry / noSymmetry

symmetry(_ folds: Int, mirrored: Bool = false)
noSymmetry()

Replicate everything drawn next into folds copies, rotated evenly around the current origin. This is the kaleidoscope, or mandala, mode. Draw one wedge and the folds complete the picture. mirrored: true adds a reflected copy per fold, mirrored across the local x-axis, which is the classic kaleidoscope's doubled symmetry.

Three panels: a single small crooked wedge with a red dot at its tip, the same wedge under eightfold symmetry forming a snowflake, and under mirrored eightfold symmetry forming a denser one with paired reflections

The fold pivot and the mirror axis are the origin and x-axis as they stand at the call. Call translate first to place the center, and rotate to aim the seam. Transforms applied after symmetry compose inside every fold, so an orbiting shape orbits in all of them at once. This is drawing state like fill, so it stays on until noSymmetry(), and withState { } restores it.

translate(width / 2, height / 2)   // fold around the canvas center
symmetry(8, mirrored: true)
drawCircle(240, 40, 30)            // appears 16 times
noSymmetry()
drawCircle(0, 0, 26)               // the center medallion, drawn once

Replication covers all 2D drawing: shapes, strokes, images, text, and SDF fields. It also carries into SVG export, while 3D geometry and GPU particles are left untouched. Each copy is real geometry, so folds multiplies the drawing cost as well, exactly as the equivalent loop would. See the Patterns/Kaleidoscope example.

withClip

withClip(_ shape: Shape, _ body: () -> Void)
withClip(_ rect: Rectangle, _ body: () -> Void)
withClip(_ circle: Circle, _ body: () -> Void)

Run body with drawing confined to the region. It is a stencil mask, so everything drawn inside the block lands only where the region covers. That includes fills, strokes, images, text, and even 3D geometry. The previous clip is restored when the block ends. Any vector Shape works, holes and concavity included, and its winding rule is honored. Open contours do not fill, so a shape with no fillable region clips everything out. Nested clips intersect, so a withClip block inside another draws only where both regions overlap.

Three panels of the same diagonal orange stripes: confined to a star, confined to a circle, and confined to both at once so only the overlap of star and circle is striped

The current transform fixes the region where it stands at the call, like a drawn fill. Transforms inside the block therefore move the drawing, not the clip. As with withState and layer { }, any drawing state the block changes is restored on exit. The clip edge anti-aliases at MSAA resolution, and the region carries into SVG export as a native <clipPath>.

translate(width / 2, height / 2)
withClip(Circle(x: 0, y: 0, radius: 300)) {
    for i in 0 ..< 24 {                     // stripes exist only in the disk
        fill(i % 2 == 0 ? .white : .black)
        drawRect(Double(i) * 30 - 360, -360, 30, 720)
    }
}
drawCircle(0, 0, 12)                        // after the block: unclipped

Clipping is scoped to the current drawing surface, so a layer { } opened inside a clip block starts unclipped. Clip inside the layer's own block instead. To mask a whole layer with soft edges or an image matte, use masked(by:) in the effects tier. withClip is the cheaper, geometric tool for keeping mid-frame drawing inside a region. See the Shapes/Clipping example.

withViewBox

withViewBox(_ rect: Rectangle, fit: ImageFit = .contain, _ body: () -> Void)

Run body inside rect as if that rectangle were the whole canvas. Drawing is clipped to it, and the coordinates are remapped, so 0...width and 0...height land on the box. That is how one window shows several versions of a sketch at once.

for (i, cell) in grid(columns: 3, rows: 2, padding: 40, gutter: 24).cells.enumerated() {
    withViewBox(cell.frame) {
        randomSeed(i)
        drawTheSketch()              // written as though it owned the window
    }
}

The block needs no changes to run in a box, and two things are remapped to keep that true:

  • background(_:) fills this box's own canvas rather than setting the frame's clear color. The clear color belongs to the whole frame and to every other box on it. A box that wiped it would take the others with it.
  • The mouse arrives in the box's coordinates, so an interactive sketch works in each box independently. A pointer outside the box reads proportionally outside 0...width, the way Rectangle.point(u:v:) does.

Both are restored on exit, and boxes nest. A box inside a box composes both mappings, and the clips intersect.

fit says what happens when the box is not the canvas's shape. It means the same thing here as it does for a picture (see ImageFit). .contain puts the whole virtual canvas inside and leaves the box showing along two edges. .cover fills the box and crops what runs past. .stretch squashes to fit. A box of the canvas's own shape gets the same answer from all three.

width and height do not change, on purpose. The code inside believes it has the whole canvas, which is what lets you hand an unmodified sketch to a box. One thing follows from that, and it is worth knowing before you lay out a sheet. The virtual canvas has the sketch's shape, not the box's. Boxes shaped like the canvas are therefore the case where nothing is letterboxed. Draw labels and frames outside the block, in canvas coordinates, so they keep one size while the drawing in each box is scaled down. See the Rendering/ViewBoxes example.

viewControl

viewControl(center: Vector2? = nil, zoom: Double = 1, in range: ClosedRange<Double> = 0.05...50)

Hand the view of the canvas to whoever is watching, so they can drag to pan and scroll or pinch to zoom. Call it once each draw(), before the drawing it should move. It is the 2D counterpart of cameraControl(), and it is opt-in the same way. A sketch that never calls it never pays for it.

override func draw() {
    background(.white)
    viewControl()
    drawTheWholeSketch()          // now pannable and zoomable
}

It leaves a plain transform in force, so everything drawn after it moves together and anything drawn before it stays put. That is where a fixed backdrop belongs. For a HUD that has to be drawn last, wrap the call and the drawing in a withState { } block. Then draw the HUD after it.

center and zoom frame the opening view, and after the first call they are the view's home. Passing the same values every frame changes nothing, so a constant framing never fights the dragging. A value that changes, a @Param turned in the inspector or an edit on the live stage, glides the view there while the viewer has not dragged or scrolled it yet, which is how a parameter tunes the view live. Once they have, the view is theirs: a changed value moves only the home, and resetView() is what opens on the new one. zoom is how many screen pixels one canvas unit covers, and range bounds where the wheel or a pinch can take it. viewCenter and viewZoom report where the view is. resetView() puts the view back on its home framing, which is what a reset key would call.

GestureAction
DragPan, and the content follows the pointer exactly
ScrollZoom about the pointer, so what you point at stays under it
PinchZoom about the pointer by the pinch itself: fingers spread to twice the distance, twice the zoom

Neither gesture is damped, unlike the 3D camera. An orbit gains from a little inertia, and a flat plane under a finger does not.

The mouse is remapped into the coordinates now on screen for the rest of the frame. That means drawCircle(mouseX, mouseY, 20) lands under the pointer at any zoom, and hit-testing keeps working. The pointer itself is restored before the next frame, so the remap never compounds. The remap lasts the frame rather than the block, so a withState { } around the call does not put it back.

Zooming costs nothing in fidelity, because the drawing is vector. Text set at four units is a smudge at the opening view and crisp four notches in, and nothing is re-rendered to get there. See the Input/PanAndZoom example.

withState

withState(_ body: () -> Void)
withState(at position: Vector2, rotation: Double = 0, scale: Double = 1, _ body: () -> Void)
withState(at position: Vector3, _ body: () -> Void)

Run body with the current transform and style saved, then restored. This is the scoped form of pushState/popState, and the one to use. The changes stay inside the braces, like scribbling on a sheet laid over your drawing and then lifting it off.

withState {
    translate(width / 2, height / 2)
    rotate(time)
    fill(.red)
    drawRect(center: .zero, width: 160, height: 40)
}
// transform and fill are back to what they were

The at: form builds the placement in, so the common block of "move there, turn, draw" collapses to its one interesting line. Inside it the origin sits at position, so draw around .zero:

withState(at: p, rotation: a) {
    drawRect(center: .zero, width: 40, height: 8)
}

The moves apply in the fixed order translate, rotate, scale. For any other order, write the block out. The Vector3 form is the same idea for placing a mesh in space.

pushState / popState

pushState()
popState()

Manually save and restore the transform and style. Prefer withState { } unless you need the calls separated.

pushState()
translate(100, 100)
drawCircle(0, 0, 40)
popState()

↑↓ move ↩ open