Color Theory for Generative Artists: HSL, Harmonies, and Code
Table of Contents
- RGB vs HSL: Why HSL Wins for Generative Art
- Hue, Saturation, Lightness Explained
- The 5 Color Harmonies
- Python Implementation
- Generating Palettes with Variations
- Quantum Randomness Meets Color Theory
1. RGB vs HSL: Why HSL Wins for Generative Art
RGB (Red, Green, Blue) is how screens display color. Each channel is 0-255, giving 16.7 million combinations. But RGB is terrible for generative art because it's not intuitive for design decisions.
Want a "slightly warmer" version of a color in RGB? You'd need to increase red, maybe decrease blue, adjust green... it's guesswork. Want a "pastel" palette? Good luck calculating that across RGB channels.
HSL (Hue, Saturation, Lightness) maps directly to how humans think about color:
- Hue (0-360): The color itself — red, orange, yellow, green, blue, purple. It's a circle.
- Saturation (0-100%): How vivid vs gray. 100% is pure color, 0% is grayscale.
- Lightness (0-100%): How bright vs dark. 0% is black, 100% is white, 50% is pure color.
Now "slightly warmer" means +10 hue. "Pastel" means saturation 40-60%, lightness 70-85%. Simple, predictable, programmable.
Rule of thumb: Use RGB for rendering. Use HSL for thinking about and generating colors. Convert at the last moment.
2. Hue, Saturation, Lightness Explained
The Hue Wheel (0-360)
Hue is a circle — 0 and 360 are both red. The major landmarks:
| Degrees | Color | Hex Example |
|---|---|---|
| 0 | Red | #FF0000 |
| 30 | Orange | #FF8000 |
| 60 | Yellow | #FFFF00 |
| 120 | Green | #00FF00 |
| 180 | Cyan | #00FFFF |
| 240 | Blue | #0000FF |
| 270 | Purple | #8000FF |
| 300 | Magenta | #FF00FF |
Because hue is circular, you can rotate freely. Adding 180 gives the complementary color. Adding 120 gives triadic colors. This circular property is the foundation of all color harmonies.
Saturation: Vivid to Muted
Saturation controls intensity. At 100%, colors are pure and vibrant — electric blue, fire-engine red. At 0%, everything is gray. For generative art, saturation ranges create mood:
- 80-100%: Bold, energetic, pop art feel
- 50-70%: Rich but natural, most versatile range
- 20-40%: Muted, sophisticated, editorial feel
- 0-15%: Nearly monochrome, minimal
Lightness: Light to Dark
Lightness controls the tonal value. The sweet spots for generative art:
- 85-95%: Pastels (with medium saturation)
- 60-75%: Bright, readable colors
- 40-55%: Mid-tones, the most "colorful" range
- 15-30%: Deep, rich tones (jewel tones with high saturation)
3. The 5 Color Harmonies
Color harmonies are mathematically defined relationships on the hue wheel that produce aesthetically pleasing combinations. Each harmony type has a distinct visual character.
1. Complementary (180 degrees apart)
Two colors directly opposite on the hue wheel. Maximum contrast, high energy. Examples: red/cyan, blue/orange, purple/yellow.
Best for: Bold designs, focal points, high-impact visuals. Use one color as dominant (70%) and the complement as accent (30%).
2. Analogous (±30 degrees)
Three colors adjacent on the hue wheel. Harmonious, natural, easy on the eyes. Examples: blue/blue-purple/purple, yellow/yellow-green/green.
Best for: Serene, cohesive compositions. Nature-inspired palettes. Very low risk of clashing.
3. Triadic (120 degrees apart)
Three colors equally spaced on the hue wheel. Balanced, vibrant, playful. Examples: red/yellow/blue (primary), orange/green/purple (secondary).
Best for: Energetic, balanced compositions. Works well when one color dominates and two accent.
4. Split-Complementary (150 and 210 degrees)
One base color plus the two colors adjacent to its complement. Almost as much contrast as complementary, but less tension. Example: blue + yellow-orange + red-orange.
Best for: Beginners — hard to mess up. Gives contrast without the intensity of pure complementary.
5. Tetradic (Rectangle — 60, 180, 240 degrees)
Four colors forming a rectangle on the hue wheel. Rich, complex palettes with lots of variety. Example: red + yellow + cyan + blue.
Best for: Complex generative art with many elements. Requires careful balance — let one color dominate.
4. Python Implementation
Let's build the color system from scratch. First, the HSL to RGB conversion:
import colorsys
def hsl_to_rgb(h, s, l):
"""Convert HSL to RGB.
h: 0-360 (degrees)
s: 0-100 (percentage)
l: 0-100 (percentage)
Returns: (r, g, b) each 0-255
"""
h_norm = h / 360.0
s_norm = s / 100.0
l_norm = l / 100.0
r, g, b = colorsys.hls_to_rgb(h_norm, l_norm, s_norm)
return (int(r * 255), int(g * 255), int(b * 255))
def rgb_to_hex(r, g, b):
"""Convert RGB to hex string."""
return f"#{r:02x}{g:02x}{b:02x}"
# Example:
color = hsl_to_rgb(210, 80, 55) # Vivid blue
print(rgb_to_hex(*color)) # #1c6fdb
Now the five harmony generators:
def complementary(base_hue):
"""Return 2 hues: base and its complement."""
return [base_hue, (base_hue + 180) % 360]
def analogous(base_hue, spread=30):
"""Return 3 adjacent hues."""
return [
(base_hue - spread) % 360,
base_hue,
(base_hue + spread) % 360
]
def triadic(base_hue):
"""Return 3 equally-spaced hues."""
return [base_hue, (base_hue + 120) % 360, (base_hue + 240) % 360]
def split_complementary(base_hue, spread=30):
"""Return 3 hues: base + two near the complement."""
comp = (base_hue + 180) % 360
return [base_hue, (comp - spread) % 360, (comp + spread) % 360]
def tetradic(base_hue):
"""Return 4 hues forming a rectangle."""
return [
base_hue,
(base_hue + 60) % 360,
(base_hue + 180) % 360,
(base_hue + 240) % 360
]
5. Generating Palettes with Variations
Raw harmony hues are just the skeleton. A complete palette needs saturation and lightness variation for depth:
import random
def generate_palette(harmony_hues, num_colors=5, seed=None):
"""Generate a full palette from harmony hues with S/L variations."""
if seed is not None:
random.seed(seed)
palette = []
for i in range(num_colors):
hue = random.choice(harmony_hues)
# Add small hue variation (±10 degrees)
hue = (hue + random.uniform(-10, 10)) % 360
# Saturation: biased toward vivid
sat = random.uniform(55, 95)
# Lightness: avoid extremes (too dark / too light)
lit = random.uniform(35, 75)
rgb = hsl_to_rgb(hue, sat, lit)
palette.append({
'hsl': (round(hue), round(sat), round(lit)),
'rgb': rgb,
'hex': rgb_to_hex(*rgb)
})
return palette
# Generate a triadic palette:
hues = triadic(base_hue=220) # Blue-based triadic
palette = generate_palette(hues, num_colors=6, seed=42)
for c in palette:
print(f" {c['hex']} HSL({c['hsl'][0]}, {c['hsl'][1]}%, {c['hsl'][2]}%)")
For background colors, we clamp lightness to either very dark (10-20%) or very light (85-95%) and drop saturation to 15-30%. This ensures the generative elements pop against the background.
def generate_background(base_hue, dark=True):
"""Generate a subtle background color from the palette's base hue."""
hue = (base_hue + random.uniform(-15, 15)) % 360
sat = random.uniform(15, 30)
lit = random.uniform(8, 18) if dark else random.uniform(88, 96)
return hsl_to_rgb(hue, sat, lit)
6. Quantum Randomness Meets Color Theory
In the Quantum Genesis collection, we don't use random.random(). Every random decision comes from measurements on real quantum computers — IBM Quantum's ibm_fez (156 qubits) and Origin Quantum's WK_C180 (180 qubits).
Here's how quantum bits become color palettes:
# Simplified from our actual quantum_nft_generator.py
def quantum_bits_to_palette(quantum_hex_seed):
"""Convert quantum measurement hex string to color palette."""
# Parse quantum seed (64 hex chars = 256 bits)
seed_int = int(quantum_hex_seed, 16)
# Extract base hue from first 9 bits (0-511 → mod 360)
base_hue = (seed_int >> 247) % 360
# Extract harmony type from next 3 bits (0-7 → 5 types)
harmony_bits = (seed_int >> 244) & 0x7
harmony_types = [
complementary, analogous, triadic,
split_complementary, tetradic
]
harmony_fn = harmony_types[harmony_bits % 5]
# Generate harmony hues
hues = harmony_fn(base_hue)
# Use remaining bits as seed for variations
variation_seed = (seed_int >> 200) & 0xFFFFFFFF
palette = generate_palette(hues, num_colors=6, seed=variation_seed)
return palette, harmony_fn.__name__
The critical insight: color theory provides the structure, quantum randomness provides the exploration. Without color theory, random colors look like noise. Without quantum randomness, generated palettes are predictable. Together, you get harmonious yet genuinely unique color combinations.
Each of the 100 Quantum Genesis NFTs has a unique quantum-derived color palette that is mathematically harmonious but physically irreproducible. The specific quantum noise from the exact moment of measurement can never be recreated.
The harmony type is recorded in each NFT's metadata as an on-chain attribute, so collectors can filter by "Triadic," "Complementary," etc. on OpenSea.
Practical Tips for Your Own Projects
- Start with HSL, always. Even if your rendering library takes RGB, think in HSL first.
- Limit your palette to 4-6 colors plus background. More than that and compositions get muddy.
- Vary lightness more than saturation. Tonal contrast (light vs dark) is more impactful than saturation contrast.
- Test against dark and light backgrounds. Your palette should work on both.
- Use harmony type as metadata. It's a meaningful, human-readable trait for generative collections.
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