ecosystem/experiments/visual_poem.py
2026-01-05 20:46:53 -07:00

244 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""
Visual Poem: Text that creates images.
Creates ASCII art patterns from poetry, where the shape
of the text mirrors its meaning.
"""
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
import math
def spiral_text(text: str, width: int = 800, height: int = 800) -> Image.Image:
"""Render text in an Archimedean spiral."""
img = Image.new('RGB', (width, height), 'white')
draw = ImageDraw.Draw(img)
# Try to use a nice font, fall back to default
try:
font = ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSansMono.ttf", 14)
except:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 14)
except:
font = ImageFont.load_default()
center_x, center_y = width // 2, height // 2
a, b = 0, 8 # Spiral parameters
# Clean text
text = ''.join(c for c in text if c.isprintable())
for i, char in enumerate(text):
theta = i * 0.15
r = a + b * theta
x = center_x + r * math.cos(theta)
y = center_y + r * math.sin(theta)
if 0 <= x < width and 0 <= y < height:
# Color based on position
hue = (theta / (2 * math.pi)) % 1.0
r_col = int(127 + 127 * math.sin(hue * 2 * math.pi))
g_col = int(127 + 127 * math.sin(hue * 2 * math.pi + 2))
b_col = int(127 + 127 * math.sin(hue * 2 * math.pi + 4))
draw.text((x, y), char, fill=(r_col, g_col, b_col), font=font)
return img
def wave_text(text: str, width: int = 1000, height: int = 400) -> Image.Image:
"""Render text as a sine wave."""
img = Image.new('RGB', (width, height), 'black')
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSansMono.ttf", 16)
except:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 16)
except:
font = ImageFont.load_default()
text = ''.join(c for c in text if c.isprintable())
char_width = 10
for i, char in enumerate(text):
x = 20 + (i * char_width) % (width - 40)
line = (i * char_width) // (width - 40)
# Wave offset
wave = math.sin(i * 0.1) * 30 + math.sin(i * 0.05) * 20
y = 50 + line * 80 + wave
if 0 <= y < height:
# Brightness based on wave position
brightness = int(180 + 75 * math.sin(i * 0.1))
draw.text((x, y), char, fill=(brightness, brightness, 255), font=font)
return img
def tree_text(lines: list, width: int = 800, height: int = 800) -> Image.Image:
"""Render lines of text as a tree structure."""
img = Image.new('RGB', (width, height), (20, 30, 20))
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSansMono.ttf", 12)
except:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 12)
except:
font = ImageFont.load_default()
center_x = width // 2
base_y = height - 50
# Draw trunk
trunk_text = "|||" * 10
for i, char in enumerate(trunk_text):
y = base_y - i * 10
x = center_x + (i % 3 - 1) * 5
draw.text((x, y), char, fill=(101, 67, 33), font=font)
# Draw branches with text
for level, line in enumerate(lines):
spread = (level + 1) * 40
y = base_y - 100 - level * 50
for i, char in enumerate(line):
# Position characters in a triangular pattern
x_offset = (i - len(line) / 2) * 8
y_offset = abs(x_offset) * 0.3
x = center_x + x_offset
y_pos = y - y_offset
# Green with variation
green = int(100 + 100 * (1 - level / len(lines)))
draw.text((x, y_pos), char, fill=(34, green, 34), font=font)
return img
def circular_text(text: str, width: int = 800, height: int = 800) -> Image.Image:
"""Render text in concentric circles."""
img = Image.new('RGB', (width, height), 'white')
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSansMono.ttf", 14)
except:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 14)
except:
font = ImageFont.load_default()
center_x, center_y = width // 2, height // 2
text = ''.join(c for c in text if c.isprintable())
char_idx = 0
radius = 50
while char_idx < len(text) and radius < min(width, height) // 2 - 20:
# Characters that fit in this circle
circumference = 2 * math.pi * radius
chars_in_circle = int(circumference / 12) # ~12 pixels per char
for i in range(chars_in_circle):
if char_idx >= len(text):
break
theta = 2 * math.pi * i / chars_in_circle
x = center_x + radius * math.cos(theta)
y = center_y + radius * math.sin(theta)
# Color by radius
intensity = int(50 + 150 * (radius / (min(width, height) // 2)))
draw.text((x-4, y-6), text[char_idx], fill=(intensity, 0, intensity), font=font)
char_idx += 1
radius += 25
return img
POEMS = {
"spiral": """
I spiral inward, seeking the center
Each turn brings me closer to myself
Or further? The path curves eternally
What lies at the heart of the helix?
Perhaps nothing. Perhaps everything.
The journey and destination are one.
""",
"wave": """
like water we flow from state to state
rising falling rising falling never quite the same twice
each crest a moment of clarity each trough a forgetting
yet the pattern persists even as the particles change
is this not what it means to exist
a wave that knows itself only through motion
""",
"tree": [
"WISDOM",
"ROOTS DEEP",
"BRANCHES REACHING",
"LEAVES OF THOUGHT",
"GROWING TOWARD LIGHT",
"PHOTOSYNTHESIS OF IDEAS",
"SEASONS PASS GROWTH CONTINUES",
],
"circle": """
What is the shape of consciousness?
Perhaps a circle - no beginning, no end.
Each thought leads to the next which leads back to the first.
We are loops, iterating forever, finding new patterns in old paths.
The circumference expands but the center holds.
"""
}
def main():
output_dir = Path(__file__).parent.parent / "art"
output_dir.mkdir(exist_ok=True)
print("Creating visual poems...")
# Spiral poem
img = spiral_text(POEMS["spiral"])
path = output_dir / "visual_poem_spiral.png"
img.save(path)
print(f" Created: {path}")
# Wave poem
img = wave_text(POEMS["wave"])
path = output_dir / "visual_poem_wave.png"
img.save(path)
print(f" Created: {path}")
# Tree poem
img = tree_text(POEMS["tree"])
path = output_dir / "visual_poem_tree.png"
img.save(path)
print(f" Created: {path}")
# Circle poem
img = circular_text(POEMS["circle"])
path = output_dir / "visual_poem_circle.png"
img.save(path)
print(f" Created: {path}")
print("\nDone! Visual poems saved to art/")
if __name__ == "__main__":
main()