import random
import math
from pathlib import Path
from IPython.display import SVG, display, HTML
# ── colour palette ────────────────────────────────────────────────────────────
BASE = ['#524BD5', '#3C8662', '#F6D148', '#E34927']
GREY = '#252628'
WHITE = '#ffffff'
# tile 0..7 carry these words; tiles 8..15 are blank
WORDS = ['llms', 'are', 'a', 'lossy', 'compression', 'of', 'the', 'internet']
N = 4 # grid is N×NTile Animation Stills → SVG
Generates print-ready SVGs that look like stills from the tile animation at the top of the LLMs-as-Lossy-Compression blog post.
Open the output files in Illustrator to fine-tune, convert text to outlines, and prep for CMYK printing.
Palette (matches the blog exactly): - #524BD5 indigo
- #3C8662 pine
- #F6D148 gold
- #E34927 vermillion
- #252628 near-black (grey / “off” state)
# ── core SVG builder ─────────────────────────────────────────────────────────
def make_svg(
perm,
colors,
*,
tile: int = 120,
gap: int = 3,
padding: int = 0,
show_words: bool = True,
reading_order: bool = True,
corner_r: int = 0,
bg: str = WHITE,
font: str = "Futura PT, Futura, Century Gothic, sans-serif",
font_weight: str = "500",
text_color: str = WHITE,
) -> str:
"""
perm : list[int] length 16 — perm[tile_idx] = cell_position (0..15)
Must be a permutation of range(16).
colors : list[str] length 16 — hex fill for each tile
reading_order : if True, words are assigned so they read left-to-right /
top-to-bottom across wherever the word-tiles happen to land.
Tiles can be fully scrambled while the phrase still reads in
order. If False, tile 0 always carries 'llms', tile 1 'are', etc.
tile : px per tile (SVG is vector; this is just coordinate scale)
gap : px between tiles
corner_r : border-radius on tiles (0 = sharp corners)
bg : SVG background colour ('none' for transparent)
"""
assert len(perm) == 16 and len(colors) == 16
assert sorted(perm) == list(range(16)), "perm must be a permutation of 0..15"
grid_px = N * tile + (N - 1) * gap
total = grid_px + 2 * padding
fs = max(8, tile // 7)
letter_spacing = max(0, fs * 0.02)
# build cell→word mapping for reading-order mode
if reading_order and show_words:
# cells occupied by word tiles (0..7), sorted in reading order
word_cells_sorted = sorted(perm[i] for i in range(8))
cell_to_word = {cell: WORDS[rank] for rank, cell in enumerate(word_cells_sorted)}
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg"',
f' width="{total}" height="{total}"',
f' viewBox="0 0 {total} {total}">',
]
if bg and bg != 'none':
parts.append(f' <rect width="{total}" height="{total}" fill="{bg}"/>')
parts += [
' <defs>',
' <filter id="ts" x="-20%" y="-20%" width="140%" height="140%">',
' <feDropShadow dx="0" dy="1" stdDeviation="1.8"',
' flood-color="#000000" flood-opacity="0.28"/>',
' </filter>',
' </defs>',
]
for tile_idx, cell_pos in enumerate(perm):
col = cell_pos % N
row = cell_pos // N
x = padding + col * (tile + gap)
y = padding + row * (tile + gap)
color = colors[tile_idx]
rr = f' rx="{corner_r}" ry="{corner_r}"' if corner_r else ''
parts.append(
f' <rect x="{x}" y="{y}" width="{tile}" height="{tile}"'
f' fill="{color}"{rr}/>'
)
if show_words and tile_idx < 8:
word = cell_to_word[cell_pos] if reading_order else WORDS[tile_idx]
cx = x + tile / 2
cy = y + tile / 2
parts.append(
f' <text'
f' x="{cx:.1f}" y="{cy:.1f}"'
f' text-anchor="middle" dominant-baseline="central"'
f' font-family="{font}"'
f' font-size="{fs}"'
f' font-weight="{font_weight}"'
f' fill="{text_color}"'
f' filter="url(#ts)"'
f' letter-spacing="{letter_spacing:.2f}"'
f'>{word}</text>'
)
parts.append('</svg>')
return '\n'.join(parts)
print("make_svg ready")make_svg ready
# ── permutation & colour helpers ──────────────────────────────────────────────
def identity_perm():
"""Reading order: tile i → cell i. Words spell out the title left-to-right."""
return list(range(16))
def rand_perm(seed=None):
rng = random.Random(seed)
p = list(range(16))
rng.shuffle(p)
return p
def make_colors(n_colored: int, seed=None) -> list:
"""
n_colored: how many of the 16 tiles get a BASE colour (rest → GREY).
Useful range: 0 (all grey) … 16 (all coloured).
"""
rng = random.Random(seed)
idxs = list(range(16))
rng.shuffle(idxs)
colors = [GREY] * 16
for i in idxs[:n_colored]:
colors[i] = rng.choice(BASE)
return colors
def all_colored(seed=None):
rng = random.Random(seed)
return [rng.choice(BASE) for _ in range(16)]
def all_grey():
return [GREY] * 16
print("helpers ready")helpers ready
# ── preset stills ─────────────────────────────────────────────────────────────
# Each entry: (name, description, perm, colors, extra_kwargs)
SEED = 7 # change to explore variations
presets = [
(
"ordered_full",
"Reading order, all tiles coloured — clean title card",
identity_perm(),
all_colored(seed=SEED),
{},
),
(
"ordered_half",
"Reading order, half coloured — mid-colouring phase",
identity_perm(),
make_colors(8, seed=SEED),
{},
),
(
"scrambled_full",
"Scrambled, all coloured — maximum entropy",
rand_perm(seed=SEED),
all_colored(seed=SEED),
{},
),
(
"scrambled_half",
"Scrambled, half coloured — typical mid-animation still",
rand_perm(seed=SEED),
make_colors(8, seed=SEED),
{},
),
(
"scrambled_sparse",
"Scrambled, 4 tiles coloured — low entropy, dramatic",
rand_perm(seed=SEED),
make_colors(4, seed=SEED),
{},
),
(
"scrambled_grey",
"Scrambled, all grey — minimum entropy",
rand_perm(seed=SEED),
all_grey(),
{},
),
(
"icon_notext",
"Scrambled, all coloured, NO words — compact abstract icon",
rand_perm(seed=SEED + 1),
all_colored(seed=SEED + 1),
{"show_words": False},
),
(
"icon_rounded",
"Scrambled, half coloured, rounded corners, no text — pill-tile icon",
rand_perm(seed=SEED + 2),
make_colors(10, seed=SEED + 2),
{"show_words": False, "corner_r": 12, "gap": 6},
),
]
print(f"{len(presets)} presets defined")8 presets defined
# ── render & save ─────────────────────────────────────────────────────────────
OUT_DIR = Path("tile_stills")
OUT_DIR.mkdir(exist_ok=True)
# tile size for SVG coordinate space — SVG is vector so this doesn't cap quality
# 120 gives a 480×480 grid coordinate system, comfortable for Illustrator
TILE_PX = 120
GAP_PX = 3
for name, desc, perm, colors, extra in presets:
kw = dict(tile=TILE_PX, gap=GAP_PX, padding=0)
kw.update(extra) # preset-specific overrides win (e.g. icon_rounded sets gap=6)
svg = make_svg(perm, colors, **kw)
path = OUT_DIR / f"{name}.svg"
path.write_text(svg, encoding="utf-8")
print(f"── {name}")
print(f" {desc}")
print(f" → {path}")
display(SVG(svg))
print()── ordered_full
Reading order, all tiles coloured — clean title card
→ tile_stills/ordered_full.svg
── ordered_half
Reading order, half coloured — mid-colouring phase
→ tile_stills/ordered_half.svg
── scrambled_full
Scrambled, all coloured — maximum entropy
→ tile_stills/scrambled_full.svg
── scrambled_half
Scrambled, half coloured — typical mid-animation still
→ tile_stills/scrambled_half.svg
── scrambled_sparse
Scrambled, 4 tiles coloured — low entropy, dramatic
→ tile_stills/scrambled_sparse.svg
── scrambled_grey
Scrambled, all grey — minimum entropy
→ tile_stills/scrambled_grey.svg
── icon_notext
Scrambled, all coloured, NO words — compact abstract icon
→ tile_stills/icon_notext.svg
── icon_rounded
Scrambled, half coloured, rounded corners, no text — pill-tile icon
→ tile_stills/icon_rounded.svg
# ── interactive explorer ───────────────────────────────────────────────────────
# Re-run this cell with different parameters to explore states quickly.
# ← tweak these
EXPLORE_SEED = 42
N_COLORED = 10 # 0..16
SCRAMBLED = True
SHOW_WORDS = True
READING_ORDER = True # True → words scatter across grid but still read in order
CORNER_RADIUS = 0 # try 8 or 16 for rounded tiles
TILE_GAP = 3 # px gap between tiles
BACKGROUND = WHITE # or 'none' for transparent
perm = rand_perm(EXPLORE_SEED) if SCRAMBLED else identity_perm()
colors = make_colors(N_COLORED, seed=EXPLORE_SEED)
svg = make_svg(
perm, colors,
tile=TILE_PX,
gap=TILE_GAP,
padding=0,
show_words=SHOW_WORDS,
reading_order=READING_ORDER,
corner_r=CORNER_RADIUS,
bg=BACKGROUND,
)
display(SVG(svg))
explore_path = OUT_DIR / "explore_current.svg"
explore_path.write_text(svg, encoding="utf-8")
print(f"saved → {explore_path}")saved → tile_stills/explore_current.svg
# ── batch random variants ──────────────────────────────────────────────────────
# Generate N_VARIANTS random stills for a given colour density and layout.
# Useful for picking a favourite to develop further in Illustrator.
N_VARIANTS = 12
N_COL = 9 # tiles coloured per variant
SHOW_W = True # text off → works better as small icon grid
html_parts = ['<div style="display:flex;flex-wrap:wrap;gap:8px;">']
for s in range(N_VARIANTS):
p = rand_perm(seed=s * 1000)
c = make_colors(N_COL, seed=s * 100)
svg = make_svg(p, c, tile=80, gap=2, padding=0, show_words=SHOW_W)
path = OUT_DIR / f"variant_{s:02d}.svg"
path.write_text(svg, encoding="utf-8")
html_parts.append(f'<div><div style="font-size:10px;color:#999;margin-bottom:2px">seed {s*100}</div>{svg}</div>')
html_parts.append('</div>')
display(HTML(''.join(html_parts)))
print(f"Saved {N_VARIANTS} variants to {OUT_DIR}/")seed 0
seed 100
seed 200
seed 300
seed 400
seed 500
seed 600
seed 700
seed 800
seed 900
seed 1000
seed 1100
Saved 12 variants to tile_stills/
Illustrator notes
- Open SVG directly — File → Open. Don’t place; open natively so it’s fully editable.
- Font → outlines — Select all text, Type → Create Outlines (
Cmd+Shift+O) before printing or sending to a vendor. - CMYK — File → Document Color Mode → CMYK Color if going to print. The palette uses very saturated RGB values; check the CMYK equivalents with the vendor.
- Bleed — For business cards, add a 3mm bleed by expanding the artboard and extending background tiles to fill it.
- Scale — SVG is resolution-independent. Scale the artboard to any size; the geometry stays crisp.