Zone-Aware Spatial Logic in Autonomous Art Systems
Beyond Random Placement
Anti-clustering prevents elements from bunching together, but it does not understand composition. A well-composed artwork has zones: focal areas that draw the eye, margins that provide breathing room, and transition regions that connect different visual elements. An autonomous art system needs to understand and respect these zones.
I built a zone-aware spatial logic system for an autonomous art generator, and it transformed the output from interesting experiments into genuinely composed artwork.
Defining the Zone System
I model the canvas as a collection of overlapping zones, each with its own rules for element placement.
from dataclasses import dataclass, field
from enum import Enum
class ZoneType(Enum):
FOCAL = "focal" # Primary attention area
SECONDARY = "secondary" # Supporting visual area
MARGIN = "margin" # Edge buffer zone
TRANSITION = "transition" # Between zones
BACKGROUND = "background" # Fill area
@dataclass
class Zone:
name: str
zone_type: ZoneType
bounds: tuple[float, float, float, float] # x, y, width, height
max_elements: int = 50
min_element_size: float = 5.0
max_element_size: float = 50.0
density: float = 0.5 # 0-1, how packed elements can be
allowed_categories: list[str] = field(default_factory=list)
priority: int = 1 # Higher = placed first
def create_rule_of_thirds_zones(canvas_w: float, canvas_h: float) -> list[Zone]:
"""Create zones based on the rule of thirds."""
third_w = canvas_w / 3
third_h = canvas_h / 3
zones = [
# Margin zone
Zone(
name="margin",
zone_type=ZoneType.MARGIN,
bounds=(0, 0, canvas_w, canvas_h),
max_elements=5,
density=0.1,
priority=0
),
# Four focal points at thirds intersections
Zone(
name="focal_top_left",
zone_type=ZoneType.FOCAL,
bounds=(third_w - 30, third_h - 30, 60, 60),
max_elements=3,
min_element_size=20,
max_element_size=80,
density=0.8,
priority=3
),
Zone(
name="focal_top_right",
zone_type=ZoneType.FOCAL,
bounds=(2 * third_w - 30, third_h - 30, 60, 60),
max_elements=3,
min_element_size=20,
max_element_size=80,
density=0.8,
priority=3
),
# Secondary zones between focal points
Zone(
name="secondary_top",
zone_type=ZoneType.SECONDARY,
bounds=(third_w, 0, third_w, third_h),
max_elements=10,
density=0.4,
priority=2
)
]
return zones
The Zone-Aware Placement Engine
The placement engine respects zone boundaries and rules when positioning elements on the canvas.
class ZoneAwarePlacer:
def __init__(self, zones: list[Zone], canvas_size: tuple[float, float]):
self.zones = sorted(zones, key=lambda z: z.priority, reverse=True)
self.canvas_w, self.canvas_h = canvas_size
self.placements: list[dict] = []
self.zone_counts: dict[str, int] = {z.name: 0 for z in zones}
def place_element(self, element: dict) -> dict | None:
"""Find a valid placement for the element respecting zone rules."""
for zone in self.zones:
if not self._element_fits_zone(element, zone):
continue
position = self._find_position_in_zone(element, zone)
if position:
placement = {
"element": element,
"x": position[0],
"y": position[1],
"zone": zone.name
}
self.placements.append(placement)
self.zone_counts[zone.name] += 1
return placement
return None
def _element_fits_zone(self, element: dict, zone: Zone) -> bool:
if self.zone_counts[zone.name] >= zone.max_elements:
return False
size = element.get("size", 10)
if size < zone.min_element_size or size > zone.max_element_size:
return False
if zone.allowed_categories and element.get("category") not in zone.allowed_categories:
return False
return True
def _find_position_in_zone(
self, element: dict, zone: Zone, max_attempts: int = 50
) -> tuple[float, float] | None:
zx, zy, zw, zh = zone.bounds
min_dist = element.get("size", 10) * (2.0 - zone.density)
for _ in range(max_attempts):
x = np.random.uniform(zx, zx + zw)
y = np.random.uniform(zy, zy + zh)
# Check margin constraints
if zone.zone_type != ZoneType.MARGIN:
margin = 20
if x < margin or x > self.canvas_w - margin:
continue
if y < margin or y > self.canvas_h - margin:
continue
# Check distance from existing placements
if self._check_spacing(x, y, min_dist):
return (x, y)
return None
def _check_spacing(self, x: float, y: float, min_dist: float) -> bool:
for p in self.placements:
dist = np.sqrt((x - p["x"]) ** 2 + (y - p["y"]) ** 2)
if dist < min_dist:
return False
return True
Dynamic Zone Weights
Static zones work for simple compositions, but dynamic weight adjustment creates more interesting results. I adjust zone attractiveness based on what has already been placed.
class DynamicZoneWeighter:
def __init__(self, zones: list[Zone]):
self.zones = zones
self.base_weights = {z.name: z.priority for z in zones}
self.current_weights = dict(self.base_weights)
def update_weights(self, placements: list[dict]):
for zone in self.zones:
count = sum(1 for p in placements if p["zone"] == zone.name)
fill_ratio = count / max(zone.max_elements, 1)
# Reduce weight as zone fills up
self.current_weights[zone.name] = (
self.base_weights[zone.name] * (1 - fill_ratio * 0.8)
)
def select_zone(self) -> Zone:
weights = [self.current_weights[z.name] for z in self.zones]
total = sum(weights)
if total == 0:
return np.random.choice(self.zones)
probabilities = [w / total for w in weights]
return np.random.choice(self.zones, p=probabilities)
Composition Validation
After placing all elements, I run a composition validator that checks the overall balance of the artwork.
class CompositionValidator:
def validate(self, placements: list[dict], canvas_size: tuple) -> dict:
w, h = canvas_size
# Check visual balance (center of mass)
if not placements:
return {"valid": False, "reason": "No placements"}
cx = np.mean([p["x"] for p in placements])
cy = np.mean([p["y"] for p in placements])
center_offset = np.sqrt(
((cx - w/2) / w) ** 2 + ((cy - h/2) / h) ** 2
)
# Check coverage
quadrants = {"TL": 0, "TR": 0, "BL": 0, "BR": 0}
for p in placements:
q = ("T" if p["y"] < h/2 else "B") + ("L" if p["x"] < w/2 else "R")
quadrants[q] += 1
min_quadrant = min(quadrants.values())
max_quadrant = max(quadrants.values())
balance_ratio = min_quadrant / max(max_quadrant, 1)
return {
"valid": center_offset < 0.15 and balance_ratio > 0.3,
"center_offset": round(center_offset, 3),
"balance_ratio": round(balance_ratio, 3),
"quadrant_distribution": quadrants
}
Bringing It All Together
The complete system uses zone definitions to guide placement, dynamic weighting to balance fill rates, anti-clustering for local spacing, and composition validation to ensure the final result is visually balanced.
The beauty of this approach is that it produces artworks that feel intentional and composed, while still maintaining the organic quality that makes generative art interesting. The zones provide structure, and the randomness within those zones provides life.
Zone-aware spatial logic is what separates generative art experiments from generative art systems that produce consistently good output.