Generative Art with Python: From Random Seeds to SVG Masterpieces
Table of Contents
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 #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:
- Background gradient
- Large geometric shapes (circles, rectangles, polygons)
- Bezier curves (flowing organic lines)
- Mid-ground elements (smaller shapes, rings)
- Particles (tiny dots and marks)
- 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 #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 #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?
- 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.
- 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.
- 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.
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
Postar um comentário