Skip to content

Working with Cells

Every cell in the grid carries data — brightness, color, position — and you use that data to drive your art.

Cell Data Properties

When a scene is created with from_image(), each cell samples the pixels beneath it:

Property Type Range Description
cell.brightness float 0.0 – 1.0 Perceived luminance (0 = black, 1 = white)
cell.color str Hex string Average color as "#rrggbb"
cell.rgb tuple (0-255, 0-255, 0-255) RGB components
cell.alpha float 0.0 – 1.0 Opacity from source image

No image?

Cells from Scene.with_grid() default to brightness 0.5, color "#808080". Use normalized_position, math, or distance_to() instead.

Brightness-Driven Effects

Size by Brightness

The most classic effect — bright areas get larger marks:

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

for cell in scene.grid:
    r = cell.brightness * 0.48
    if r > 0.03:
        cell.add_dot(radius=r, color="#ffffff")

Brightness to radius

White dots sized by brightness — the classic dot art look.

Rotation by Brightness

Drive shape rotation from the image:

for cell in scene.grid:
    rotation = cell.brightness * 90
    size = 0.4 + cell.brightness * 0.4
    cell.add_polygon(
        Polygon.square(size=size),
        fill=cell.color,
        opacity=0.7,
        rotation=rotation,
    )

Brightness-driven rotation

Squares rotate and grow with brightness, colored by the source image.

Color Fill

The most direct approach — fill each cell with its sampled color:

for cell in scene.grid:
    cell.add_fill(color=cell.color)

Color fill mosaic

A gradient image reproduced as a pixelated color mosaic.

Neighbors and Edge Detection

Every cell knows its 8 neighbors:

cell.above          # Cell | None
cell.below          # Cell | None
cell.left           # Cell | None
cell.right          # Cell | None
cell.above_left     # Cell | None  (diagonal)
cell.above_right    # Cell | None
cell.below_left     # Cell | None
cell.below_right    # Cell | None

Comparing a cell's brightness to its neighbors reveals edges:

for cell in scene.grid:
    edge = 0.0
    if cell.right:
        edge += abs(cell.brightness - cell.right.brightness)
    if cell.below:
        edge += abs(cell.brightness - cell.below.brightness)
    edge = min(edge * 3, 1.0)  # Amplify
    if edge > 0.1:
        cell.add_dot(radius=edge * 0.4375, color="#00d9ff", opacity=edge)

Edge detection

Edges glow cyan — only cells where brightness changes sharply get marks.

Position-Based Effects

Distance to a Point

cell.distance_to() measures pixel distance to any cell, point, or coordinate:

center = scene.grid[10, 10]
max_d = center.distance_to(scene.grid[0, 0])

for cell in scene.grid:
    d = cell.distance_to(center)
    t = 1 - (d / max_d)  # 1 at center, 0 at corners
    cell.add_dot(radius=t * 0.4375, color=colors.primary, opacity=0.3 + t * 0.7)

Radial distance effect

Dots fade and shrink with distance from the center cell.

Normalized Position

cell.normalized_position returns (nx, ny) where both range from 0.0 (top-left) to 1.0 (bottom-right):

for cell in scene.grid:
    nx, ny = cell.normalized_position
    size = 0.3 + ny * 0.5   # Grow downward
    # ... color gradient from left to right

Position-based gradient

Diamonds grow downward, colors shift from left to right.

Sub-Cell Sampling

For higher-detail effects, sample the image at multiple points within each cell:

for cell in scene.grid:
    for (rx, ry), pos in [
        ((0.25, 0.25), "top_left"),
        ((0.75, 0.25), "top_right"),
        ((0.25, 0.75), "bottom_left"),
        ((0.75, 0.75), "bottom_right"),
    ]:
        color = cell.sample_hex(rx, ry)      # (1)!
        brightness = cell.sample_brightness(rx, ry)
        cell.add_dot(at=pos, radius=brightness * 0.219, color=color)
  1. sample_hex(rx, ry) reads the pixel at relative position (rx, ry) within the cell. Only works with from_image().

Sub-cell sampling

4 dots per cell, each sampling a different quadrant — effectively 4x resolution.

Filtering with where()

Use grid.where() to select cells by any condition:

# Dark cells get fills, bright cells get stars
for cell in scene.grid.where(lambda c: c.brightness < 0.4):
    cell.add_fill(color=cell.color, opacity=0.5)

for cell in scene.grid.where(lambda c: c.brightness >= 0.4):
    cell.add_polygon(
        Polygon.star(points=4, size=0.3 + cell.brightness * 0.5),
        fill=cell.color, opacity=0.7,
    )

Where threshold

Different treatments for dark (fills) and bright (stars) cells.

What's Next?

You've mastered reading cell data. Now learn all the entity types you can place in cells:

Drawing with Entities →