Skip to content

Transforms & Layout

Rotate, scale, fit, connect, and layer entities for precise control over your compositions.

Rotation

Every entity supports rotation as a parameter or .rotate() method:

cell.add_polygon(
    Polygon.square(size=0.6),
    fill=colors.primary,
    rotation=(nx + ny) * 180,
)

Rotation grid

Squares rotate smoothly from 0 to 360 degrees across the grid.

Scaling with fit_to_surface

entity.fit_to_surface(scale) auto-sizes any entity to fit within its surface. scale is the fraction of surface area to fill (0.0 to 1.0). Works with both EntityGroup and Text entities.

By default, fit_to_surface uses bounds(visual=True) so that stroke width is accounted for — stroked entities won't spill beyond surface edges after fitting. Pass visual=False for pure geometric fitting that ignores stroke width:

Scale comparison

The same group at 20%, 40%, 60%, 80%, 90%, and 100% of cell size.

Position-Aware Fitting

Pass at=(rx, ry) to fit at a specific position within the surface:

group.fit_to_surface(0.5, at=(0.15, 0.15))   # Near top-left, constrained by edges
group.fit_to_surface(0.5, at=(0.5, 0.5))     # Centered
group.fit_to_surface(0.5, at=(0.85, 0.85))   # Near bottom-right

fit_to_surface with positions

Same size, different positions — the entity respects surface boundaries.

Rotational Fitting

When a shape doesn't match the surface's aspect ratio, two fitting modes help fill the space:

  • rotate=True — Finds the rotation angle that maximizes fill. Uses a closed-form O(1) solution (3 candidate angles: 0°, 90°, and the optimal balanced angle).
  • match_aspect=True — Rotates so the bounding box matches the surface's proportions. Useful when you want the shape to echo the surface's shape rather than simply be as large as possible.
group = EntityGroup()
group.add(Rect.at_center((0, 0), 70, 14, fill="coral"))
cell.add(group)
group.fit_to_surface(0.85, rotate=True)        # maximize fill
# OR
group.fit_to_surface(0.85, match_aspect=True)  # match surface proportions

Fitting modes comparison

Default fit vs rotate vs match_aspect -- for a wide rect bar (middle row) and the pyfreeform logo (bottom row).

Both modes work on any entity type — EntityGroup, Rect, Polygon, Ellipse, Line, Curve, and Text. Dot is symmetric so rotation is a no-op.

rotate and match_aspect are mutually exclusive — passing both raises ValueError.


Connections

Link entities with Connection objects that auto-update when entities move. Connections are covered in depth in Connections & Anchors.

Connected network

Dots connected in a grid network — connections link alternate cells.

z_index Layering

Control draw order with z_index — higher values render on top:

cell.add_fill(color="navy", z_index=0)           # Background
cell.add_ellipse(fill="coral", opacity=0.2, z_index=1)  # Behind
cell.add_polygon(Polygon.hexagon(), fill="gold", z_index=2)  # Middle
cell.add_dot(color="white", z_index=3)            # On top

z_index layering

Four distinct layers: grid lines → ellipse → hexagon → dot.

map_range Utility

map_range() converts a value from one range to another — like converting between units. If a cell's horizontal position (nx) goes from 0 to 1 but you want a radius between 2 and 9:

from pyfreeform import map_range

radius = map_range(nx, 0, 1, 2, 9)
# nx=0.0 → radius 2    (left edge: small)
# nx=0.5 → radius 5.5  (middle: medium)
# nx=1.0 → radius 9    (right edge: large)

rotation = map_range(ny, 0, 1, 0, 90)          # vertical position → rotation
opacity = map_range(nx + ny, 0, 2, 0.3, 1.0)   # diagonal position → opacity

map_range demonstration

Diamonds where size, rotation, and opacity are all driven by mapped position values.

Swap the output range to reverse the direction — map_range(nx, 0, 1, 9, 2) makes the left edge large and the right edge small.


See also

For the full transforms API, see Transforms.

What's Next?

Learn how connections link entities with live references and explore the anchor system:

← Text & Typography Connections & Anchors →