import numpy as np
import colorsys
import math
import random
from itertools import product
from PIL import Image, ImageDraw, ImageFont
# === Configuration ===
SIZE = 4096
TOTAL_PIXELS = SIZE * SIZE
TOTAL_COLORS = 2**24 # 16,777,216
ALPHA_BG = int(255 * 0.25) # 25% opacity (75% transparent)
ALPHA_FG = 255 # fully opaque for shapes
# Dot radii per ring (inner → outer)
RADIUS_MAP = [10, 9, 8, 7, 6, 5, 4, 3]
# Simple roots for Coxeter element
simple_roots = np.array([
[ 1,-1, 0, 0, 0, 0, 0, 0],
[ 0, 1,-1, 0, 0, 0, 0, 0],
[ 0, 0, 1,-1, 0, 0, 0, 0],
[ 0, 0, 0, 1,-1, 0, 0, 0],
[ 0, 0, 0, 0, 1,-1, 0, 0],
[ 0, 0, 0, 0, 0, 1,-1, 0],
[ 0, 0, 0, 0, 0, 0, 1,-1],
[ .5,.5,.5,.5,.5,.5,.5,-.5]
], dtype=float)
# === 1) Build the 240 E8 roots ===
roots = []
for i in range(8):
for j in range(i+1, 8):
v = np.zeros(8)
v[i], v[j] = 1, -1
roots.extend([v.copy(), (-v).copy()])
v[i], v[j] = 1, 1
roots.extend([v.copy(), (-v).copy()])
for signs in product([1, -1], repeat=8):
if np.prod(signs) == 1:
roots.append(np.array(signs, float) * 0.5)
roots = np.vstack(roots)
# === 2) Compute Coxeter element ===
def reflect(alpha):
α = alpha.reshape(8,1)
return np.eye(8) - 2 * (α @ α.T) / (α.T @ α)
C = np.eye(8)
for α in simple_roots:
C = reflect(α) @ C
# === 3) Petrie-plane basis from primitive eigenvector ===
h = 30
eigvals, eigvecs = np.linalg.eig(C)
target = np.exp(2j * np.pi / h)
idx = np.argmin(np.abs(eigvals - target))
v = eigvecs[:, idx]
B = np.vstack([v.real, v.imag]).T # (8,2)
# === 4) Project to pixel coordinates ===
proj = roots @ B
margin = 0.05 * SIZE
scale = (SIZE/2 - margin) / np.max(np.linalg.norm(proj, axis=1))
coords_f = proj * scale + SIZE/2
coords = np.round(coords_f).astype(int)
# === 5) Ring assignment ===
angles = np.angle(proj[:,0] + 1j*proj[:,1]) % (2*np.pi)
exponents = np.array([1,7,11,13,17,19,23,29])
ideal = (2*np.pi * exponents / h) % (2*np.pi)
ring_idx = np.argmin(np.abs(angles.reshape(-1,1) - ideal), axis=1).astype(int)
# === 6) Reserve text label pixels first ===
used = np.zeros((SIZE, SIZE), bool)
font = ImageFont.truetype("DejaVuSans.ttf", 48)
label_fill, label_outline = [], []
for k, text in enumerate(["I","II","III","IV","V","VI","VII","VIII"]):
pts = [coords[i] for i in range(240) if ring_idx[i]==k]
cx, cy = np.round(np.mean(pts, axis=0)).astype(int)
mask = Image.new("1", (SIZE, SIZE)); dmask = ImageDraw.Draw(mask)
dmask.text((cx, cy), text, font=font, fill=1)
arr = np.array(mask, bool)
# outline via dilation
pad = np.pad(arr, ((1,1),(1,1)), mode='constant', constant_values=False)
dil = np.zeros_like(arr)
for dy in (-1,0,1):
for dx in (-1,0,1):
if dx == 0 and dy == 0: continue
dil |= pad[1+dy:1+dy+SIZE, 1+dx:1+dx+SIZE]
out = dil & ~arr
ys, xs = np.where(out)
for y, x in zip(ys, xs):
label_outline.append((x, y)); used[y, x] = True
ys, xs = np.where(arr)
for y, x in zip(ys, xs):
label_fill.append((x, y)); used[y, x] = True
# === 7) Collect shape and background pixels ===
# 7a) Edges, Dots, Halos (same as before)...
# 7b) ...
# 7c) Boundaries – define mid_circle here so it is available
# Midpoint circle algorithm for ring outlines
def mid_circle(cx, cy, r):
x, y, err = r, 0, 0
pts = []
while x >= y:
for dx, dy in [( x, y),( y, x),(-y, x),(-x, y),(-x,-y),(-y,-x),( y,-x),( x,-y)]:
pts.append((cx+dx, cy+dy))
y += 1
err += 1 + 2*y
if 2*(err-x) + 1 > 0:
x -= 1
err += 1 - 2*x
return pts
# Now collect shapes and background pixels
line_px, dot_px, halo_px, bound_px = [], [], [], []
# Edges
line_px, dot_px, halo_px, bound_px = [], [], [], []
# Edges
gram = roots @ roots.T
iu, ju = np.where(np.triu(np.isclose(gram, 1.0), k=1))
def bres(x0, y0, x1, y1):
dx, sx = abs(x1-x0), (1 if x0 -dy: err -= dy; x0 += sx
if e2 < dx: err += dx; y0 += sy
return pts
for i, j in zip(iu, ju):
for x, y in bres(*coords[i], *coords[j]):
if not used[y, x]: line_px.append((x, y)); used[y, x] = True
# Dots & halos
circle_off = {k: [(dx, dy)
for dy in range(-r, r+1)
for dx in range(-int(math.sqrt(r*r-dy*dy)), int(math.sqrt(r*r-dy*dy))+1)]
for k, r in enumerate(RADIUS_MAP)}
for rid, (x0, y0) in enumerate(coords):
k = ring_idx[rid]
for dx, dy in circle_off[k]:
x, y = x0+dx, y0+dy
if not used[y, x]: dot_px.append((x, y)); used[y, x] = True
R = RADIUS_MAP[k] + 1
for dy in range(-R, R+1):
dxm = int(math.sqrt(R*R - dy*dy))
for dx in (-dxm, dxm):
x, y = x0+dx, y0+dy
if not used[y, x]: halo_px.append((x, y)); used[y, x] = True
# Boundaries
def mid_circle(cx, cy, r):
x, y, err = r, 0, 0; pts = []
while x >= y:
for dx, dy in [( x, y),( y, x),(-y, x),(-x, y),(-x,-y),(-y,-x),( y,-x),( x,-y)]:
pts.append((cx+dx, cy+dy))
y += 1; err += 1 + 2*y
if 2*(err-x) + 1 > 0: x -= 1; err += 1 - 2*x
return pts
cx, cy = SIZE//2, SIZE//2
for k in range(8):
rad = int(round(np.linalg.norm(proj[ring_idx==k], axis=1).mean()*scale))
for x, y in mid_circle(cx, cy, rad):
if not used[y, x]: bound_px.append((x, y)); used[y, x] = True
# Background
bg_px = [(x, y) for y in range(SIZE) for x in range(SIZE) if not used[y, x]]
# === 8) Palette slicing & ordering ===
idxs = np.arange(TOTAL_COLORS, dtype=np.uint32)
pal = np.empty((TOTAL_COLORS, 3), dtype=np.uint8)
pal[:,0] = (idxs >> 16) & 0xFF; pal[:,1] = (idxs >> 8) & 0xFF; pal[:,2] = idxs & 0xFF
hsv = np.array([colorsys.rgb_to_hsv(*(c/255.0)) for c in pal])
v = hsv[:,2]; s = hsv[:,1]
# Label fill: brightest & saturated
fill_idx = np.lexsort((-s, -v))[:len(label_fill)]
# Label outline: darkest
outline_idx = np.argsort(v)[:len(label_outline)]
used_idx = set(fill_idx.tolist() + outline_idx.tolist())
# Remaining sorted for shapes by brightness+sat (bright start)
remaining = [i for i in np.lexsort((-s, -v)) if i not in used_idx]
i = 0
line_idx = remaining[i:i+len(line_px)]; i += len(line_px)
halo_idx = remaining[i:i+len(halo_px)]; i += len(halo_px)
dot_idx = remaining[i:i+len(dot_px)]; i += len(dot_px)
bound_idx = remaining[i:i+len(bound_px)]; i += len(bound_px)
bg_idx = remaining[i:i+len(bg_px)]; i += len(bg_px)
assert i == len(remaining)
# Extract colors
fill_cols = pal[fill_idx]; outline_cols = pal[outline_idx]
line_cols = pal[line_idx]; halo_cols = pal[halo_idx]
dot_cols = pal[dot_idx]; bound_cols = pal[bound_idx]
bg_cols = pal[bg_idx]
# Completely shuffle background noise-style
pair_bg = list(zip(bg_px, bg_cols)); random.shuffle(pair_bg)
bg_px, bg_cols = zip(*pair_bg)
# === 9) Render layer-by-layer ===
img = Image.new("RGBA", (SIZE, SIZE), (0,0,0,255)); px = img.load()
# 1) background (25% opaque)
for (x, y), (r, g, b) in zip(bg_px, bg_cols): px[x, y] = (r, g, b, ALPHA_BG)
# 2) boundaries
for (x, y), (r, g, b) in zip(bound_px, bound_cols): px[x, y] = (r, g, b, ALPHA_FG)
# 3) lines (bright)
for (x, y), (r, g, b) in zip(line_px, line_cols): px[x, y] = (r, g, b, ALPHA_FG)
# 4) halos
for (x, y), (r, g, b) in zip(halo_px, halo_cols): px[x, y] = (r, g, b, int(ALPHA_FG * 0.3))
# 5) dots
for (x, y), (r, g, b) in zip(dot_px, dot_cols): px[x, y] = (r, g, b, ALPHA_FG)
# 6) label outline
for (x, y), (r, g, b) in zip(label_outline, outline_cols): px[x, y] = (r, g, b, ALPHA_FG)
# 7) label fill (topmost)
for (x, y), (r, g, b) in zip(label_fill, fill_cols): px[x, y] = (r, g, b, ALPHA_FG)
# === 10) Save ===
img.save("E8_Petrie_exact24bit_final_4096.png")
print("✅ Done: background fully shuffled, labels unobstructed, bright lines, all colors used.")