Skip to content

Your First Artwork

PyFreeform gives you two ways to create art: from an image or from scratch. Both use the same powerful grid system.

Load an Image

Scene.from_image() divides a photo into a grid of cells. Each cell knows the color and brightness of the pixels beneath it.

from pyfreeform import Scene

scene = Scene.from_image("MonaLisa.jpg", grid_size=35, cell_size=12)

for cell in scene.grid:
    cell.add_dot(radius=0.25, color=cell.color)  # (1)!

scene.save("artwork.svg")
  1. cell.color is the hex color sampled from the image at this cell's position.

Step 1: uniform dots with image colors
Every cell gets a dot colored by the image — already recognizable!

Size by Brightness

Make bright areas pop and dark areas recede by tying dot radius to brightness:

for cell in scene.grid:
    radius = cell.brightness * 0.42  # (1)!
    if radius > 0.025:
        cell.add_dot(radius=radius, color=cell.color)
  1. cell.brightness is a float from 0.0 (black) to 1.0 (white).

Step 2: brightness-driven dot sizes
Larger dots in bright areas create depth and dimension.

Polish It

Add a subtle border and vary opacity for a finished look:

scene = Scene.from_image("MonaLisa.jpg", grid_size=40, cell_size=11)

for cell in scene.grid:
    radius = cell.brightness * 0.45
    if radius > 0.027:
        cell.add_dot(
            radius=radius,
            color=cell.color,
            opacity=0.6 + cell.brightness * 0.4,
        )

for cell in scene.grid.border():  # (1)!
    cell.add_border(color="#333344", width=0.3)

scene.save("artwork.svg")
  1. grid.border() selects only the cells on the grid's outer edge.

Step 3: polished with border and opacity
Opacity variation and a border frame give it a gallery-ready feel.

Start with a Grid

Scene.with_grid() creates an empty grid — no image needed. Use position and math to drive the visuals.

from pyfreeform import Scene, Palette

colors = Palette.sunset()
scene = Scene.with_grid(cols=12, rows=12, cell_size=25, background=colors.background)

for cell in scene.grid:
    if (cell.row + cell.col) % 2 == 0:
        cell.add_fill(color=colors.primary, opacity=0.7)
    else:
        cell.add_fill(color=colors.secondary, opacity=0.3)

scene.save("pattern.svg")

Step 1: checkerboard fills
A warm checkerboard using the Sunset palette.

Add Shapes

Use cell.normalized_position to drive size, rotation, and opacity based on where each cell sits in the grid:

from pyfreeform import Polygon

for cell in scene.grid:
    nx, ny = cell.normalized_position  # (1)!
    t = (nx + ny) / 2
    cell.add_polygon(
        Polygon.diamond(size=0.3 + t * 0.5),
        fill=colors.accent,
        opacity=0.4 + t * 0.6,
        rotation=t * 45,
    )
  1. normalized_position returns (nx, ny) where both values range from 0.0 (top-left) to 1.0 (bottom-right).

Step 2: diamonds growing across the grid
Diamonds that grow and rotate from corner to corner.

Layer Curves for Flow

Stack curves on top for an organic finish:

import math

for cell in scene.grid:
    nx, ny = cell.normalized_position
    curvature = math.sin(nx * math.pi * 2) * 0.6
    cell.add_curve(
        start="left", end="right",
        curvature=curvature,
        width=0.8,
        color=colors.line,
        opacity=0.3 + ny * 0.4,
    )

Step 3: curves layered on diamonds
Curves add rhythm and movement to the geometric base.


What's Next?

You've seen how PyFreeform turns a few lines of code into visual art. Now learn the concepts behind it:

How PyFreeform Works →