Tile 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)

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×N
# ── 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
of internet lossy llms compression are a the
seed 100
internet llms compression the of a lossy are
seed 200
internet are of compression the a llms lossy
seed 300
are llms lossy the a internet of compression
seed 400
the compression are lossy a llms of internet
seed 500
of the compression internet a are lossy llms
seed 600
compression the llms are lossy internet of a
seed 700
llms the of compression lossy a internet are
seed 800
are llms compression a the of internet lossy
seed 900
llms the compression a of internet are lossy
seed 1000
llms lossy compression are of internet the a
seed 1100
internet the of a lossy compression are llms
Saved 12 variants to tile_stills/

Illustrator notes

  1. Open SVG directly — File → Open. Don’t place; open natively so it’s fully editable.
  2. Font → outlines — Select all text, Type → Create Outlines (Cmd+Shift+O) before printing or sending to a vendor.
  3. 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.
  4. Bleed — For business cards, add a 3mm bleed by expanding the artboard and extending background tiles to fill it.
  5. Scale — SVG is resolution-independent. Scale the artboard to any size; the geometry stays crisp.