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:
FITscales uniformly bymin(w, h), centres the design, and paints the leftover withcanvas.letterbox_colorafter 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.EXTENDuses 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 tocanvas.left()/right()/bottom()/top(), since design coordinates no longer describe the edges.OFFdoes 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: Colorfill_gradient: Optional[Gradient]: Paints the fill instead offill_colorwhile set;fill_coloris kept for when it is cleared.fill_enabled: Boolstroke_color: Colorstroke_thickness: Float64stroke_enabled: Boolstroke_alignment: StrokeAlign: Where a region's stroke sits against its edge;INSIDEunless set. Lines and curves are always stroked centred.stroke_cap: StrokeCap: How the open ends of lines and curves are finished;BUTTunless set. Regions' strokes and closed curves have no ends, unless dashed.stroke_dash: StrokeDash: How the stroke is broken into dashes;SOLIDunless 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: Colorfont: Optional[Font]: The face text is drawn in; none for the packaged Noto Sans.font_size: Float64font_weight: FontWeightfont_italic: Booltext_alignment: Aligntint: Coloropacity: Float64blend_mode: BlendModeshadow_color: Colorshadow_offset: Vector2Dshadow_blur: Float64shadow_spread: Float64shadow_inset: Boolshadow_follows_transform: Boolshadow_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: Point2Dzoom: 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)