Paths & Parametric Positioning¶
This is PyFreeform's "killer feature" — position any entity at any point along any path using the along / t system.
The Core Idea¶
Every Line, Curve, and Ellipse is a Pathable — it has a point_at(t) method where t goes from 0.0 (start) to 1.0 (end). You can place entities along these paths:
- The dot's position slides along the diagonal based on brightness.
Along Curves¶
Curves make the positioning non-linear and organic:
curve = cell.add_curve(start="bottom_left", end="top_right", curvature=0.6, ...)
for t_val in [0.25, 0.5, 0.75]:
cell.add_dot(along=curve, t=t_val, radius=0.10, color=colors.primary)
Along Ellipses¶
Ellipses are closed paths — t=0 is the rightmost point, going counterclockwise:
| t value | Position |
|---|---|
| 0.0 | Right |
| 0.25 | Top |
| 0.5 | Left |
| 0.75 | Bottom |
ellipse = cell.add_ellipse(at="center", rx=0.4, ry=0.25, fill="none", stroke=colors.line)
cell.add_dot(along=ellipse, t=ny, radius=0.10, color=colors.accent)
Understanding t Values¶
On a single path, t selects the position:
Built-in Path Shapes¶
PyFreeform includes four ready-to-use path shapes, accessible as Path.Wave, Path.Spiral, Path.Lissajous, and Path.Zigzag:
from pyfreeform import Path
wave = Path.Wave(start=(cx - 10, cy), end=(cx + 10, cy), amplitude=8, frequency=3)
cell.add_path(wave, segments=32, width=1.5, color=colors.primary)
All four work with add_path(), along=/t= positioning, and as connection shapes. See Connections & Paths for full parameter details.
Filled Closed Paths¶
Use closed=True and fill= to create filled shapes from any path. Layering semi-transparent fills produces striking geometric art:
liss = Path.Lissajous(center=(200, 200), a=3, b=2, size=150)
path = Path(liss, closed=True, fill="#4a90d9", color="#6ab0ff",
width=1.2, fill_opacity=0.25, stroke_opacity=0.7, segments=128)
scene.place(path)
Relative Coordinates¶
Path shapes use pixel coordinates by default. When working inside a cell you usually don't want to multiply by cell.width and cell.height. Pass relative=True to add_path() and write the pathable's coordinates as surface-relative fractions (0.0–1.0) — exactly like every other add_* method:
# Without relative=True — pixel math required
w, h = cell.width, cell.height
cell.add_path(
Path.Wave(start=(w * 0.05, h * 0.5), end=(w * 0.95, h * 0.5), amplitude=h * 0.15),
width=2, color="coral",
)
# With relative=True — fraction-first, no pixel math
cell.add_path(
Path.Wave(start=(0.05, 0.5), end=(0.95, 0.5), amplitude=0.15),
relative=True, width=2, color="coral",
)
# Works for all four built-in shapes
cell.add_path(
Path.Spiral(center=(0.5, 0.5), end_radius=0.38, turns=4),
relative=True, width=1, color="teal",
)
The four built-in shapes already use normalized defaults (Wave() defaults to start=(0,0), end=(1,0)) — relative=True is the bridge that maps those fractions to the cell's actual pixel dimensions at render time.
Use relative=True with path shapes, not with entities
relative=True is designed for Wave, Spiral, Lissajous, and Zigzag. Don't use it with an Ellipse entity as a pathable — the entity already lives in pixel space and would be double-scaled.
Custom Pathables¶
You can also create your own — any object with point_at(t) -> Coord works as a path:
from pyfreeform import Coord
class MyPath:
def point_at(self, t):
x = t * 100
y = 50 + 20 * math.sin(t * math.pi * 4)
return Coord(x, y)
Sub-Paths and Arcs¶
Use start_t and end_t to render only a portion of a path:
cell.add_path(
ellipse,
start_t=0.1, # Start at 10% around
end_t=0.6, # End at 60% around
segments=24,
width=2,
color=colors.primary,
)
Text Along Paths¶
Pass a path to add_text(along=) without t to warp text along the full path:
curve = cell.add_curve(start=(0.05, 0.7), end=(0.95, 0.3), curvature=0.5, ...)
cell.add_text("Text flows along any path", along=curve, font_size=0.05, color=colors.accent)
<textPath>.Alignment¶
Set align=True to rotate entities to follow the path's tangent direction:
cell.add_polygon(
Polygon.triangle(size=0.06),
along=curve, t=0.5, align=True, # (1)!
fill=colors.primary,
)
- The triangle points in the direction the curve is heading at
t=0.5.
Offset¶
Use along_offset to shift an entity perpendicular to the path — useful for labels beside lines:
conn = rect_a.connect(rect_b, style=arrow_style)
scene.add_text("label", along=conn, t=0.5, along_offset=-0.02, font_size=0.015, color="#aaa")
Negative values shift above the line, positive values shift below. This is direction-independent — the same sign always means the same visual side, regardless of which way the path travels.
See also
For the full path and connection API, see Connections & Paths.
What's Next?¶
Explore the shape system and learn to compose reusable groups: