Skip to content

Shapes

Rect

Rect draws a filled rectangle. Set its size with .size(width, height), fill color with .color(), and position with .xy(x, y). By default the rect has zero size and is placed according to the parent's layout (centered for the default layout).

with Scene():
    Rect().size(160, 90).color("steelblue")

Output image

fairyflow frame 0

Stroke

Add an outline with .stroke_color() and .stroke_width(). Setting a fill color to None gives a hollow shape.

with Scene():
    r = Rect().size(160, 90)
    r.color("lightyellow").stroke_color("navy").stroke_width(4)

Output image

fairyflow frame 0

Positioning

See Positioning for .xy(), .align_x(), .align_y(), and .move().


Ellipse

Ellipse draws an ellipse. When width == height it becomes a circle. Its API is identical to Rect.

with Scene():
    Ellipse().size(160, 110).color("coral")

Output image

fairyflow frame 0
with Scene():
    Ellipse().size(80, 80).color("orchid").xy(30, 60)
    Ellipse().size(80, 40).color("gold").xy(130, 80)
    Ellipse().size(40, 80).color("steelblue").xy(220, 60)

Output image

fairyflow frame 0

Path

Path draws an arbitrary vector shape from a sequence of commands. Use .stroke_color() to set the line color and .stroke_width() for line thickness. Like Rect, it can also be filled with .color().

Line segments

with Scene():
    p = Path()
    p.stroke_color("darkslateblue").stroke_width(3).color("lavender")
    p.move_to().xy(30, 100)
    p.line_to().xy(150, 40)
    p.line_to().xy(270, 100)
    p.line_to().xy(150, 160)
    p.close()

Output image

fairyflow frame 0

Cubic Bézier curves

.cubic_to() appends a cubic Bézier segment. Use .c1_xy(dx, dy) and .c2_xy(dx, dy) to set the two control point offsets (relative to the segment start and end respectively).

with Scene():
    p = Path()
    p.stroke_color("darkorange").stroke_width(3)
    p.move_to().xy(40, 150)
    p.cubic_to().xy(260, 150).c1_xy(60, -130).c2_xy(-60, -130)

Output image

fairyflow frame 0

Arrows

.triangle_arrow() adds a filled arrowhead at the end of a path. Pass "start" to place it at the beginning instead. The arrowhead is automatically sized to match the stroke width.

with Scene():
    p = Path()
    p.stroke_color("steelblue").stroke_width(3)
    p.move_to().xy(40, 100)
    p.line_to().xy(260, 100)
    p.triangle_arrow()
    p.triangle_arrow("start").color("tomato")

Output image

fairyflow frame 0

The arrowhead inherits the path's stroke color by default. Call .color() on the returned arrow object to override it independently — as shown above with the red start arrow.

The arrowhead scales automatically with stroke width. Pass length and width to triangle_arrow() to override the size explicitly — length is the tip-to-base distance, width is the base width (both default to 3 × stroke_width):

with Scene():
    # top: automatic size (stroke_width=8 → arrow 24×24)
    p = Path()
    p.stroke_color("steelblue").stroke_width(8)
    p.move_to().xy(40, 60)
    p.line_to().xy(250, 60)
    p.triangle_arrow()

    # bottom: same stroke but arrow manually set to length=40, width=20
    q = Path()
    q.stroke_color("steelblue").stroke_width(8)
    q.move_to().xy(40, 140)
    q.line_to().xy(250, 140)
    q.triangle_arrow(length=40, width=20)

Output image

fairyflow frame 0

Path cropping

crop_start and crop_end trim the path from either end. Values are in [0.0, 1.0] where 0.0 is the full extent. This is mainly used to animate paths drawing themselves in.

with Scene():
    p = Path()
    p.stroke_color("mediumseagreen").stroke_width(4)
    p.move_to().xy(30, 100)
    p.cubic_to().xy(150, 40).c1_xy(50, -60).c2_xy(-50, -60)
    p.cubic_to().xy(270, 100).c1_xy(50, 60).c2_xy(-50, 60)
    p.crop_end(0.5)

Output image

fairyflow frame 0