Skip to content

create.render

Drawing: Canvas and its with-block guards, Style, Camera, and the settings that shape a frame (Antialiasing, Autoscale, Align, StrokeCap, StrokeDash, StrokeAlign, RenderBackend).

RenderBackend

struct RenderBackend

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • CPU = RenderBackend(Int(0))
  • GPU = RenderBackend(Int(1))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

from_arguments

@staticmethod
def from_arguments() -> Self

The backend named on the command line: GPU when the program was run with a gpu argument, CPU otherwise or with cpu, the last named winning. So one build runs on either, ./sketch gpu or pixi run example shadows gpu, passed as run(..., backend=RenderBackend.from_arguments()); other arguments are left for the program.

Antialiasing

struct Antialiasing

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • OFF = Antialiasing(Int(0))
  • LOW = Antialiasing(Int(1))
  • MEDIUM = Antialiasing(Int(2))
  • HIGH = Antialiasing(Int(3))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

Align

struct Align

Which point of a box a position names — one value for both axes.

The nine constants are the nine points of the box, so a single argument says everything canvas.text_alignment needs:

# # # # # # # # # # # # # # # # # # # # # #
# TOP_LEFT ———————— TOP ——————— TOP_RIGHT #
# |                  |                  | #
# |                  |                  | #
# LEFT —————————— CENTER —————————— RIGHT #
# |                  |                  | #
# |                  |                  | #
# BOTTOM_LEFT ——— BOTTOM ——— BOTTOM_RIGHT #
# # # # # # # # # # # # # # # # # # # # # #

The one-word constants name an edge's midpoint: TOP is top-centre, LEFT is middle-left, CENTER is the middle of the box.

Edges of the box, not typographic baselines — there is no separate text_baseline. TOP and BOTTOM mean the visual top and bottom even though y grows upward, because glyphs are not flipped by the coordinate system; only their anchor point is mapped.

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • TOP = Align(Int(0))
  • CENTER = Align(Int(1))
  • BOTTOM = Align(Int(2))
  • LEFT = Align(Int(3))
  • RIGHT = Align(Int(4))
  • TOP_LEFT = Align(Int(5))
  • BOTTOM_LEFT = Align(Int(6))
  • TOP_RIGHT = Align(Int(7))
  • BOTTOM_RIGHT = Align(Int(8))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

StrokeAlign

struct StrokeAlign

Where a region's stroke sits against the shape's edge.

Alignment Stroke
INSIDE Within the shape, its outer edge on the shape's (the default)
CENTER Centred on the edge, half its thickness either side
OUTSIDE Around the shape, its inner edge on the shape's

The fill always meets the stroke's inner edge, so OUTSIDE leaves the fill the whole shape and INSIDE erodes it by the thickness. The default keeps what is drawn within what contains, overlaps and clip see. Only regions have an inside: a rectangle, circle, triangle, sector or polygon. Lines and curves are always stroked centred, and a self-crossing polygon's seams (edges running through its inside) stay centred too.

Set with canvas.stroke_alignment(alignment), Style(stroke_alignment=) or canvas.style(stroke_alignment=); like all style it resets each frame. A type of its own, so a bare number never passes for one.

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • INSIDE = StrokeAlign(Int(0))
  • CENTER = StrokeAlign(Int(1))
  • OUTSIDE = StrokeAlign(Int(2))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

StrokeCap

struct StrokeCap

How the two open ends of a stroked line or curve are finished.

Cap End
BUTT Square, flush with the end point (the default)
ROUND A half disc, its diameter the stroke thickness
SQUARE Square, pushed out past the end point by half the thickness

Only open ends have caps: a Line, a Bezier, an Arc short of a whole turn, and an open Spline. A closed curve and the stroke of a region (rectangle, circle, triangle, sector, polygon) have none, unless the stroke is dashed (StrokeDash): then each dash has two. A line of no length draws a dot under ROUND, a square lined up with the local axes under SQUARE, and nothing under BUTT.

Set with canvas.stroke_cap(cap), Style(stroke_cap=) or canvas.style(stroke_cap=); like all style it resets each frame. A type of its own, so a bare number never passes for one.

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • BUTT = StrokeCap(Int(0))
  • ROUND = StrokeCap(Int(1))
  • SQUARE = StrokeCap(Int(2))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

StrokeDash

struct StrokeDash

How a stroke is broken into dashes along its length.

Value Stroke
SOLID Unbroken (the default)
DASHED Dashes 3 thicknesses long, 2 apart
DOTTED Round dots as wide as the stroke, 2 thicknesses apart
StrokeDash(on, off) Dashes on long, off apart
StrokeDash(on, off, on, off, ...) Up to four on/off pairs, repeated

A preset is a look; a constructor is exact control. The presets scale with the stroke's thickness, so they suit any stroke as they are. A constructor's lengths are world units, scaled by autoscale, the camera and the transform as the thickness is, and keep their spacing whatever the thickness. A dot is a dash of no length: DOTTED is always round, while StrokeDash(0, 8) takes stroke_cap, so it draws dots under ROUND, squares under SQUARE and nothing under BUTT.

canvas.stroke_dash(StrokeDash.DASHED)
with canvas.style(stroke_dash=StrokeDash(12, 4, 2, 4)):  # dash-dot
    canvas.circle((0, 0), 80)

Where it starts. A line starts at its start, a curve at its first point, a polygon or triangle at its first vertex, all in their own order. A rectangle starts at its top-left corner and a circle or ellipse at its right-most point, both running counter-clockwise. A sector starts at its tip, out along its start radius. offset starts the pattern that far into itself, so raising it every frame marches the dashes backwards along the stroke (with_offset). A closed outline doesn't stretch the pattern to fit: where it comes back round, a dash can meet a shorter one.

Every dash is a stroke of its own: stroke_cap finishes both its ends, a region's dashes included, and a dash round a corner is mitred as a solid stroke is. Dashes whose caps reach into each other overlap there, which a translucent stroke shows. Lengths are measured along the middle of the stroke, after stroke_alignment has placed it. The fill still meets the stroke's inner edge, so a gap shows what is behind the shape; StrokeAlign.OUTSIDE puts the fill under the gaps instead.

Set with canvas.stroke_dash(dash), Style(stroke_dash=) or canvas.style(stroke_dash=); like all style it resets each frame.

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • SOLID = StrokeDash(_kind=Int(0), _offset=Float64("0"))
  • DASHED = StrokeDash(_kind=Int(1), _offset=Float64("0"))
  • DOTTED = StrokeDash(_kind=Int(2), _offset=Float64("0"))

Constructors

@staticmethod
def __init__(out self, on: Float64, off: Float64, *, offset: Float64 = Float64("0"))

Dashes on world units long, off apart, starting offset into the pattern. A negative length counts as none.

@staticmethod
def __init__(out self, *lengths: Float64, *, offset: Float64 = Float64("0")) raises

The pattern on, off, on, off, ..., in world units, repeated along the stroke and starting offset into it. A negative length counts as none.

Raises unless lengths is whole on/off pairs, at most four of them.

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

with_offset

def with_offset(self, offset: Float64) -> Self

This pattern starting offset world units into itself, for marching dashes: dash.with_offset(context.time.elapsed * 20).

offset

def offset(self) -> Float64

How far into the pattern the stroke starts, in world units.

Autoscale

struct Autoscale

How the program's design size maps onto the window.

The design size is the coordinate space a program is authored in -- the size passed to run, or pinned by context.design_size. It is a property of the program, not of the display, and a resize or a fullscreen toggle never changes it. What changes is how it lands on the window:

  • FIT scales uniformly by min(w, h), centres the design, and paints the leftover with canvas.letterbox_color after the frame is rendered -- which also clips anything rendered past the design bounds. canvas.width()/height() never move, so a layout written against them survives any window size.
  • EXTEND uses the same scale factor but anchors at the origin and paints no bars, so the leftover becomes extra world: canvas.width()/height() grow with the window. Layout must anchor to the origin or to canvas.left()/right()/bottom()/top(), since design coordinates no longer describe the edges.
  • OFF does not scale at all -- canvas.width()/height() are window pixels and the design size goes unused, so layout has to survive any window size on its own.

run starts programs in FIT, because OFF punishes the obvious way to write a program: coordinates laid out against the size the author had, silently rearranged on any other display. create can pass either other mode to context.autoscale.

FIT pins canvas.width()/height() to the design size, so a rectangle at a fixed x is always inside the world by the same margin. EXTEND and OFF let a resize move right()/left()/top()/bottom() themselves, which can carry them past an entity between frames — code that reacts to a boundary under either mode must set an entity's state (position, sign of velocity) from the boundary, not toggle it, or a window shrunk past the entity toggles it every frame forever instead of correcting it.

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • OFF = Autoscale(Int(0))
  • FIT = Autoscale(Int(1))
  • EXTEND = Autoscale(Int(2))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

Style

struct Style

How the next shape or glyph is painted, independent of where it goes.

The canvas holds one, set piece by piece through canvas.fill, canvas.stroke and the other style setters, read by every render call until changed and rebuilt fresh each frame. A program can also build its own and apply it whole with canvas.style(s), to reuse one look across render calls, frames or entities:

var label = Style(stroke_enabled=False, text_color=Color.WHITE, font_size=24)
with canvas.style(label):
    canvas.text("Score", (0, canvas.top() - 20))

A shadow is part of the style too, so a card's look can carry its own:

var card = Style(
    fill=Color.WHITE,
    stroke_enabled=False,
    shadow=Color(0, 0, 0, 80),
    shadow_blur=16,
)

corner_radius rounds the shapes the canvas builds from numbers -- canvas.rectangle(p, w, h), triangle, polygon and sector -- unless the call names its own corner_radius. A shape value carries its own radius, so canvas.draw(shape) and canvas.clip(shape) ignore the style's: what is drawn from a shape is what its contains tests.

The constructor's keywords are named after the canvas setters and act like them: naming any part of the fill, stroke or shadow switches it on (a part, not a modifier such as stroke_alignment, stroke_cap, stroke_dash or corner_radius), unless fill_enabled, stroke_enabled or shadow_enabled says otherwise, so Style(shadow=Color.RED, shadow_enabled=False) keeps a shadow ready but off. fill_gradient is the keyword for fill's Gradient overload, since one keyword can't take both: given beside fill, the gradient paints and the colour is kept for when it is cleared. Every part left unset is what a fresh frame starts with.

Implements Copyable, Deinitable, ImplicitlyCopyable, Movable, Writable.

Fields

  • fill_color: Color
  • fill_gradient: Optional[Gradient]: Paints the fill instead of fill_color while set; fill_color is kept for when it is cleared.
  • fill_enabled: Bool
  • stroke_color: Color
  • stroke_thickness: Float64
  • stroke_enabled: Bool
  • stroke_alignment: StrokeAlign: Where a region's stroke sits against its edge; INSIDE unless set. Lines and curves are always stroked centred.
  • stroke_cap: StrokeCap: How the open ends of lines and curves are finished; BUTT unless set. Regions' strokes and closed curves have no ends, unless dashed.
  • stroke_dash: StrokeDash: How the stroke is broken into dashes; SOLID unless set.
  • corner_radius: Float64: How far the shapes the canvas builds from numbers are rounded, in world units; 0 is sharp. A shape value keeps its own.
  • text_color: Color
  • font: Optional[Font]: The face text is drawn in; none for the packaged Noto Sans.
  • font_size: Float64
  • font_weight: FontWeight
  • font_italic: Bool
  • text_alignment: Align
  • tint: Color
  • opacity: Float64
  • blend_mode: BlendMode
  • shadow_color: Color
  • shadow_offset: Vector2D
  • shadow_blur: Float64
  • shadow_spread: Float64
  • shadow_inset: Bool
  • shadow_follows_transform: Bool
  • shadow_enabled: Bool

Constructors

@staticmethod
def __init__(
    out self,
    *,
    fill: Optional[Color] = None,
    fill_gradient: Optional[Gradient] = None,
    fill_enabled: Optional[Bool] = None,
    stroke: Optional[Color] = None,
    stroke_thickness: _OptionalFloat64 = None,
    stroke_enabled: Optional[Bool] = None,
    stroke_alignment: StrokeAlign = StrokeAlign.INSIDE,
    stroke_cap: StrokeCap = StrokeCap.BUTT,
    stroke_dash: StrokeDash = StrokeDash.SOLID,
    corner_radius: _OptionalFloat64 = None,
    text_color: Color = Color.BLACK,
    font: Optional[Font] = None,
    font_size: _OptionalFloat64 = None,
    font_weight: FontWeight = FontWeight.REGULAR,
    font_italic: Bool = False,
    text_alignment: Align = Align.CENTER,
    tint: Color = Color.WHITE,
    opacity: _OptionalFloat64 = None,
    blend_mode: BlendMode = BlendMode.NORMAL,
    shadow: Optional[Color] = None,
    shadow_offset: Optional[Vector2D] = None,
    shadow_blur: _OptionalFloat64 = None,
    shadow_spread: _OptionalFloat64 = None,
    shadow_inset: Optional[Bool] = None,
    shadow_follows_transform: Bool = False,
    shadow_enabled: Optional[Bool] = None,
)

Camera

struct Camera

What part of world space maps onto the screen.

position is the world point centred on screen; zoom scales around it (1.0 = no scaling). A field the program owns and moves on its own schedule — like Image or Tween — not something the run loop writes. canvas.camera(camera) applies it to every render call from that point on; the default identity camera behaves exactly like no camera at all.

Implements Copyable, Deinitable, ImplicitlyCopyable, Movable, Writable.

Fields

  • position: Point2D
  • zoom: Float64

Constructors

@staticmethod
def __init__(out self, position: Point2D = Point2D(SIMD(0), SIMD(0)), zoom: Float64 = 1)

matrix

def matrix(self) -> Matrix[Int(3), Int(3)]

World-to-screen mapping: scale around position, then recentre.

to_world

def to_world(self, screen: Point2D) -> Point2D

A screen-space point — context.mouse.position, say — into world space.

The inverse of matrix(), done in arithmetic rather than through a general matrix inverse.

to_screen

def to_screen(self, world: Point2D) -> Point2D

A world-space point into screen space.

Canvas

struct Canvas

One frame: the geometry and the rendering API.

This is the object a program is handed to render a frame with. width/height are the screen extent and left/right/bottom/top its edges — use those rather than width arithmetic, since the origin is centred and two of them are negative. scale is the autoscale factor. Screen space is camera-independent: these don't know a Camera exists, since a program sets one on the canvas's transform, not on the geometry it reports.

A mut parameter because the program renders on it, and the recording it appends to is the frame's whole output. What it does not carry is a setting, the clock or input: everything that outlives a frame is in Context, handed to update beside it. A Canvas that carried a dial would be offering to change something it is not around to see the effect of.

The canvas the loop hands to create and update is built fresh each frame and dropped before the frame is presented. The machinery that must survive the frame goes in and out through _PersistentCanvasState.

Canvas(width, height) builds an offscreen canvas instead: a program's own, drawn into with the same calls and read back with color_at and snapshot, which may be kept as a field across frames. It starts transparent, and has no frames to present: each read replays what has been drawn since the last one onto its own pixels, so drawing after a read lands on what is already there.

It takes no parameters, and holds no Surface. It used to need one origin parameter for the framebuffer it borrowed, which constrained the whole API: a second parameter would have broken every Program.update signature at once, and pointing an existing Canvas at a new framebuffer could not compile at all. Both limits are gone because a Canvas no longer touches pixels — it records, and the backend replays onto a Surface the canvas never sees. Don't reintroduce a Surface field or a parameter to hold one.

Its extent comes from the Viewport rather than from a framebuffer. The two can disagree for one frame after a resize, which is harmless here: the extent is only used for coordinate arithmetic, and the replay clips against the real surface it is handed.

A render call touches no pixels: it appends a RenderCommand to the backend's recording, and the backend replays the whole frame afterwards. So a Canvas is a recorder, and the geometry it records is local — the shape as the program asked for it, paired with the current transform — never device pixels. A style is resolved at record time, so a later fill() cannot reach back and change what an earlier command paints.

Every pixel write blends source-over, so a fill, stroke, image, glyph or background with a < 255 composites with what is already there — unless blend_mode picks another way to combine them.

Implements Deinitable, DrawTarget, Movable.

Constructors

@staticmethod
def __init__(
    out self,
    width: Float64,
    height: Float64,
    *,
    scale: Float64 = 1,
    antialiasing: Antialiasing = Antialiasing.MEDIUM,
) raises

An offscreen canvas of width by height screen units, with scale pixels to the unit — scale=canvas.scale() matches the window's density.

Screen space as on the frame canvas: origin centred, y up. Always rasterised on the CPU, whatever backend the program runs on; what comes out of it is an Image, which any canvas can draw.

@staticmethod
def __init__(
    out self,
    width: Int,
    height: Int,
    *,
    scale: Float64 = 1,
    antialiasing: Antialiasing = Antialiasing.MEDIUM,
) raises

width

def width(self) -> Float64

The width in screen units, right() - left(): the design width under Autoscale.FIT, more under EXTEND as the window grows — by a fraction of a unit too, as the window's pixels divide out — the window's pixels under OFF. Rebuilt every frame.

height

def height(self) -> Float64

The height in screen units, top() - bottom(); see width.

bounds

def bounds(self) -> Rectangle

The screen as a rectangle in screen space, left() to right() and bottom() to top(): centred on the origin, width() by height(). What snapshot and clip take, and what contains asks whether something is on the screen.

scale

def scale(self) -> Float64

Window pixels per screen unit: what font size, stroke thickness and image size are scaled by.

left

def left(self) -> Float64

Screen x of the left edge — negative, since the origin is centred.

right

def right(self) -> Float64

bottom

def bottom(self) -> Float64

Screen y of the bottom edge — negative, since y grows upward.

top

def top(self) -> Float64

transform

def transform(mut self, m: Matrix[Int(3), Int(3)]) -> TransformGuard[origin_of(self)]

Apply m to everything rendered inside a with block.

with canvas.transform(translate(50.0, 50.0)):
    canvas.rectangle((0, 0), 100, 100)

The matrix pops on exit, including on an early return or a raise. _push_transform is the same push without that guarantee -- a missed pop shifts every later render in the frame, so go through here.

style

def style(
    mut self,
    *,
    fill: Optional[Color] = None,
    fill_gradient: Optional[Gradient] = None,
    fill_enabled: Optional[Bool] = None,
    stroke: Optional[Color] = None,
    stroke_thickness: _OptionalFloat64 = None,
    stroke_enabled: Optional[Bool] = None,
    stroke_alignment: Optional[StrokeAlign] = None,
    stroke_cap: Optional[StrokeCap] = None,
    stroke_dash: Optional[StrokeDash] = None,
    corner_radius: _OptionalFloat64 = None,
    text_color: Optional[Color] = None,
    font: Optional[Font] = None,
    font_size: _OptionalFloat64 = None,
    font_weight: Optional[FontWeight] = None,
    font_italic: Optional[Bool] = None,
    text_alignment: Optional[Align] = None,
    tint: Optional[Color] = None,
    opacity: _OptionalFloat64 = None,
    blend_mode: Optional[BlendMode] = None,
    shadow: Optional[Color] = None,
    shadow_offset: Optional[Vector2D] = None,
    shadow_blur: _OptionalFloat64 = None,
    shadow_spread: _OptionalFloat64 = None,
    shadow_inset: Optional[Bool] = None,
    shadow_follows_transform: Optional[Bool] = None,
    shadow_enabled: Optional[Bool] = None,
) -> StyleGuard[origin_of(self)]

Scope style changes to a with block, restoring the previous style on exit.

Each keyword applied goes through the setter of the same name, so it behaves exactly like calling that setter; the rest of the style is kept. fill_gradient goes through fill's Gradient overload; given beside fill, the gradient paints. With no keywords it only scopes: for helpers that set style before rendering, where without it a callee's stroke_enabled(False) silently applies to whatever the caller renders next.

with canvas.style(fill=Color.RED, stroke_enabled=False):
    canvas.circle(position, 10)
def style(mut self, style: Style) -> StyleGuard[origin_of(self)]

Render with style for a with block, then restore the previous one.

Replaces the whole style: a field style doesn't set is at its Style() default inside the block, not whatever was set before. Applied as soon as this is called, like transform(m), so outside a with it holds for the rest of the frame.

with canvas.style(self.label_style):
    canvas.text("Score", (0, canvas.top() - 20))

to_screen

def to_screen(self, local: Point2D) -> Point2D

A point in the current transform's frame, in screen space: through the transform, then the camera (none inside overlay). Where something drawn at local lands on screen.

For its position in world space, ask the camera: camera.to_world(canvas.to_screen(local)).

clip

def clip[R: Region](
    mut self,
    region: R,
    invert: Bool = False,
) -> ClipGuard[origin_of(self)]

Render only inside region for a with block — or, with invert, only outside it. region is any Region: a Rectangle, Circle, Triangle, Sector or Polygon, rounded by its own corner_radius; a polygon's inside is by the nonzero rule.

with canvas.clip(Rectangle((0, 0), 200, 120)):
    canvas.image(self.photo, (0, 0))

region is placed by the transform and camera current here, and stays there for the block: a transform or overlay inside it moves what is rendered, not the clip. A clip inside another keeps only what both keep. Every render call is clipped, its shadow and background() included; the letterbox is not. The clip's edge is the edge canvas.draw(region) would have.

Closes on exit, including on an early return or a raise.

to_local

def to_local(self, screen: Point2D) -> Point2D

A screen-space point — context.mouse.position, say — in the current transform's frame: back through the camera (none inside overlay), then the transform. So canvas.to_local(context.mouse.position) picks in the frame a shape is drawn in, with or without a camera.

camera

def camera(mut self, camera: Camera)

Set the active camera. Applies to every render call and every nested transform() from here on, until changed again or canvas.overlay() suspends it — and resets to identity next frame, like the rest of the transform state.

canvas.camera(self.camera)
canvas.image(self.player.position, ...)  # world-space coordinates
with canvas.overlay():
    canvas.text("Score: " + str(self.score), (0, canvas.top() - 20))

overlay

def overlay(mut self) -> OverlayGuard[origin_of(self)]

Suspend the camera and any active transform for a with block, so what's rendered inside lands in screen space regardless of where the camera looks — for a HUD or other UI that must stay put.

Pops back to whatever camera and transform were active on exit, including on an early return or a raise.

fill

def fill(mut self, color: Optional[Color] = None)

Paint the inside of shapes, switching the fill on. A color replaces any gradient; left unset, the current fill is kept, colour or gradient, so fill() alone brings back the last fill. The fill is off by default, in white, so fill() on a fresh frame paints shapes white.

Holds until changed or until the frame ends — every frame starts from the Style defaults, so nothing set here leaks into the next one.

def fill(mut self, gradient: Gradient)

Paint the inside of shapes with gradient, switching the fill on. It spans each shape's own bounding box, so it moves and turns with the shape; see Gradient. Only fills take one: strokes, text and shadows keep their colours.

Holds until a fill(color) replaces it or the frame ends. The fill color is kept meanwhile, so fill(color) alone brings a solid back.

stroke

def stroke(mut self, color: Optional[Color] = None, thickness: _OptionalFloat64 = None)

Stroke shapes, switching the stroke on if stroke_enabled(False) had turned it off. color/thickness left unset keep their current values. Worth knowing stroke is on by default, in black, 1 unit thick — a rectangle rendered without stroke_enabled(False) gets one nobody asked for.

Thickness is in world units, scaled by autoscale like every other coordinate, and never rendered thinner than one pixel; a fractional one (thickness=1.5) rounds to whole pixels once scaled.

fill_enabled

def fill_enabled(mut self, enabled: Bool)

Switch the fill off or back on. The fill color and any gradient are kept while off, so fill_enabled(True) brings back the same fill; fill(...) switches it on too.

stroke_enabled

def stroke_enabled(mut self, enabled: Bool)

Switch the stroke off or back on. Color and thickness are kept while off, so stroke_enabled(True) brings back the same stroke; stroke(...) switches it on too.

stroke_alignment

def stroke_alignment(mut self, alignment: StrokeAlign)

Place the stroke of every region inside its edge (StrokeAlign.INSIDE, the default), centred on it (CENTER) or around it (OUTSIDE). The fill meets the stroke's inner edge. Lines and curves have no inside and are always stroked centred, so it changes nothing there. Doesn't switch the stroke on.

stroke_cap

def stroke_cap(mut self, cap: StrokeCap)

Finish the open ends of lines and curves with cap: StrokeCap.BUTT (the default) flush with the end point, ROUND a half disc, SQUARE pushed out by half the thickness. A region's stroke and a closed curve have no ends, so it changes nothing there unless the stroke is dashed, when it finishes every dash. Doesn't switch the stroke on.

stroke_dash

def stroke_dash(mut self, dash: StrokeDash)

Break every stroke into dashes: StrokeDash.DASHED or DOTTED, scaled to the stroke's thickness, or exact lengths in world units such as StrokeDash(12, 6). StrokeDash.SOLID (the default) draws it unbroken again. Each dash's ends take stroke_cap, a region's too; see StrokeDash for where a pattern starts. Doesn't switch the stroke on.

corner_radius

def corner_radius(mut self, radius: Float64)

Round the shapes built from numbers by rectangle, triangle, polygon and sector, in world units, unless a call names its own corner_radius. A shape value carries its own radius, so draw(shape) and clip(shape) ignore this one: what is drawn from a shape is what its contains tests. Clamped per shape as the shape describes.

def corner_radius(mut self, radius: Int)

clear

def clear(mut self)

Discard everything drawn so far, back to how the canvas started.

An offscreen canvas is transparent again. The frame canvas starts the frame over: opened with the clear to gray 200 while the autoclear is on, or from the last frame's pixels while it is off. Style, transform, camera and settings are untouched — this undoes drawing, not state — and inside a clip block the clip still applies to what is drawn next. To paint over what is there instead, use background().

background

def background(mut self, color: Color)

Paint the whole framebuffer — the usual first call in update.

A translucent color blends instead of clearing, which is how motion trails are rendered: canvas.background(Color(0x11, 0x11, 0x11, 24)) fades the previous frame a little further each time. Trails need canvas.autoclear(False) set in create, or the frame's own clear wipes what they were fading.

An opaque color replaces the autoclear rather than stacking on it, so opening update with this costs one clear, not two.

def background(mut self, gradient: Gradient)

Paint the whole framebuffer with gradient — a sky, a vignette.

It spans the screen, left() to right() and bottom() to top(), whatever the camera is doing; under EXTEND that is the whole window. Where FIT leaves letterbox bars, the bars are painted over it as usual.

Like a colour, it replaces the autoclear if every stop is opaque, and blends over what is there otherwise.

save_image

def save_image(
    mut self,
    path: String,
    scale: Float64 = 1,
    transparent: Bool = False,
) raises

Save this frame as a PNG at the design size, times scale.

Window-independent by construction: the size of the file is the space the program renders in, never the size of the window, and the letterbox bars are absent because they belong to a window this image is not of. The same call under either backend produces the same image — the capture is rasterised on the CPU from the recorded commands, so the GPU path needs no readback. That is what makes this the export to use for artwork, posters and golden-image tests; use save_screenshot for what the user actually saw.

transparent drops the frame's background, leaving alpha 0 wherever nothing was rendered. scale multiplies the output resolution, so scale=2.0 gives a 2x export of the identical layout.

Deferred, not immediate: the file is written when the frame is presented, so it holds the whole frame however early in update this was called. A failure to write raises there, from present, rather than here.

An offscreen canvas has no frame to present, so it writes at once, what it has drawn so far. Its pixels are already rasterised, so a scale other than its own resamples them, and transparent keeps their alpha rather than dropping a background it has painted.

A relative path is resolved against the directory of the calling source file, not the working directory, as Image.load's is.

color_at

def color_at(mut self, position: Point2D) raises -> Color

The colour at position in what this frame has drawn so far.

position is in screen space, whatever the camera or transform — a read is of the screen, not the scene — so canvas.color_at(context.mouse.position) picks under the mouse as it is. Outside the screen it is Color.TRANSPARENT.

Reads what is drawn so far: a render call after this one does not change what it returned. The pixels are those save_image would write at scale 1, on either backend, without the letterbox. Reading replays the frame on the CPU once, and again only after something more is drawn, so many reads in a row cost one replay.

With canvas.autoclear(False), a read starts from the last frame's pixels, as the frame did — from the second frame that reads on; the first sees only what it drew itself. That needs the CPU backend, which is also the only one that accumulates.

On an offscreen canvas a read replays what was drawn since the last one onto the canvas's own pixels, at its own scale.

snapshot

def snapshot(mut self, scale: Float64 = 1) raises -> Image

What this frame has drawn so far, as an Image of the whole screen: width * scale by height * scale pixels.

Drawn back with canvas.image(shot, (0, 0), canvas.width(), canvas.height()) it lines up with the frame exactly — a feedback effect is a snapshot kept as a field and drawn under the next frame. scale=canvas.scale() gives the window's own resolution instead. Everything color_at says about what is read applies.

def snapshot(mut self, region: Rectangle, scale: Float64 = 1) raises -> Image

What this frame has drawn so far inside region, as an Image of region.w * scale by region.h * scale pixels.

region is in screen space, like color_at's position, so it is always upright, and a turned one raises; outside the screen the image is transparent.

On an offscreen canvas scale=canvas.scale() reads its pixels as they are; any other scale resamples them, nearest pixel.

save_screenshot

def save_screenshot(mut self, path: String) raises

Save this frame as a PNG at the framebuffer's own resolution.

The complement of save_image: this answers what the user saw, so it is the size of the drawable, letterbox bars included, rendered by whichever rasteriser actually drew the frame. That makes it deliberately machine-dependent — window size, HiDPI scaling and any driver antialiasing are all in it, and two machines will not produce the same file. Right for a bug report or for sharing a running sketch; use save_image for anything that has to be reproducible.

Deferred and raising in the same way as save_image: the file is written when the frame is presented, and a failure raises from there.

An offscreen canvas writes at once: its own pixels, at its own density, alpha kept — it has no window to have been seen in.

A relative path is resolved against the directory of the calling source file, as save_image's is.

start_recording

def start_recording(
    mut self,
    path: String,
    *,
    seconds: _OptionalFloat64 = None,
    frames: Optional[Int] = None,
    fps: Float64 = 30,
    scale: Float64 = 1,
    dither: Bool = False,
) raises

Record the frames from this one on as an animated GIF at path.

Each frame is what save_image would save: the design size times scale, no letterbox, replayed on the CPU, so either backend and every machine records the same pixels, and a headless run records deterministically. Recording starts with the frame this is called in, captured whole however early in update it was called, and runs until seconds of the run's time or frames frames, until stop_recording, or until the run ends, which finishes the file; cancel_recording ends it without one. A new start_recording finishes the recording in progress first.

Until it finishes, the frames stream to path with .partial added, which then replaces path in one step: a file already at path is untouched until then, and stays if the recording is cancelled.

The run's clock is sampled fps times a second, so the GIF plays at the program's own speed whatever the display's rate: a window faster than fps skips frames, and a slow frame stays on screen longer. A GIF times frames in hundredths of a second, and many players slow anything faster than 50 fps, so fps is at most 50.

Each frame gets a palette of its own: exact if it has 256 colours or fewer, the nearest 256 otherwise. dither mixes neighbouring pixels so smooth gradients don't band, at the cost of a grainier file.

Capturing and encoding run as each frame is presented, so a long recording of a heavy scene slows it down. With the autoclear off, each frame holds only what was drawn in it, as save_image does.

Raises at once for a path not ending in .gif, seconds and frames both given, a value that isn't positive, fps above 50, a file that can't be opened, or an offscreen canvas, which has no frames to record.

A relative path is resolved against the directory of the calling source file, as save_image's is.

stop_recording

def stop_recording(mut self) raises

Finish the recording in progress now and save it: the file is complete when this returns, and the frame it is called in is not in it. cancel_recording ends one without saving. Nothing happens when nothing is being recorded.

cancel_recording

def cancel_recording(mut self) raises

End the recording in progress without saving it: no file appears at its path, and a file already there is left as it was. Nothing happens when nothing is being recorded.

recording

def recording(self) -> Bool

Whether a recording started by start_recording is in progress.

rectangle

def rectangle(
    mut self,
    position: Point2D,
    w: Float64,
    h: Float64,
    *,
    corner_radius: _OptionalFloat64 = None,
    rotation: _OptionalFloat64 = SIMD(0),
)

Draw the w by h rectangle centred on position, its corners rounded by corner_radius, or by the style's when not given (clamped to half the shorter side), and turned about its centre by rotation radians counter-clockwise, as Rectangle describes it.

def rectangle(
    mut self,
    position: Point2D,
    w: Int,
    h: Int,
    *,
    corner_radius: _OptionalFloat64 = None,
    rotation: _OptionalFloat64 = SIMD(0),
)

circle

def circle(mut self, position: Point2D, r: Float64)
def circle(mut self, position: Point2D, r: Int)

ellipse

def ellipse(mut self, position: Point2D, w: Float64, h: Float64)

Draw the axis-aligned ellipse centred on position, w wide and h high: an Ellipse, filling the rectangle rectangle(position, w, h) would draw. Turn one with transform.

The stroke is inset, as a circle's is, and as thick all round: the fill stops where it starts, and a translucent ellipse composites once. It is drawn as a polygon, flattened as finely as a circle of its larger radius, so filled it costs what a circle does and stroked what a stroked polygon of its outline does, about three times a circle's. It casts no inset shadow; a blurred drop shadow costs what a bezier's does. With w == h it covers what circle(position, w / 2) does, to within its flattening.

def ellipse(mut self, position: Point2D, w: Int, h: Int)

capsule

def capsule(mut self, start: Point2D, end: Point2D, r: Float64)

Draw the capsule round the segment from start to end: every point within r of it, a Capsule, its ends half discs.

The stroke is inset, as a circle's is: the fill stops where it starts, and a translucent capsule composites once. Unlike a round-capped line, it has a fill and a stroke of its own. It is drawn as a polygon, each end flattened as finely as a circle of r, so filled it costs what a circle does and stroked what a stroked polygon of its outline does. It casts no inset shadow; a blurred drop shadow costs what a bezier's does. A zero r draws nothing.

def capsule(mut self, start: Point2D, end: Point2D, r: Int)

sector

def sector(
    mut self,
    position: Point2D,
    r: Float64,
    start_angle: Float64,
    sweep_angle: Float64,
    *,
    corner_radius: _OptionalFloat64 = None,
)

Draw the pie slice of the circle around position with radius r, from start_angle round by sweep_angle: a Sector, its tip at position.

Angles are radians, counter-clockwise from the +x axis (y is up); a negative sweep runs clockwise, and a full turn or more is the whole disc. The stroke is inset, as a circle's is: the fill stops where it starts, and a translucent sector composites once. It casts no inset shadow; a blurred drop shadow costs what a bezier's does. A zero radius or sweep draws nothing.

corner_radius, or the style's when not given, rounds the tip and both ends of the arc, as Sector describes. A rounded sector is drawn as a polygon, so it costs what a stroked polygon of its flattened outline does.

def sector(
    mut self,
    position: Point2D,
    r: Int,
    start_angle: Float64,
    sweep_angle: Float64,
    *,
    corner_radius: _OptionalFloat64 = None,
)

polygon

def polygon(mut self, *vertices: Point2D, *, corner_radius: _OptionalFloat64 = None)

Draw the polygon through vertices, closed back to the first.

Filled by the nonzero rule, so a self-crossing polygon fills every area its edges wind round, whichever way they run. The stroke is inset along the outside, as a sector's is, and centred on an edge running through the inside — a pentagram's inner pentagon — so every edge is stroked as thick; a translucent polygon composites once. It casts no inset shadow; a blurred drop shadow costs what a bezier's does. Fewer than three vertices draw nothing.

corner_radius, or the style's when not given, rounds every corner, as Polygon describes; each rounded corner adds vertices, and so cost. A Polygon value is drawn with draw.

def polygon(
    mut self,
    var vertices: List[Point2D],
    corner_radius: _OptionalFloat64 = None,
)

Draw the polygon through vertices, a list built at run time: otherwise as the variadic polygon, and like spline's points.

line

def line(mut self, start: Point2D, end: Point2D)

Stroke the line from start to end in the stroke colour and thickness, its ends finished as stroke_cap says. A line has no interior, so fill never applies; a line of no length is a dot under StrokeCap.ROUND or SQUARE and nothing under BUTT.

bezier

def bezier(mut self, start: Point2D, control1: Point2D, control2: Point2D, end: Point2D)

Stroke the cubic Bézier from start to end, shaped by the two controls, in the stroke colour and thickness.

A curve, like a line, has no interior: fill never applies, and with the stroke off nothing is drawn. The curve is flattened at replay, in device pixels, so it stays smooth under any camera zoom; its ends are finished as stroke_cap says, like a line's, unless end is start: then the curve closes on a mitred joint and has none. Joints are mitred and caps share the end's edge, so a translucent stroke composites once, except at a sharp cusp, where it may overlap slightly.

A blurred shadow is blurred afresh every frame (it cannot be cached like an image's), and on the GPU costs one extra draw call.

spline

def spline(
    mut self,
    var points: List[Point2D],
    alpha: Float64 = 0.5,
    closed: Bool = False,
)

Stroke a smooth curve through every one of points, in order, in the stroke colour and thickness: a Spline, which is Catmull-Rom.

alpha sets how the spacing of the points shapes the curve: 0 can loop or cusp where points bunch up, 0.5 (centripetal) never does, 1 hugs the points more loosely still. With closed, the curve runs on from the last point back to the first, and that joint is mitred like the rest.

Drawn as one stroke, like bezier: no fill, nothing with the stroke off, ends as stroke_cap says when open, and a translucent stroke composites once. A blurred shadow costs what a bezier's does. Fewer than two distinct points draw nothing.

arc

def arc(
    mut self,
    position: Point2D,
    r: Float64,
    start_angle: Float64,
    sweep_angle: Float64,
)

Stroke the arc of the circle around position with radius r, from start_angle round by sweep_angle, in the stroke colour and thickness: an Arc.

Angles are radians, counter-clockwise from the +x axis (y is up); a negative sweep runs clockwise, and a full turn or more is the whole circle, closed on a mitred joint.

Drawn as one stroke, like bezier: no fill, nothing with the stroke off, ends as stroke_cap says short of a whole turn, and a translucent stroke composites once. A blurred shadow costs what a bezier's does. A zero radius or sweep draws nothing.

def arc(mut self, position: Point2D, r: Int, start_angle: Float64, sweep_angle: Float64)

triangle

def triangle(
    mut self,
    a: Point2D,
    b: Point2D,
    c: Point2D,
    *,
    corner_radius: _OptionalFloat64 = None,
)

Draw the triangle through a, b and c, its corners rounded by corner_radius, or by the style's when not given, as Triangle describes it.

image

def image(mut self, image: Image, position: Point2D)

Render image at its own pixel size.

The same command as the sized overload: at a pixel scale of 1 the two agree exactly, and the one-image-pixel-per-framebuffer-pixel shortcut they used to differ by now lives inside blit_image, where the replay can take it without the record site having to know.

def image(mut self, image: Image, position: Point2D, w: Float64, h: Float64)
def image(mut self, animator: Animator, position: Point2D)

Render the animator's current frame, centred at position.

def image(mut self, animator: Animator, position: Point2D, w: Float64, h: Float64)
def image(mut self, image: Image, position: Point2D, w: Int, h: Int)
def image(mut self, animator: Animator, position: Point2D, w: Int, h: Int)

text_color

def text_color(mut self, color: Color)

Paint glyphs in color. Separate from fill, so a shape colour and a label colour do not have to be set in turn; a fully transparent one skips the text entirely.

font_size

def font_size(mut self, size: Float64)

Text height in world units, scaled by autoscale like a coordinate. Glyphs are rasterised at whole pixel sizes, so a fractional size rounds once scaled.

def font_size(mut self, size: Int)

font_weight

def font_weight(mut self, weight: FontWeight)

Stroke weight of the face, a FontWeight. The packaged Noto faces are variable, so this interpolates rather than swapping files.

font_italic

def font_italic(mut self, enabled: Bool)

Draw text in the face's italic: the font's italic_path file where it has one (the packaged Noto Sans does), otherwise its own italic axis, otherwise its upright glyphs slanted.

tint

def tint(mut self, color: Color)

Multiply whatever is rendered next by color, channel by channel.

Reaches fill, gradient, stroke, text, image and shadow alike, never background. Color.WHITE (the default) leaves it untouched; a colour recolours and darkens, and its alpha fades like opacity, which it stacks with. Tints a whole look at once, where fill, stroke and text_color each set one part:

with canvas.style(tint=Color(255, 90, 90)):
    self.player.draw(canvas)

Resolved at record time, like every other style setting.

opacity

def opacity(mut self, value: Float64)

Fade whatever is rendered next: fill, gradient, stroke, text, image and shadow alike, never background. 1.0 (the default) leaves it untouched; 0.0 renders nothing visible. Each shape composites once, so a translucent one is evenly translucent, stroke and fill included. Resolved at record time, like every other style setting — it cannot reach back and fade what was already rendered.

blend_mode

def blend_mode(mut self, mode: BlendMode)

Combine whatever is rendered next with what is already there by mode — BlendMode.ADD for glows, MULTIPLY for shadows — instead of painting over it. BlendMode.NORMAL (the default) paints over.

Applies to shapes, text and images, never to background.

shadow

def shadow(
    mut self,
    color: Optional[Color] = None,
    offset: Optional[Vector2D] = None,
    blur: _OptionalFloat64 = None,
    spread: _OptionalFloat64 = None,
    inset: Optional[Bool] = None,
)

Cast a shadow behind what is rendered next, switching shadows on. Parts left unset keep their current values, so shadow() alone brings back the last shadow, and shadow(blur=12) changes only the blur; a fresh frame starts at a translucent black, an offset of (4, -4) (down and right, since y is up), a blur of 8 and no spread.

Works like CSS drop-shadow: the shadow is the shape's whole silhouette, fill and stroke together, cast once — a translucent fill shows its own shadow through it. Lines cast their stroke, text its glyphs and images their alpha. blur is the CSS blur radius, twice the Gaussian's standard deviation; spread grows the silhouette before blurring. Both are world units, scaled like a coordinate.

inset=True paints the shadow inside the shape instead, over its fill and within its stroke, as if the shape were a hole cut in a surface: the band shows on the side the offset points away from. Only rectangles, circles and triangles take an inset shadow; lines, text and images cast none while it is set.

The offset is fixed to the screen, so a rotated shape's shadow still falls the same way; see shadow_follows_transform. The shadow combines with what is beneath it by the style's blend_mode, like the shape itself.

canvas.shadow(Color(0, 0, 0, 96), offset=Vector2D(6, -6), blur=12)
canvas.rectangle((0, 0), 120, 80)

shadow_enabled

def shadow_enabled(mut self, enabled: Bool)

Switch shadows off or back on. Colour, offset, blur, spread and inset are kept while off, so shadow_enabled(True) brings back the same shadow; shadow(color) switches it on too. Off by default.

shadow_follows_transform

def shadow_follows_transform(mut self, follows: Bool)

Whether the shadow's offset turns with the transform.

False (the default) keeps it fixed to the screen, as a light overhead would: a rotating shape's shadow always falls the same way. Camera zoom and autoscale still scale it, so it stays in proportion. True applies the offset in the shape's own frame, before the transform, so it rotates and shears with the shape, as a CSS shadow does under a CSS transform.

text_alignment

def text_alignment(mut self, align: Align)

Anchor the next text at one of the nine points of its box.

One argument covers both axes: Align.CENTER (the default) centres the text on the position, like every other shape, and Align.TOP_LEFT hangs it below and to the right of it. The one-word constants name an edge's midpoint — Align.TOP is top-centre. These are edges of the text box, not typographic baselines.

antialiasing

def antialiasing(self) -> Antialiasing

How smooth shape edges are: run's antialiasing unless set.

def antialiasing(mut self, level: Antialiasing)

Smooth shape edges at level — OFF for hard pixels.

A frame-wide setting: both backends rasterise the frame when it is presented, so the level covers every shape in this frame, including those drawn before the call, and lasts until changed. run's antialiasing argument is the starting level; set it here, in create or any frame, to change it. See Antialiasing.

autoclear

def autoclear(self) -> Bool

Whether frames open with the clear to gray 200. On unless set.

def autoclear(mut self, enabled: Bool)

Switch the clear each frame opens with off or back on.

On by default, clearing to gray 200 so the default style is visible on a program that never calls background(). Off lets ink accumulate across frames (CPU backend only — the GPU swaps buffers).

A frame-wide setting: it applies to this whole frame, wherever in it it is called — off takes this frame's opening clear back out, on puts it back — and lasts until changed, so set it once in create. An opaque background() already drawn this frame stays either way.

No effect on an offscreen canvas, which has no frames to open and starts transparent; it is only read back.

letterbox_color

def letterbox_color(self) -> Color

The colour of the bars outside the design area. Black unless set.

def letterbox_color(mut self, color: Color)

Colour the bars outside the design area under Autoscale.FIT.

A frame-wide setting: the bars are painted after the frame is drawn, so this frame's bars take it wherever in the frame it is called, and it lasts until changed — set it once in create. Not part of a style. No effect on an offscreen canvas, which has no window to bar.

font

def font(mut self, font: Font)

Draw the next text in font, a face from Font.load.

Part of the style, like fill: it holds for the rest of the frame, or for a with canvas.style(font=...) block, and each text call keeps the face it was drawn with. The next frame starts in the packaged Noto Sans again, so a font for the whole program is a field set at the top of update, or kept in a Style.

text_width

def text_width(self, text: String) raises -> Float64

How wide text(text, ...) would render under the current font, size and weight, in the units the next render call is positioned in: the width of its widest line.

Exact for any prefix of a line too, since glyphs sit advance to advance with no kerning, so a caret after a line's first few characters goes at the line's left edge plus the width of those characters. With the default Align.CENTER, that left edge is position.x - text_width(line) / 2.

Raises only if the default font has to be loaded and cannot be.

def text_width(self, text: String, *, width: Float64) raises -> Float64

How wide text(text, ..., width) would render: the width of its widest line once wrapped at width, at most width unless a single glyph is wider.

Raises only if the default font has to be loaded and cannot be.

text_height

def text_height(self, text: String) raises -> Float64

How tall text(text, ...) would render under the current font and size, in the units the next render call is positioned in: from the font's ascent above the first line to its descent below the last, the box text_alignment anchors. Each line break adds the font's line height.

Raises only if the default font has to be loaded and cannot be.

def text_height(self, text: String, *, width: Float64) raises -> Float64

How tall text(text, ..., width) would render: its height once wrapped at width, every line it wraps onto counted.

Raises only if the default font has to be loaded and cannot be.

text

def text(mut self, text: String, position: Point2D)

Draw text at position in the current text style, anchored by text_alignment.

A newline starts a new line, the font's line height below; each line is aligned on its own, and the alignment's top and bottom are those of the whole block (see text_height).

def text(mut self, text: String, position: Point2D, width: Float64)

Draw text at position as text(text, position) does, wrapped onto as many lines as keep each at most width units wide.

A line breaks at its last space that fits, the spaces dropped; a word wider than width alone breaks between its letters. Newlines still break, and each line is aligned on its own, so text_alignment both anchors the block and aligns its lines: Align.TOP_LEFT sets ragged right text hanging from position. Measure the block with text_width(text, width=...) and text_height(text, width=...).

StyleGuard

struct StyleGuard[origin: MutOrigin]

Restores the style the frame had when the scope was entered.

Style is a plain value, so the guard carries its own snapshot and no stack is needed — nesting works because each guard restores what it saw. Only canvas.style builds one.

Implements Deinitable, Movable.

__enter__

def __enter__(mut self)

__exit__

def __exit__(mut self)

TransformGuard

struct TransformGuard[origin: MutOrigin]

Pops the matrix canvas.transform pushed, on scope exit.

Only canvas.transform builds one: its constructor's _-keywords are the library's own.

Implements Deinitable, Movable.

__enter__

def __enter__(mut self)

__exit__

def __exit__(mut self)

OverlayGuard

struct OverlayGuard[origin: MutOrigin]

Restores the camera and transform canvas.overlay suspended, on scope exit. Only canvas.overlay builds one.

Implements Deinitable, Movable.

__enter__

def __enter__(mut self)

__exit__

def __exit__(mut self)

ClipGuard

struct ClipGuard[origin: MutOrigin]

Closes the clip canvas.clip opened, on scope exit. Only canvas.clip builds one.

Holds the clip that was current before it rather than popping a stack, so nesting works the way StyleGuard's does.

Implements Deinitable, Movable.

__enter__

def __enter__(mut self)

__exit__

def __exit__(mut self)