Generative Art with Python: From Random Seeds to SVG Masterpieces

The Quantum Genesis collection is 100 pieces of abstract art, each generated by a Python script fed with random data from real quantum computers. No human picked the colors. No artist arranged the shapes. The quantum measurements from IBM's ibm_fez and Origin Quantum's WK_C180 processors determined everything.

This post is a complete walkthrough of how we built the art generator. By the end, you'll understand every layer of the composition and have enough code to build your own generative art system.

Quantum Genesis NFT #12 - generative abstract art from Origin Quantum measurements

Quantum Genesis #12 — Origin Quantum WK_C180. Every color, shape, and curve was determined by quantum measurements.

Architecture: Seeds to SVG

The pipeline has four stages:

Quantum Computer → Raw Measurements (4096 shots)
    → SHA-256 Hash (256-bit seed)
        → Python QuantumRNG (deterministic from seed)
            → SVG Artwork (layered composition)

The quantum computer produces a probability distribution from 4096 measurement shots. We hash this distribution into a 256-bit seed. From that seed, a deterministic pseudo-random number generator produces all the random values needed for the artwork. This means the art is reproducible from the seed — but the seed itself is unreproducible, because it came from quantum measurements.

Color Theory: HSL and Harmony Types

Colors make or break generative art. Random RGB values produce muddy, clashing palettes. Instead, we work in HSL (Hue, Saturation, Lightness) and use established color harmony rules to generate palettes that are visually pleasing.

We implemented five harmony types:

class ColorHarmony:
    """Generate harmonious color palettes from a base hue."""

    @staticmethod
    def complementary(base_hue: float) -> list[float]:
        """Two colors opposite on the wheel (180° apart)."""
        return [base_hue, (base_hue + 180) % 360]

    @staticmethod
    def analogous(base_hue: float) -> list[float]:
        """Three colors adjacent on the wheel (±30°)."""
        return [
            (base_hue - 30) % 360,
            base_hue,
            (base_hue + 30) % 360
        ]

    @staticmethod
    def triadic(base_hue: float) -> list[float]:
        """Three colors equally spaced (120° apart)."""
        return [
            base_hue,
            (base_hue + 120) % 360,
            (base_hue + 240) % 360
        ]

    @staticmethod
    def split_complementary(base_hue: float) -> list[float]:
        """Base + two colors adjacent to its complement."""
        return [
            base_hue,
            (base_hue + 150) % 360,
            (base_hue + 210) % 360
        ]

    @staticmethod
    def tetradic(base_hue: float) -> list[float]:
        """Four colors forming a rectangle on the wheel."""
        return [
            base_hue,
            (base_hue + 90) % 360,
            (base_hue + 180) % 360,
            (base_hue + 270) % 360
        ]

The quantum seed determines which harmony type to use and what the base hue is. Saturation and lightness are varied per element but kept within ranges that avoid washed-out or overly dark colors:

# Saturation: 50-90% (vibrant but not neon)
# Lightness: 35-75% (visible against both light and dark backgrounds)
saturation = 50 + rng.random() * 40
lightness = 35 + rng.random() * 40

The QuantumRNG Class

This is the bridge between quantum data and art. It's a deterministic PRNG seeded by the quantum measurement hash:

import hashlib
import struct

class QuantumRNG:
    """Deterministic RNG seeded by quantum measurement data."""

    def __init__(self, seed_hex: str):
        """
        Args:
            seed_hex: 64-char hex string (SHA-256 of quantum measurements)
        """
        self.seed = int(seed_hex, 16)
        self.state = self.seed
        self.calls = 0

    def random(self) -> float:
        """Return a float in [0, 1)."""
        # xorshift128+ algorithm
        self.state ^= (self.state << 13) & ((1 << 256) - 1)
        self.state ^= (self.state >> 17)
        self.state ^= (self.state << 5) & ((1 << 256) - 1)
        self.calls += 1
        # Take lower 53 bits for double precision
        return (self.state & ((1 << 53) - 1)) / (1 << 53)

    def randint(self, a: int, b: int) -> int:
        """Return integer in [a, b] inclusive."""
        return a + int(self.random() * (b - a + 1))

    def choice(self, items: list):
        """Pick a random element from a list."""
        return items[self.randint(0, len(items) - 1)]

    def gauss(self, mu: float, sigma: float) -> float:
        """Approximate Gaussian using Box-Muller transform."""
        import math
        u1 = max(self.random(), 1e-10)
        u2 = self.random()
        return mu + sigma * math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2)

The key insight: the RNG is deterministic. Given the same seed, it produces the same sequence of random numbers, which produces the same artwork. But the seed came from a quantum computer, so it's genuinely random and unreproducible.

Setting Up the SVG Canvas

We generate SVG (Scalable Vector Graphics) rather than raster images. SVG is resolution-independent, compresses well, and is easy to generate programmatically:

def create_svg(width: int = 1000, height: int = 1000) -> str:
    """Create the SVG document wrapper."""
    return f'''<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 {width} {height}"
     width="{width}" height="{height}">
  <defs>
    <!-- Gradients and filters defined here -->
  </defs>
  <!-- Layers inserted here -->
</svg>'''

Our canvas is 1000x1000 pixels. The artwork is composed in layers, back to front:

  1. Background gradient
  2. Large geometric shapes (circles, rectangles, polygons)
  3. Bezier curves (flowing organic lines)
  4. Mid-ground elements (smaller shapes, rings)
  5. Particles (tiny dots and marks)
  6. Noise texture overlay

Layer 1: Background and Gradients

The background sets the mood for the entire piece. We use radial and linear gradients with colors from the harmony palette:

def generate_background(rng: QuantumRNG, palette: list) -> str:
    """Generate background with gradient."""
    bg_type = rng.choice(["radial", "linear", "dual"])
    c1 = palette[0]
    c2 = palette[1 % len(palette)]

    if bg_type == "radial":
        cx = 30 + rng.random() * 40  # Center 30-70%
        cy = 30 + rng.random() * 40
        return f'''
        <defs>
          <radialGradient id="bg" cx="{cx}%" cy="{cy}%"
                          r="70%" fx="{cx}%" fy="{cy}%">
            <stop offset="0%" stop-color="hsl({c1},70%,15%)" />
            <stop offset="100%" stop-color="hsl({c2},60%,8%)" />
          </radialGradient>
        </defs>
        <rect width="1000" height="1000" fill="url(#bg)" />'''

    elif bg_type == "linear":
        angle = rng.randint(0, 360)
        return f'''
        <defs>
          <linearGradient id="bg" gradientTransform="rotate({angle})">
            <stop offset="0%" stop-color="hsl({c1},65%,12%)" />
            <stop offset="100%" stop-color="hsl({c2},55%,6%)" />
          </linearGradient>
        </defs>
        <rect width="1000" height="1000" fill="url(#bg)" />'''

    else:  # dual gradient
        return f'''
        <rect width="1000" height="1000" fill="hsl({c1},50%,10%)" />
        <rect width="1000" height="1000"
              fill="hsl({c2},40%,20%)" opacity="0.3"
              rx="200" ry="200" />'''

We keep backgrounds dark (lightness 6-15%) so the vibrant elements on top pop. This is a deliberate design choice — dark backgrounds create drama and make colors feel more intense.

Layer 2: Geometric Shapes

The geometric layer places 5-15 large shapes across the canvas. Each shape has randomized position, size, color, rotation, and opacity:

def generate_shapes(rng: QuantumRNG, palette: list, count: int) -> str:
    """Generate geometric shapes layer."""
    shapes = []
    for _ in range(count):
        shape_type = rng.choice(["circle", "rect", "polygon", "ring"])
        x = rng.random() * 1000
        y = rng.random() * 1000
        hue = rng.choice(palette)
        sat = 50 + rng.random() * 40
        lit = 40 + rng.random() * 30
        opacity = 0.1 + rng.random() * 0.5
        color = f"hsl({hue},{sat}%,{lit}%)"

        if shape_type == "circle":
            r = 30 + rng.random() * 150
            shapes.append(
                f'<circle cx="{x}" cy="{y}" r="{r}" '
                f'fill="{color}" opacity="{opacity:.2f}" />'
            )
        elif shape_type == "rect":
            w = 40 + rng.random() * 200
            h = 40 + rng.random() * 200
            angle = rng.random() * 360
            shapes.append(
                f'<rect x="{x}" y="{y}" width="{w}" height="{h}" '
                f'fill="{color}" opacity="{opacity:.2f}" '
                f'transform="rotate({angle:.1f},{x},{y})" rx="5" />'
            )
        elif shape_type == "polygon":
            sides = rng.randint(3, 8)
            radius = 40 + rng.random() * 120
            points = []
            for i in range(sides):
                angle = (360 / sides) * i + rng.random() * 20
                px = x + radius * math.cos(math.radians(angle))
                py = y + radius * math.sin(math.radians(angle))
                points.append(f"{px:.1f},{py:.1f}")
            shapes.append(
                f'<polygon points="{" ".join(points)}" '
                f'fill="{color}" opacity="{opacity:.2f}" />'
            )
        else:  # ring
            r = 40 + rng.random() * 100
            sw = 2 + rng.random() * 8
            shapes.append(
                f'<circle cx="{x}" cy="{y}" r="{r}" '
                f'fill="none" stroke="{color}" '
                f'stroke-width="{sw:.1f}" opacity="{opacity:.2f}" />'
            )

    return "\n".join(shapes)

The low opacity values (0.1-0.6) are crucial. When semi-transparent shapes overlap, they create emergent colors and depth that neither shape has alone. This layering effect is what gives generative art its characteristic richness.

Quantum Genesis NFT #34 - showing the layered composition technique

Quantum Genesis #34 — IBM Quantum. Notice how overlapping semi-transparent shapes create emergent color interactions.

Layer 3: Bezier Curves and Flow Fields

Bezier curves add organic, flowing quality to the artwork. They're the secret ingredient that makes computer-generated art feel alive rather than rigid:

def generate_bezier_curves(rng: QuantumRNG, palette: list, count: int) -> str:
    """Generate flowing bezier curves."""
    curves = []
    for _ in range(count):
        hue = rng.choice(palette)
        sat = 60 + rng.random() * 30
        lit = 50 + rng.random() * 25
        color = f"hsl({hue},{sat}%,{lit}%)"
        sw = 1 + rng.random() * 4
        opacity = 0.2 + rng.random() * 0.6

        # Start point
        sx, sy = rng.random() * 1000, rng.random() * 1000

        # Build a path with multiple cubic bezier segments
        segments = rng.randint(2, 5)
        path = f"M {sx:.1f} {sy:.1f}"

        cx, cy = sx, sy
        for _ in range(segments):
            # Control point 1: offset from current position
            cp1x = cx + rng.gauss(0, 150)
            cp1y = cy + rng.gauss(0, 150)
            # Control point 2: offset from endpoint
            ex = cx + rng.gauss(0, 200)
            ey = cy + rng.gauss(0, 200)
            cp2x = ex + rng.gauss(0, 100)
            cp2y = ey + rng.gauss(0, 100)

            path += f" C {cp1x:.1f} {cp1y:.1f}, {cp2x:.1f} {cp2y:.1f}, {ex:.1f} {ey:.1f}"
            cx, cy = ex, ey

        curves.append(
            f'<path d="{path}" fill="none" stroke="{color}" '
            f'stroke-width="{sw:.1f}" opacity="{opacity:.2f}" '
            f'stroke-linecap="round" />'
        )

    return "\n".join(curves)

We use Gaussian-distributed offsets (rng.gauss) for control points rather than uniform random. This creates smoother, more natural-looking curves. Uniform random produces sharp zigzags; Gaussian produces gentle flows with occasional dramatic sweeps.

Flow Field Variation

Some NFTs use a flow field approach where curves follow a Perlin-like noise field. Instead of random control points, each curve follows the gradient of a noise function, creating coherent patterns that look like wind or water:

def noise_2d(x: float, y: float, seed: int) -> float:
    """Simple value noise for flow fields."""
    # Hash-based noise (not true Perlin, but fast and good enough)
    n = int(x * 7 + y * 13 + seed * 31) & 0xFFFF
    n = (n * 15731 + 789221) & 0x7FFFFFFF
    return (n / 0x7FFFFFFF) * 2 - 1

def generate_flow_field(rng: QuantumRNG, palette: list) -> str:
    """Generate curves that follow a noise field."""
    lines = []
    num_lines = rng.randint(20, 50)
    noise_seed = rng.randint(0, 10000)

    for _ in range(num_lines):
        x = rng.random() * 1000
        y = rng.random() * 1000
        hue = rng.choice(palette)
        color = f"hsl({hue},{60 + rng.random()*30}%,{50 + rng.random()*20}%)"

        points = [f"M {x:.1f} {y:.1f}"]
        for step in range(30):
            angle = noise_2d(x / 200, y / 200, noise_seed) * math.pi * 2
            x += math.cos(angle) * 8
            y += math.sin(angle) * 8
            points.append(f"L {x:.1f} {y:.1f}")

        path = " ".join(points)
        lines.append(
            f'<path d="{path}" fill="none" stroke="{color}" '
            f'stroke-width="1.5" opacity="0.4" stroke-linecap="round" />'
        )

    return "\n".join(lines)

Layer 4: Particles and Noise Texture

The particle layer adds texture and depth. Hundreds of tiny dots scattered across the canvas create a sense of space, like stars or dust:

def generate_particles(rng: QuantumRNG, palette: list, count: int) -> str:
    """Generate particle scatter."""
    particles = []
    for _ in range(count):
        x = rng.random() * 1000
        y = rng.random() * 1000
        r = 0.5 + rng.random() * 3
        hue = rng.choice(palette)
        lit = 60 + rng.random() * 30
        opacity = 0.2 + rng.random() * 0.6

        particles.append(
            f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" '
            f'fill="hsl({hue},70%,{lit}%)" opacity="{opacity:.2f}" />'
        )

    return "\n".join(particles)

For the noise texture overlay, we use an SVG filter that adds subtle grain:

def generate_noise_filter() -> str:
    """SVG filter for subtle noise texture."""
    return '''
    <filter id="noise">
      <feTurbulence type="fractalNoise" baseFrequency="0.65"
                     numOctaves="3" stitchTiles="stitch" />
      <feColorMatrix type="saturate" values="0" />
    </filter>
    <rect width="1000" height="1000" filter="url(#noise)"
          opacity="0.05" />'''

The noise overlay at 5% opacity adds analog film-like grain that softens the digital precision of the vector shapes.

Putting It All Together

The main generator function orchestrates all layers:

import math

def generate_artwork(seed_hex: str, token_id: int) -> str:
    """Generate complete SVG artwork from quantum seed."""
    rng = QuantumRNG(seed_hex)

    # Choose color harmony
    base_hue = rng.random() * 360
    harmony_type = rng.choice([
        "complementary", "analogous", "triadic",
        "split_complementary", "tetradic"
    ])
    harmony_fn = getattr(ColorHarmony, harmony_type)
    palette = harmony_fn(base_hue)

    # Determine complexity (driven by quantum entropy)
    num_shapes = rng.randint(5, 15)
    num_curves = rng.randint(3, 10)
    num_particles = rng.randint(50, 300)
    use_flow_field = rng.random() > 0.6

    # Build SVG layers
    bg = generate_background(rng, palette)
    shapes = generate_shapes(rng, palette, num_shapes)
    if use_flow_field:
        curves = generate_flow_field(rng, palette)
    else:
        curves = generate_bezier_curves(rng, palette, num_curves)
    particles = generate_particles(rng, palette, num_particles)
    noise = generate_noise_filter()

    # Compose final SVG
    svg = f'''<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 1000 1000" width="1000" height="1000">
  <!-- Quantum Genesis #{token_id} -->
  <!-- Seed: {seed_hex[:16]}... -->
  <!-- Harmony: {harmony_type} | Base hue: {base_hue:.0f}° -->
  {bg}
  <g id="shapes">{shapes}</g>
  <g id="curves">{curves}</g>
  <g id="particles">{particles}</g>
  {noise}
</svg>'''

    return svg

Each artwork takes about 50ms to generate. The quantum measurement (which happened earlier) took ~19 seconds per NFT on IBM's ibm_fez. The art generation itself is the fast part — all the time goes into getting the quantum seed.

Results: 100 Unique Artworks

Here's what the full pipeline produced:

Quantum Genesis NFT #67 - rich color composition from IBM Quantum measurements

Quantum Genesis #67 — IBM Quantum ibm_torino. Triadic color harmony with flow field curves.

Each artwork is unique not just in the "different random seed" sense, but in structure. The quantum measurements determine:

  • Color harmony type (5 possibilities)
  • Base hue (360 degrees)
  • Number of shapes (5-15)
  • Shape types (circles, rectangles, polygons, rings)
  • Curve style (bezier or flow field)
  • Particle density (50-300)
  • Every position, size, rotation, and opacity

The parameter space is effectively infinite. Two seeds that differ by a single bit produce completely different artworks.

What Makes This Different From Math.random()?

Technically, you could seed this generator with any random source and get similar-looking art. What makes the quantum source special?

  1. Provenance. Each seed is cryptographically linked (via SHA-256) to a specific quantum circuit run on a specific processor at a specific time. This is verifiable on-chain.
  2. True randomness. Classical PRNGs are deterministic — if you know the state, you can predict all future outputs. Quantum measurements are fundamentally random per the laws of physics.
  3. Unreproducibility. You cannot run the same circuit on the same processor and get the same measurements. Calibration drift, thermal noise, and quantum indeterminacy guarantee uniqueness.

The art generator is the same for all 100 NFTs. What makes each one unique is the quantum seed — and that seed comes from the universe itself.

Build Your Own

You don't need a quantum computer to experiment with generative art. Start with random.seed(42) and swap in a quantum source later. The techniques above — HSL color harmony, layered SVG composition, bezier curves, particle systems — work with any random source.

The full code for the Quantum Genesis generator is available in the collection's metadata and referenced in our Dev.to article.

@media (prefers-color-scheme: dark) { background:#1a1a2e; border-color:#2d2d4a; box-shadow:0 1px 3px rgba(0,0,0,0.3); }
Marcelo Santos
Marcelo Santos
Engenheiro Quântico • Artista Generativo • Founder Quantum Art Lab

Desenvolvo sistemas que usam hardware quântico real (IBM Quantum, Origin Quantum) para gerar arte e NFTs com proveniência verificável on-chain. Escrevo sobre computação quântica aplicada, criptografia, Web3 e arte generativa — tudo com código que roda em processadores quânticos de verdade.

Comentários

Postagens mais visitadas deste blog

How to List NFTs on OpenSea: The Complete 2026 Guide

Polygon vs Ethereum for NFTs: Why We Chose Polygon (and Saved $490)

Quantum Error Correction Explained: Why Your Qubits Need Backup