Canvas Drawing

The Canvas view provides a drawing surface backed by Core Graphics. You can render shapes, lines, text, and images using declarative drawing commands from the nib.draw module.

Creating a canvas

import nib

canvas = nib.Canvas(width=400, height=300, background_color="#1a1a1a")

The canvas also accepts all standard view modifiers (padding, opacity, corner_radius, etc.) as keyword arguments.

Drawing commands

All drawing commands live in the nib.draw module. Pass a list of commands to canvas.draw():

canvas.draw([
    nib.draw.Rect(x=10, y=10, width=100, height=50, fill="#3498db"),
    nib.draw.Circle(cx=200, cy=100, radius=40, fill="#e74c3c"),
    nib.draw.Line(x1=10, y1=200, x2=390, y2=200, stroke="#2ecc71"),
    nib.draw.Text("Hello Canvas!", x=10, y=280, fill="#ffffff"),
])

Rect

Draws a rectangle, optionally with rounded corners:

nib.draw.Rect(
    x=10, y=10,
    width=100, height=80,
    corner_radius=8,
    fill="#3498db",
    stroke="#2980b9",
    stroke_width=2,
    opacity=0.9,
)

Circle

Draws a circle from a center point and radius:

nib.draw.Circle(cx=200, cy=100, radius=40, fill="#e74c3c")

Ellipse

Draws an ellipse with independent horizontal and vertical radii:

nib.draw.Ellipse(cx=150, cy=100, rx=80, ry=40, fill="#9b59b6")

Line

Draws a straight line between two points:

nib.draw.Line(
    x1=10, y1=10,
    x2=200, y2=200,
    stroke="#000000",
    stroke_width=2,
    line_cap="round",  # "butt", "round", or "square"
)

Text

Draws text at a position:

nib.draw.Text(
    "Hello World",
    x=10, y=50,
    fill="#ffffff",
    font=nib.Font.system(20, weight=nib.FontWeight.BOLD),
    alignment=nib.HorizontalAlignment.CENTER,
)

Arc

Draws an arc segment:

import math

nib.draw.Arc(
    cx=100, cy=100,
    radius=50,
    start_angle=0,
    end_angle=math.pi,
    fill="#f39c12",
    stroke="#e67e22",
)

Path

Draws a path from a list of points:

nib.draw.Path(
    points=[(10, 10), (100, 50), (50, 100)],
    closed=True,
    fill="#2ecc71",
    stroke="#27ae60",
    stroke_width=2,
)

Polygon

A convenience wrapper around Path with closed=True:

nib.draw.Polygon(
    points=[(100, 10), (190, 80), (160, 170), (40, 170), (10, 80)],
    fill="#1abc9c",
)

BezierPath

Draws complex curves using typed path elements:

from nib.draw import BezierPath, MoveTo, CubicTo, QuadraticTo, LineTo, Close

nib.draw.BezierPath(
    elements=[
        MoveTo(50, 150),
        CubicTo(cp1x=50, cp1y=50, cp2x=150, cp2y=50, x=150, y=150),
        Close(),
    ],
    fill="#e74c3c",
    stroke="#c0392b",
)

Available path elements:

Element Parameters Description
MoveTo(x, y) x, y Move to a point without drawing
LineTo(x, y) x, y Draw a straight line to a point
CubicTo(...) cp1x, cp1y, cp2x, cp2y, x, y Cubic bezier curve
QuadraticTo(...) cp1x, cp1y, x, y, w Quadratic bezier curve
Close() none Close the path back to the start
ArcTo(...) x, y, radius, rotation, large_arc, clockwise Arc segment
Oval(...) x, y, width, height Ellipse inscribed in rectangle
PathRect(...) x, y, width, height, border_radius Rectangle sub-path
SubPath(...) x, y, elements Nested sub-path at offset

Image

Draws an image from raw bytes:

with open("photo.jpg", "rb") as f:
    nib.draw.Image(
        data=f.read(),
        x=10, y=10,
        width=200,
        height=150,
    )

Points

Draws a set of points with different modes:

nib.draw.Points(
    points=[(10, 10), (50, 50), (100, 30), (150, 80)],
    point_mode=nib.draw.PointMode.POINTS,  # POINTS, LINES, or POLYGON
    stroke="#e74c3c",
    stroke_width=5,
    stroke_cap="round",
)

Fill

Fills the entire canvas with a color or gradient:

nib.draw.Fill(fill="#1a1a1a")

Colors

All color parameters accept hex strings or nib.Color objects:

# Hex strings
nib.draw.Rect(x=0, y=0, width=100, height=100, fill="#FF5733")

# nib.Color constants
nib.draw.Circle(cx=50, cy=50, radius=30, fill=nib.Color.RED)

Gradients

Fill parameters on Rect, Circle, Ellipse, BezierPath, and Fill accept gradient objects instead of solid colors:

LinearGradient

nib.draw.Rect(
    x=0, y=0, width=200, height=100,
    fill=nib.draw.LinearGradient(
        start=(0, 0),
        end=(200, 100),
        colors=["#FF0000", "#0000FF"],
        stops=[0.0, 1.0],  # optional
    ),
)

RadialGradient

nib.draw.Circle(
    cx=100, cy=100, radius=80,
    fill=nib.draw.RadialGradient(
        center=(100, 100),
        radius=80,
        colors=[nib.Color.YELLOW, nib.Color.RED],
    ),
)

SweepGradient

nib.draw.Circle(
    cx=100, cy=100, radius=80,
    fill=nib.draw.SweepGradient(
        center=(100, 100),
        colors=[nib.Color.RED, nib.Color.GREEN, nib.Color.BLUE, nib.Color.RED],
    ),
)

Drawing methods

The Canvas provides three methods to manage drawing commands:

canvas.draw(commands)

Replaces all current commands and triggers a re-render:

canvas.draw([
    nib.draw.Rect(x=0, y=0, width=100, height=100, fill="#3498db"),
])

canvas.append(command)

Adds a single command to the existing list:

canvas.append(nib.draw.Circle(cx=50, cy=50, radius=20, fill="#e74c3c"))

canvas.clear()

Removes all commands and updates the display:

canvas.clear()

Gesture handling

Enable gestures to respond to mouse and trackpad input on the canvas. Gestures are enabled automatically when you provide any gesture callback:

canvas = nib.Canvas(
    width=400, height=300,
    on_pan_start=handle_start,
    on_pan_update=handle_update,
    on_pan_end=handle_end,
    on_hover=handle_hover,
)

You can also set gesture callbacks after creation:

canvas = nib.Canvas(width=400, height=300)
canvas.on_pan_start = handle_start
canvas.on_pan_update = handle_update

PanEvent

All gesture callbacks receive a PanEvent dataclass with the cursor coordinates:

from nib import PanEvent

def handle_update(e: PanEvent):
    print(f"x={e.x}, y={e.y}")