Thumbnail.

E8 v2

Description

This is an evolution of E8 v1. Made with Chatgpt and Google Colab.

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.")

Author

ACJ
49 entries

Stats

Date
Colors16,777,216
Pixels16,777,216
Dimensions4,096 × 4,096
Bytes13,405,694