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¶
opacityon every entity and style: 0.0 (transparent) to 1.0 (opaque). Default 1.0 emits no SVG attribute.fill_opacity/stroke_opacityon shapes (Rect, Ellipse, Polygon, Path, ShapeStyle): optional overrides for independent control.- Simple entities (Dot, Line, Curve, Text, Connection): SVG
opacityattribute. - Shapes: SVG
fill-opacity+stroke-opacityattributes.
Brightness System¶
Scale any color toward black with a 0–1 multiplier (0.0 = black, 1.0 = unchanged). Mirrors the opacity pattern:
color_brightnessoncolor=entities and styles (Dot, Line, Curve, Text, Fill, Border, Connection, and their style classes).fill_brightness/stroke_brightnesson shapes (Rect, Ellipse, Polygon) andShapeStyle— independent per channel, just likefill_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 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. |
gray ¶
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. |
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 ¶
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)".
to_svg_def
abstractmethod
¶
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
¶
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). |
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) |
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 ¶
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) |
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 ¶
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) |
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.
monochrome
classmethod
¶
monochrome() -> Palette
Black, white, and grays.
Classic, elegant simplicity.
| 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.
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.
|
required |
tip
|
tuple[float, float]
|
|
(10, 5)
|
view_size
|
int
|
Size of the coordinate space (default 10). |
10
|
Returns:
| Type | Description |
|---|---|
Callable[[str, str, float], str]
|
A generator function |
Callable[[str, str, float], str]
|
produces a complete SVG |
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 |
required |
start_generator
|
Callable[[str, str, float], str] | None
|
Optional separate generator for |
None
|
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
)