Skip to content

Types & Utilities

Core types used throughout PyFreeform, plus utility functions for common operations.


Coord dataclass

Coord(x: float, y: float)

An immutable 2D coordinate with math operations.

Coords are the foundation of all positioning in PyFreeform. They support arithmetic operations and common geometric calculations.

Attributes:

Name Type Description
x float

Horizontal coordinate

y float

Vertical coordinate

Example
p1 = Coord(100, 200)
p2 = Coord(50, 50)
p1 + p2              # Coord(150, 250)
p1.distance_to(p2)   # 158.11...

x instance-attribute

x: float

y instance-attribute

y: float

distance_to

distance_to(other: Coord) -> float

Calculate Euclidean distance to another coord.

midpoint

midpoint(other: Coord) -> Coord

Return the midpoint between this coord and another.

lerp

lerp(other: Coord, t: float) -> Coord

Linear interpolation between this coord and another.

Parameters:

Name Type Description Default
other Coord

The target coord.

required
t float

Interpolation factor (0 = self, 1 = other). Values outside 0-1 extrapolate beyond the coords.

required

Returns:

Type Description
Coord

The interpolated coord.

Example

p1 = Coord(0, 0)
p2 = Coord(100, 100)
p1.lerp(p2, 0.5)
Coord(50.0, 50.0)

normalized

normalized() -> Coord

Return a unit vector pointing in the same direction.

dot

dot(other: Coord) -> float

Calculate dot product with another coord (as vectors).

rotated

rotated(angle: float, origin: Coord | None = None) -> Coord

Rotate coord around an origin.

Parameters:

Name Type Description Default
angle float

Rotation angle in radians (counter-clockwise).

required
origin Coord | None

Center of rotation (default: origin 0,0).

None

Returns:

Type Description
Coord

The rotated coord.

clamped

clamped(min_x: float, min_y: float, max_x: float, max_y: float) -> Coord

Return coord clamped to the given bounds.

rounded

rounded(decimals: int = 0) -> Coord

Return coord with coordinates rounded.

as_tuple

as_tuple() -> tuple[float, float]

Return as a plain tuple.

coerce classmethod

coerce(value: CoordLike) -> Coord

Convert a CoordLike to a Coord, passing through if already a Coord.

Supports arithmetic — addition, subtraction, scalar multiply/divide, negation:

a = Coord(100, 200)
b = Coord(50, 50)

a + b       # Coord(150, 250)
a - b       # Coord(50, 150)
a * 2       # Coord(200, 400)
a / 2       # Coord(50.0, 100.0)
-a          # Coord(-100, -200)
a[0], a[1]  # 100, 200 (subscript access)

RelCoord dataclass

RelCoord(rx: float, ry: float)

An immutable 2D relative coordinate (fractions 0.0-1.0).

Used for positioning within surfaces. (0, 0) is top-left, (1, 1) is bottom-right. Fields are named rx and ry to prevent confusion with pixel Coord(x, y).

Example
p = RelCoord(0.5, 0.5)
p.rx, p.ry            # (0.5, 0.5)
p + RelCoord(0.1, 0)  # RelCoord(0.6, 0.5)

rx instance-attribute

rx: float

ry instance-attribute

ry: float

lerp

lerp(other: RelCoord, t: float) -> RelCoord

Linear interpolation between this and another relative coord.

clamped

clamped(min_rx: float = 0.0, min_ry: float = 0.0, max_rx: float = 1.0, max_ry: float = 1.0) -> RelCoord

Return clamped to valid range (default 0.0-1.0).

as_tuple

as_tuple() -> tuple[float, float]

Return as a plain tuple.

coerce classmethod

coerce(value: RelCoordLike) -> RelCoord

Convert a RelCoordLike to a RelCoord, passing through if already a RelCoord.

Fields are rx, ry (not x, y) to distinguish from pixel Coord. Supports the same arithmetic as Coord:

a = RelCoord(0.2, 0.3)
b = RelCoord(0.1, 0.1)

a + b   # RelCoord(0.3, 0.4)
a - b   # RelCoord(0.1, 0.2)
a * 2   # RelCoord(0.4, 0.6)
rx, ry = a  # destructuring works

Where RelCoord Appears

API Usage
entity.at Returns RelCoord (or None if in absolute mode)
add_*(..., at=) Accepts RelCoord, plain tuple, or named position string
cell.normalized_position Returns RelCoord (cell position within grid, 0.0–1.0)

Plain tuples work too

All APIs that accept RelCoord also accept plain (rx, ry) tuples. Returned values are RelCoord instances with named fields and helper methods.


AnchorSpec

AnchorSpec is the type accepted by entity.anchor(), surface.anchor(), and the start_anchor/end_anchor parameters of connect(). It unifies three forms:

Form Example Description
str "center", "top_right", "v0" Named anchor (entity-specific)
tuple[float, float] (0.7, 0.3) Relative coordinate within bounding box
RelCoord RelCoord(0.7, 0.3) Same as tuple, with named fields

For entities, tuples/RelCoords resolve against the axis-aligned bounding box. Rect overrides this to use local coordinate space (rotation-aware). For surfaces, they resolve against the surface's rectangular region.

rect.anchor("top_right")           # Named anchor
rect.anchor((0.7, 0.3))            # 70% across, 30% down
dot.connect(rect, end_anchor="left") # In connections

PaintLike

PaintLike = ColorLike | Gradient — the union type for all fill=, color=, and stroke= parameters. Accepts any solid color or gradient object.


Image

Image(red: ndarray, green: ndarray, blue: ndarray, alpha: ndarray | None = None)

An image composed of multiple layers.

Each layer represents a channel of the image (red, green, blue, alpha) or a computed property (brightness, grayscale). Layers are created lazily when accessed.

Attributes:

Name Type Description
width int

Image width in pixels

height int

Image height in pixels

layers dict[str, Layer]

Dictionary of layer name to Layer object

Example
img = Image.load("photo.png")
img.width, img.height       # (800, 600)
red_layer = img["red"]
color = img.hex_at(100, 100) # '#ff5733'

Create an image from channel arrays.

Use Image.load() to create from a file instead.

Parameters:

Name Type Description Default
red ndarray

Red channel as 2D numpy array (0-255).

required
green ndarray

Green channel as 2D numpy array (0-255).

required
blue ndarray

Blue channel as 2D numpy array (0-255).

required
alpha ndarray | None

Optional alpha channel as 2D numpy array (0-255).

None

width property

width: int

Image width in pixels.

height property

height: int

Image height in pixels.

has_alpha property

has_alpha: bool

Whether this image has an alpha channel.

size property

size: tuple[int, int]

Image size as (width, height) tuple.

load classmethod

load(path: str | Path, frame: int = 0) -> Image

Load an image from a file.

Supports PNG, JPEG, GIF, WebP, BMP, TIFF, and other formats supported by Pillow.

Parameters:

Name Type Description Default
path str | Path

Path to the image file.

required
frame int

For animated images (GIF), which frame to load (default: 0).

0

Returns:

Type Description
Image

A new Image instance.

Raises:

Type Description
FileNotFoundError

If the file doesn't exist.

ValueError

If the file can't be decoded as an image.

from_pil classmethod

from_pil(pil_img: Image) -> Image

Create an Image from a PIL Image object.

Parameters:

Name Type Description Default
pil_img Image

A PIL Image object.

required

Returns:

Type Description
Image

A new Image instance.

rgb_at

rgb_at(x: int, y: int) -> tuple[int, int, int]

Get RGB color at a position.

Parameters:

Name Type Description Default
x int

Horizontal position (0 = left).

required
y int

Vertical position (0 = top).

required

Returns:

Type Description
tuple[int, int, int]

Tuple of (red, green, blue) as integers 0-255.

hex_at

hex_at(x: int, y: int) -> str

Get hex color string at a position.

Parameters:

Name Type Description Default
x int

Horizontal position (0 = left).

required
y int

Vertical position (0 = top).

required

Returns:

Type Description
str

Color as "#rrggbb" string.

rgba_at

rgba_at(x: int, y: int) -> tuple[int, int, int, int]

Get RGBA color at a position.

Parameters:

Name Type Description Default
x int

Horizontal position (0 = left).

required
y int

Vertical position (0 = top).

required

Returns:

Type Description
int

Tuple of (red, green, blue, alpha) as integers 0-255.

int

If no alpha channel exists, alpha is 255.

alpha_at

alpha_at(x: int, y: int) -> float

Get alpha (opacity) at a position.

Parameters:

Name Type Description Default
x int

Horizontal position (0 = left).

required
y int

Vertical position (0 = top).

required

Returns:

Type Description
float

Alpha value as float 0.0-1.0 (0 = transparent, 1 = opaque).

resize

resize(width: int, height: int) -> Image

Resize to exact dimensions.

May distort aspect ratio. For aspect-preserving resize, use fit().

Parameters:

Name Type Description Default
width int

Target width.

required
height int

Target height.

required

Returns:

Type Description
Image

A new Image with the specified dimensions.

fit

fit(width: int, height: int) -> Image

Resize to fit within bounds, preserving aspect ratio.

The result will be at most width x height, but may be smaller in one dimension to maintain proportions.

Parameters:

Name Type Description Default
width int

Maximum width.

required
height int

Maximum height.

required

Returns:

Type Description
Image

A new Image that fits within the bounds.

quantize

quantize(cols: int | None = None, rows: int | None = None) -> Image

Resize to a specific grid size.

Useful for creating dot art with a specific number of dots. If only cols or rows is specified, the other is calculated to maintain aspect ratio.

Parameters:

Name Type Description Default
cols int | None

Target number of columns (width in pixels).

None
rows int | None

Target number of rows (height in pixels).

None

Returns:

Type Description
Image

A new Image with the specified grid dimensions.

downscale

downscale(factor: int) -> Image

Downscale the image by an integer factor.

Uses averaging for smooth results.

Parameters:

Name Type Description Default
factor int

Downscale factor (e.g., 2 = half size).

required

Returns:

Type Description
Image

A new, smaller Image.

See also

For image-to-art workflows, see Image to Art.


Layer

Layer(data: ndarray)

A 2D matrix of values representing one channel of an image.

Layers are the building blocks of images in PyFreeform. Each layer holds a single channel of data (e.g., red, green, blue, alpha, brightness).

Attributes:

Name Type Description
data ndarray

The underlying numpy array

width int

Matrix width (number of columns)

height int

Matrix height (number of rows)

Example
layer = Layer(np.zeros((100, 100)))
layer.width, layer.height  # (100, 100)
layer[50, 50]              # 0.0

Create a layer from a numpy array.

Parameters:

Name Type Description Default
data ndarray

A 2D numpy array of values.

required

Raises:

Type Description
ValueError

If data is not 2-dimensional.

width property

width: int

Matrix width (number of columns).

height property

height: int

Matrix height (number of rows).

A single-channel grayscale array. Access values with layer[x, y] (returns 0–255).


Utility Functions

map_range

map_range(value: float, in_min: float = 0, in_max: float = 1, out_min: float = 0, out_max: float = 1, clamp: bool = False) -> float

Convert a value from one range to another.

Think of it like converting between units. If brightness goes from 0 to 1 but you want a radius between 2 and 10, this does the conversion for you:

radius = map_range(brightness, 0, 1, 2, 10)
# brightness 0.0 → radius 2
# brightness 0.5 → radius 6
# brightness 1.0 → radius 10

Swap the output range to reverse the direction:

radius = map_range(brightness, 0, 1, 10, 2)
# brightness 0.0 → radius 10  (dark = big)
# brightness 1.0 → radius 2   (bright = small)

Parameters:

Name Type Description Default
value float

The input value to convert.

required
in_min float

Start of the input range (default: 0).

0
in_max float

End of the input range (default: 1).

1
out_min float

Start of the output range (default: 0).

0
out_max float

End of the output range (default: 1).

1
clamp bool

If True, keep the result within the output range.

False
radius = map_range(cell.brightness, 0, 1, 2, 10)
# brightness 0.0 → radius 2, brightness 1.0 → radius 10

# Swap output range to reverse direction
radius = map_range(cell.brightness, 0, 1, 10, 2)  # dark = big, bright = small

apply_brightness

apply_brightness(color: ColorLike, brightness: float) -> str

Apply a brightness multiplier to a color.

Scales each RGB channel by brightness (0.0 = black, 1.0 = unchanged).

Parameters:

Name Type Description Default
color ColorLike

Any supported color format (name, hex, or RGB tuple).

required
brightness float

Multiplier from 0.0 (black) to 1.0 (unchanged).

required

Returns:

Type Description
str

Hex color string with brightness applied.

Example
apply_brightness("coral", 0.5)   # half-bright coral
apply_brightness("white", 0.0)   # '#000000'
apply_brightness((255, 0, 0), 1) # '#ff0000'
apply_brightness("coral", 0.5)    # Half-bright coral → "#7f3f28"
apply_brightness("white", 0.0)    # Pure black → "#000000"
apply_brightness((255, 0, 0), 1)  # Unchanged red → "#ff0000"

gray

gray(brightness: float) -> str

Create a grayscale color from a brightness value.

Shorthand for apply_brightness("white", brightness).

Parameters:

Name Type Description Default
brightness float

0.0 (black) to 1.0 (white).

required

Returns:

Type Description
str

Hex color string.

Example
gray(0.0)   # '#000000'
gray(0.5)   # '#808080'
gray(1.0)   # '#ffffff'
gray(0.0)   # "#000000" (black)
gray(0.5)   # "#808080" (mid-gray)
gray(1.0)   # "#ffffff" (white)

get_angle_at

get_angle_at(path: Pathable, t: float, epsilon: float = 1e-05) -> float

Get tangent angle in degrees at parameter t along a path.

If path implements FullPathable, uses angle_at() directly. Otherwise falls back to numeric differentiation.

Parameters:

Name Type Description Default
path Pathable

Any Pathable object.

required
t float

Parameter from 0.0 to 1.0.

required
epsilon float

Step size for numeric differentiation.

1e-05

Returns:

Type Description
float

Angle in degrees (0 = rightward, 90 = downward in SVG coords).

display

Display utilities for PyFreeform.

display

display(target: Scene | str | Path) -> None

Display an SVG in the default web browser.

Can display either a Scene (renders to temp file first) or an existing SVG file.

Parameters:

Name Type Description Default
target Scene | str | Path

A Scene to render, or a path to an existing SVG file.

required
Example
from pyfreeform import Scene, Dot, display
scene = Scene(200, 200)
scene.add(Dot(100, 100, radius=50, color="coral"))  # Dot() uses pixels
display(scene)  # Opens in browser

display("my_art.svg")  # Opens existing file