Skip to content

Styling & Caps

Colors, opacity, style classes, palettes, and the cap system for line endpoints.

See also

For creative styling techniques and palette usage, see Colors, Styles, and Palettes.


The Color Parameter Split

Critical API distinction: fill= vs color=

This is the most common source of errors. Using the wrong parameter will raise a TypeError.

Parameter Used by
color= Dot, Line, Curve, Text, add_dot, add_line, add_curve, add_text, add_fill, add_border, all style classes
fill= Rect, Ellipse, Polygon, add_rect, add_ellipse, add_polygon, Path (closed)

ShapeStyle.color maps to fill= when applied to shapes.

Color Formats

Anywhere a color is accepted (ColorLike), you can use:

  • Named colors: "red", "coral", "navy", etc.
  • Hex: "#ff0000", "#f00", "#FF0000"
  • RGB tuple: (255, 0, 0)

All paint parameters also accept gradient objects. See Gradients below.

Opacity System

  • opacity on every entity and style: 0.0 (transparent) to 1.0 (opaque). Default 1.0 emits no SVG attribute.
  • fill_opacity / stroke_opacity on shapes (Rect, Ellipse, Polygon, Path, ShapeStyle): optional overrides for independent control.
  • Simple entities (Dot, Line, Curve, Text, Connection): SVG opacity attribute.
  • Shapes: SVG fill-opacity + stroke-opacity attributes.

Brightness System

Scale any color toward black with a 0–1 multiplier (0.0 = black, 1.0 = unchanged). Mirrors the opacity pattern:

  • color_brightness on color= entities and styles (Dot, Line, Curve, Text, Fill, Border, Connection, and their style classes).
  • fill_brightness / stroke_brightness on shapes (Rect, Ellipse, Polygon) and ShapeStyle — independent per channel, just like fill_opacity/stroke_opacity.

Brightness is applied before rendering. The original color is multiplied by the brightness value:

cell.add_dot(color="coral", color_brightness=0.5)  # Half-bright coral
cell.add_ellipse(fill="gold", fill_brightness=cell.brightness, stroke_brightness=1.0)

Standalone Functions

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'

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'

Gradients

Use LinearGradient or RadialGradient anywhere a color is accepted. See the Gradients guide for visual examples.

LinearGradient

LinearGradient(*stops: _StopInput, angle: float = 0, x1: float | None = None, y1: float | None = None, x2: float | None = None, y2: float | None = None, spread_method: str = 'pad', gradient_units: str = 'objectBoundingBox')

Bases: Gradient

A linear gradient that transitions colors along a line.

Example::

# Simple left-to-right
LinearGradient("red", "blue")

# 45-degree angle
LinearGradient("red", "blue", angle=45)

# Explicit coordinates
LinearGradient("red", "blue", x1=0, y1=0, x2=1, y2=1)

# With explicit stop offsets
LinearGradient(("red", 0.0), ("gold", 0.3), ("blue", 1.0))

Create a linear gradient.

Parameters:

Name Type Description Default
*stops _StopInput

Color stops — see module docstring for accepted formats.

()
angle float

Direction in degrees (0 = right, 90 = down). Ignored when explicit coordinates are given.

0
x1 float | None

Start point x (fraction 0-1 for objectBoundingBox).

None
y1 float | None

Start point y.

None
x2 float | None

End point x.

None
y2 float | None

End point y.

None
spread_method str

'pad', 'reflect', or 'repeat'.

'pad'
gradient_units str

'objectBoundingBox' or 'userSpaceOnUse'.

'objectBoundingBox'

RadialGradient

RadialGradient(*stops: _StopInput, cx: float = 0.5, cy: float = 0.5, r: float = 0.5, fx: float | None = None, fy: float | None = None, fr: float = 0, spread_method: str = 'pad', gradient_units: str = 'objectBoundingBox')

Bases: Gradient

A radial gradient that radiates colors from a center point.

Example::

# Simple center-out
RadialGradient("white", "black")

# Off-center focal point
RadialGradient("white", "black", fx=0.3, fy=0.3)

# Custom center and radius
RadialGradient("red", "blue", cx=0.2, cy=0.2, r=0.8)

Create a radial gradient.

Parameters:

Name Type Description Default
*stops _StopInput

Color stops — see module docstring for accepted formats.

()
cx float

Center x of the end circle (fraction 0-1).

0.5
cy float

Center y of the end circle.

0.5
r float

Radius of the end circle.

0.5
fx float | None

Focal point x (center of start circle). Defaults to cx.

None
fy float | None

Focal point y. Defaults to cy.

None
fr float

Radius of the start circle (default 0).

0
spread_method str

'pad', 'reflect', or 'repeat'.

'pad'
gradient_units str

'objectBoundingBox' or 'userSpaceOnUse'.

'objectBoundingBox'

Gradient

Gradient(*stops: _StopInput, spread_method: str = 'pad', gradient_units: str = 'objectBoundingBox')

Bases: ABC

Base class for SVG gradient paint servers.

Gradients can be used anywhere a color is accepted (fill=, stroke=, color=). They are emitted as <defs> entries and referenced via fill="url(#id)".

gradient_id property

gradient_id: str

Deterministic SVG id for this gradient.

stops property

stops: tuple[GradientStop, ...]

The normalized color stops.

to_svg_ref

to_svg_ref() -> str

Return the SVG paint reference string, e.g. url(#grad-abc).

to_svg_def abstractmethod

to_svg_def() -> str

Render the full <linearGradient> or <radialGradient> element.

Color Stops

A gradient is built from color stops — each stop says "this color, at this position." The position (offset) is a number from 0.0 (start) to 1.0 (end). The gradient smoothly blends between consecutive stops.

For example, LinearGradient("red", "blue") creates two stops: red at 0.0 and blue at 1.0. Adding more colors adds more stops, evenly spaced by default. Use tuples to control placement: ("red", 0.7) puts red at the 70% mark.

You can also set per-stop opacity with a third value: ("white", 0.0, 0.5) means white at the start, 50% transparent.

GradientStop dataclass

GradientStop(color: str, offset: float, opacity: float = 1.0)

A single color stop in a gradient.

Attributes:

Name Type Description
color str

Hex color string (already normalized).

offset float

Position along the gradient (0.0 to 1.0).

opacity float

Stop opacity (0.0 to 1.0, default 1.0).

to_svg
to_svg() -> str

Render as an SVG <stop> element.


Style Classes

All 5 style classes are dataclasses. Use dataclasses.replace() for immutable updates:

from dataclasses import replace

base_style = PathStyle(width=2, color="coral")
thick_style = replace(base_style, width=4)
arrow_style = replace(base_style, end_cap="arrow")

FillStyle dataclass

FillStyle(color: PaintLike = 'black', z_index: int = 0, opacity: float = 1.0, color_brightness: float | None = None)

Configuration for simple color fills (dots, backgrounds).

Use with cell.add_dot() or cell.add_fill():

style = FillStyle(color="coral")
cell.add_dot(radius=0.05, style=style)
cell.add_fill(style=style)

Attributes:

Name Type Description
color PaintLike

Fill color as hex, name, or RGB tuple (default: "black")

z_index int

Layer order - higher renders on top (default: 0)

opacity float

Opacity 0.0-1.0 (default: 1.0, fully opaque)

to_kwargs

to_kwargs() -> dict[str, Any]

Convert to keyword arguments for add_dot() / add_fill().

PathStyle dataclass

PathStyle(width: float = 1, color: PaintLike = 'black', z_index: int = 0, cap: CapName = 'round', start_cap: CapName | None = None, end_cap: CapName | None = None, opacity: float = 1.0, color_brightness: float | None = None)

Configuration for lines, curves, connections, and paths.

Use with cell.add_line(), cell.add_curve(), entity.connect(), etc.:

style = PathStyle(width=2, color="navy")
cell.add_diagonal(style=style)

# Arrow cap on one end
style = PathStyle(width=2, end_cap="arrow")
cell.add_line(start="left", end="right", style=style)

# Connections
dot1.connect(dot2, style=PathStyle(width=2, color="red", end_cap="arrow"))

Attributes:

Name Type Description
width float

Stroke width in pixels (default: 1)

color PaintLike

Stroke color (default: "black")

z_index int

Layer order (default: 0)

cap CapName

Line cap style applied to both ends (default: "round")

start_cap CapName | None

Override cap for the start end (default: None, uses cap)

end_cap CapName | None

Override cap for the end end (default: None, uses cap)

opacity float

Opacity 0.0-1.0 (default: 1.0, fully opaque)

to_kwargs

to_kwargs() -> dict[str, Any]

Convert to keyword arguments for add_line() / add_curve() / Connection().

BorderStyle dataclass

BorderStyle(width: float = 0.5, color: PaintLike = '#cccccc', z_index: int = 0, opacity: float = 1.0, color_brightness: float | None = None)

Configuration for borders and outlines.

Use with cell.add_border():

style = BorderStyle(width=1, color="gray")
cell.add_border(style=style)

Attributes:

Name Type Description
width float

Stroke width in pixels (default: 0.5)

color PaintLike

Stroke color (default: "#cccccc")

z_index int

Layer order (default: 0)

opacity float

Opacity 0.0-1.0 (default: 1.0, fully opaque)

to_kwargs

to_kwargs() -> dict[str, Any]

Convert to keyword arguments for add_border().

ShapeStyle dataclass

ShapeStyle(color: PaintLike = 'black', stroke: PaintLike | None = None, stroke_width: float = 1, z_index: int = 0, opacity: float = 1.0, fill_opacity: float | None = None, stroke_opacity: float | None = None, fill_brightness: float | None = None, stroke_brightness: float | None = None)

Configuration for filled shapes (Rect, Ellipse, Polygon).

Use with cell.add_ellipse() or cell.add_polygon():

style = ShapeStyle(color="coral", stroke="navy", stroke_width=2)
cell.add_ellipse(style=style)
cell.add_polygon(Polygon.hexagon(), style=style)

Note: color maps to fill at the entity level.

Attributes:

Name Type Description
color PaintLike

Fill color (default: "black")

stroke PaintLike | None

Stroke color (default: None for no stroke)

stroke_width float

Stroke width in pixels (default: 1)

z_index int

Layer order (default: 0)

opacity float

Opacity for both fill and stroke 0.0-1.0 (default: 1.0)

fill_opacity float | None

Override opacity for fill only (default: None, uses opacity)

stroke_opacity float | None

Override opacity for stroke only (default: None, uses opacity)

to_kwargs

to_kwargs() -> dict[str, Any]

Convert to keyword arguments for add_ellipse() / add_polygon() / add_rect().

TextStyle dataclass

TextStyle(color: PaintLike = 'black', font_family: str = 'sans-serif', bold: bool = False, italic: bool = False, text_anchor: str = 'middle', baseline: str = 'middle', rotation: float = 0, z_index: int = 0, opacity: float = 1.0, color_brightness: float | None = None)

Configuration for text appearance.

Use with cell.add_text():

style = TextStyle(color="navy", bold=True)
cell.add_text("Hello", font_size=0.20, style=style)

Attributes:

Name Type Description
color PaintLike

Text color (default: "black")

font_family str

Font family (default: "sans-serif")

bold bool

Bold text (default: False)

italic bool

Italic text (default: False)

text_anchor str

Horizontal alignment (default: "middle")

baseline str

Vertical alignment (default: "middle")

rotation float

Rotation in degrees (default: 0)

z_index int

Layer order (default: 0)

opacity float

Opacity 0.0-1.0 (default: 1.0, fully opaque)

to_kwargs

to_kwargs() -> dict[str, Any]

Convert to keyword arguments for add_text().


Palette dataclass

Palette(background: str = '#ffffff', primary: str = '#000000', secondary: str = '#666666', accent: str = '#ff0000', line: str = '#333333', grid: str = '#cccccc')

A curated color palette for consistent, beautiful artwork.

Palettes provide named colors for different purposes: - background: Scene background color - primary: Main element color (dots, fills) - secondary: Supporting element color - accent: Highlight color for emphasis - line: Color for lines and connections - grid: Color for grid outlines and borders

Use pre-built palettes or create custom ones:

# Pre-built
colors = Palette.midnight()
colors = Palette.sunset()

# Custom
colors = Palette(
    background="#1a1a2e",
    primary="#ff6b6b",
    secondary="#4ecdc4",
)

# Access colors
scene.background = colors.background
cell.add_dot(color=colors.primary)

Attributes:

Name Type Description
background str

Scene/canvas background color

primary str

Main element color

secondary str

Supporting element color

accent str

Highlight/emphasis color

line str

Line and connection color

grid str

Grid outline and border color

midnight classmethod

midnight() -> Palette

Dark blue theme with coral accent.

Perfect for dramatic, high-contrast art pieces.

sunset classmethod

sunset() -> Palette

Warm oranges and purples.

Evokes warmth and energy.

ocean classmethod

ocean() -> Palette

Cool blues and teals.

Calm, serene aesthetic.

forest classmethod

forest() -> Palette

Natural greens and earth tones.

Organic, grounded feel.

monochrome classmethod

monochrome() -> Palette

Black, white, and grays.

Classic, elegant simplicity.

paper classmethod

paper() -> Palette

Light theme with paper-like background.

Clean, minimalist aesthetic.

neon classmethod

neon() -> Palette

Vibrant neon colors on dark background.

Bold, electric energy.

pastel classmethod

pastel() -> Palette

Soft pastel colors.

Gentle, approachable aesthetic.

Palette Background Vibe
Palette.midnight() #1a1a2e Dark blue with coral accent
Palette.sunset() #2d1b4e Warm oranges and purples
Palette.ocean() #0a1628 Cool blues and teals
Palette.forest() #1a2e1a Natural greens and earth
Palette.monochrome() #0a0a0a Black, white, grays
Palette.paper() #fafafa Light, clean, minimalist
Palette.neon() #0d0d0d Vibrant neon electric
Palette.pastel() #fef6e4 Soft, gentle pastels

Cap System

Line, Curve, Path, and Connection endpoints support caps. All cap parameters are typed as CapName, so your IDE will autocomplete the available options.

from pyfreeform import CapName  # Literal["butt", "round", "square", "arrow", "arrow_in", "diamond"]

SVG provides three native caps ("round", "square", "butt"). PyFreeform extends this with marker-based caps that use SVG <marker> elements.

Built-in Cap Type Description
"round" SVG native Semicircle extending past the endpoint
"square" SVG native Rectangle extending past the endpoint
"butt" SVG native Flat end, flush with the endpoint
"arrow" Marker Arrowhead pointing away from the path
"arrow_in" Marker Arrowhead pointing into the path
"diamond" Marker Diamond shape centered on the endpoint

Per-end caps: start_cap and end_cap override the base cap:

line = cell.add_line(start="left", end="right", cap="round", end_cap="arrow")
line.effective_start_cap  # "round" (inherited from cap)
line.effective_end_cap    # "arrow" (overridden)

Creating Custom Caps

A cap shape is just a list of (x, y) vertices in a 10x10 grid. No SVG knowledge needed.

cap_shape

cap_shape(vertices: Sequence[tuple[float, float]], *, tip: tuple[float, float] = (10, 5), view_size: int = 10) -> Callable[[str, str, float], str]

Create a cap generator from a list of vertices and a tip position.

Each vertex is an (x, y) point in a view_size x view_size coordinate space (default 10x10). The vertices are joined into a closed polygon automatically.

Parameters:

Name Type Description Default
vertices Sequence[tuple[float, float]]

Points forming the cap shape, e.g. [(0, 0), (10, 5), (0, 10)] for a right-pointing arrow.

required
tip tuple[float, float]

(x, y) point where the cap attaches to the stroke endpoint. For a right-pointing arrow this is the right edge (10, 5); for a left-pointing one (0, 5).

(10, 5)
view_size int

Size of the coordinate space (default 10).

10

Returns:

Type Description
Callable[[str, str, float], str]

A generator function (marker_id, color, size) -> str that

Callable[[str, str, float], str]

produces a complete SVG <marker> element.

Example
register_cap("diamond", cap_shape(
    [(5, 0), (10, 5), (5, 10), (0, 5)],
    tip=(5, 5),
))

register_cap

register_cap(name: str, generator: Callable[[str, str, float], str], *, start_generator: Callable[[str, str, float], str] | None = None) -> None

Register a new marker-based cap type.

Use cap_shape() to create the generator without writing raw SVG.

Parameters:

Name Type Description Default
name str

Cap name (e.g. "arrow", "diamond").

required
generator Callable[[str, str, float], str]

Function (marker_id, color, size) -> svg_string (used for marker-end).

required
start_generator Callable[[str, str, float], str] | None

Optional separate generator for marker-start. If provided, the start marker uses an explicitly reversed shape instead of relying on SVG2 orient="auto-start-reverse".

None
Example
from pyfreeform import cap_shape, register_cap

register_cap("diamond", cap_shape(
    [(5, 0), (10, 5), (5, 10), (0, 5)],
    tip=(5, 5),
))

Tip position controls alignment -- the point on your shape that sits exactly at the stroke endpoint:

  • (10, 5) -- right edge, center height (right-pointing arrow tip)
  • (0, 5) -- left edge, center height (left-pointing arrow tip)
  • (5, 5) -- dead center (symmetric shapes like diamonds)

Directional caps need a separate reversed shape for the start end. Symmetric caps only need one shape:

# Symmetric -- same shape in both directions
register_cap("diamond", cap_shape(DIAMOND, tip=(5, 5)))

# Directional -- separate start/end shapes
register_cap(
    "arrow",
    cap_shape([(0, 0), (10, 5), (0, 10)], tip=(10, 5)),                 # end: points outward
    start_generator=cap_shape([(10, 0), (0, 5), (10, 10)], tip=(0, 5)),  # start: points outward
)