| 4 min read

Zone-Aware Spatial Logic in Autonomous Art Systems

autonomous art spatial logic generative art algorithms Python creative coding

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.