update ascii-video skill: design patterns, local time, examples

- New references/design-patterns.md: layer hierarchy (bg/content/accent),
  directional parameter arcs, scene concepts and visual metaphors,
  counter-rotating systems, wave collision, progressive fragmentation,
  entropy/consumption, staggered crescendo buildup, scene ordering
- New references/examples.md: copy-paste-ready scenes at every complexity
- Update scenes.md: local time convention (t=0 at scene start)
- Update SKILL.md: add design-patterns.md to reference table
- Add README.md to hermes-agent copy
- Sync all reference docs with canonical source (SHL0MS/ascii-video)
This commit is contained in:
SHL0MS 2026-03-13 19:13:12 -04:00
parent bfb82b5cee
commit cda5910ab0
12 changed files with 3503 additions and 95 deletions

View file

@ -1,12 +1,43 @@
# Architecture Reference
**Cross-references:**
- Effect building blocks (value fields, noise, SDFs, particles): `effects.md`
- `_render_vf()`, blend modes, tonemap, masking: `composition.md`
- Scene protocol, render_clip, SCENES table: `scenes.md`
- Shader pipeline, feedback buffer, output encoding: `shaders.md`
- Complete scene examples: `examples.md`
- Input sources (audio analysis, video, TTS): `inputs.md`
- Performance tuning, hardware detection: `optimization.md`
- Common bugs (broadcasting, font, encoding): `troubleshooting.md`
## Grid System
### Resolution Presets
```python
RESOLUTION_PRESETS = {
"landscape": (1920, 1080), # 16:9 — YouTube, default
"portrait": (1080, 1920), # 9:16 — TikTok, Reels, Stories
"square": (1080, 1080), # 1:1 — Instagram feed
"ultrawide": (2560, 1080), # 21:9 — cinematic
"landscape4k":(3840, 2160), # 16:9 — 4K
"portrait4k": (2160, 3840), # 9:16 — 4K portrait
}
def get_resolution(preset="landscape", custom=None):
"""Returns (VW, VH) tuple."""
if custom:
return custom
return RESOLUTION_PRESETS.get(preset, RESOLUTION_PRESETS["landscape"])
```
### Multi-Density Grids
Pre-initialize multiple grid sizes. Switch per section for visual variety.
Pre-initialize multiple grid sizes. Switch per section for visual variety. Grid dimensions auto-compute from resolution:
| Key | Font Size | Grid (1920x1080) | Use |
**Landscape (1920x1080):**
| Key | Font Size | Grid (cols x rows) | Use |
|-----|-----------|-------------------|-----|
| xs | 8 | 400x108 | Ultra-dense data fields |
| sm | 10 | 320x83 | Dense detail, rain, starfields |
@ -15,7 +46,34 @@ Pre-initialize multiple grid sizes. Switch per section for visual variety.
| xl | 24 | 137x37 | Short quotes, large titles |
| xxl | 40 | 80x22 | Giant text, minimal |
**Grid sizing for text-heavy content**: When displaying readable text (quotes, lyrics, testimonials), use 20px (`lg`) as the primary grid. This gives 160 columns -- plenty for lines up to ~50 chars centered. For very short quotes (< 60 chars, <= 3 lines), 24px (`xl`) makes them more impactful. Only init the grids you actually use -- each grid pre-rasterizes all characters which costs ~0.3-0.5s.
**Portrait (1080x1920):**
| Key | Font Size | Grid (cols x rows) | Use |
|-----|-----------|-------------------|-----|
| xs | 8 | 225x192 | Ultra-dense, tall data columns |
| sm | 10 | 180x148 | Dense detail, vertical rain |
| md | 16 | 112x100 | Default balanced |
| lg | 20 | 90x80 | Readable text (~30 chars/line centered) |
| xl | 24 | 75x66 | Short quotes, stacked |
| xxl | 40 | 45x39 | Giant text, minimal |
**Square (1080x1080):**
| Key | Font Size | Grid (cols x rows) | Use |
|-----|-----------|-------------------|-----|
| sm | 10 | 180x83 | Dense detail |
| md | 16 | 112x56 | Default balanced |
| lg | 20 | 90x45 | Readable text |
**Key differences in portrait mode:**
- Fewer columns (90 at `lg` vs 160) — lines must be shorter or wrap
- Many more rows (80 at `lg` vs 45) — vertical stacking is natural
- Aspect ratio correction flips: `asp = cw / ch` still works but the visual emphasis is vertical
- Radial effects appear as tall ellipses unless corrected
- Vertical effects (rain, embers, fire columns) are naturally enhanced
- Horizontal effects (spectrum bars, waveforms) need rotation or compression
**Grid sizing for text in portrait**: Use `lg` (20px) for 2-3 word lines. Max comfortable line length is ~25-30 chars. For longer quotes, break aggressively into many short lines stacked vertically — portrait has vertical space to spare. `xl` (24px) works for single words or very short phrases.
Grid dimensions: `cols = VW // cell_width`, `rows = VH // cell_height`.
@ -59,7 +117,23 @@ FONT_PREFS_LINUX = [
("Noto Sans Mono", "/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf"),
("Ubuntu Mono", "/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf"),
]
FONT_PREFS = FONT_PREFS_MACOS if platform.system() == "Darwin" else FONT_PREFS_LINUX
FONT_PREFS_WINDOWS = [
("Consolas", r"C:\Windows\Fonts\consola.ttf"),
("Courier New", r"C:\Windows\Fonts\cour.ttf"),
("Lucida Console", r"C:\Windows\Fonts\lucon.ttf"),
("Cascadia Code", os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Windows\Fonts\CascadiaCode.ttf")),
("Cascadia Mono", os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Windows\Fonts\CascadiaMono.ttf")),
]
def _get_font_prefs():
s = platform.system()
if s == "Darwin":
return FONT_PREFS_MACOS
elif s == "Windows":
return FONT_PREFS_WINDOWS
return FONT_PREFS_LINUX
FONT_PREFS = _get_font_prefs()
```
**Multi-font rendering**: use different fonts for different layers (e.g., monospace for background, a bolder variant for overlay text). Each GridLayer owns its own font:
@ -77,8 +151,8 @@ Before initializing grids, gather all characters that need bitmap pre-rasterizat
all_chars = set()
for pal in [PAL_DEFAULT, PAL_DENSE, PAL_BLOCKS, PAL_RUNE, PAL_KATA,
PAL_GREEK, PAL_MATH, PAL_DOTS, PAL_BRAILLE, PAL_STARS,
PAL_BINARY, PAL_MUSIC, PAL_BOX, PAL_CIRCUIT, PAL_ARROWS,
PAL_HERMES]: # ... all palettes used in project
PAL_HALFFILL, PAL_HATCH, PAL_BINARY, PAL_MUSIC, PAL_BOX,
PAL_CIRCUIT, PAL_ARROWS, PAL_HERMES]: # ... all palettes used in project
all_chars.update(pal)
# Add any overlay text characters
all_chars.update("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,-:;!?/|")
@ -87,21 +161,31 @@ all_chars.discard(" ") # space is never rendered
### GridLayer Initialization
Each grid pre-computes coordinate arrays for vectorized effect math:
Each grid pre-computes coordinate arrays for vectorized effect math. The grid automatically adapts to any resolution (landscape, portrait, square):
```python
class GridLayer:
def __init__(self, font_path, font_size):
def __init__(self, font_path, font_size, vw=None, vh=None):
"""Initialize grid for any resolution.
vw, vh: video width/height in pixels. Defaults to global VW, VH."""
vw = vw or VW; vh = vh or VH
self.vw = vw; self.vh = vh
self.font = ImageFont.truetype(font_path, font_size)
asc, desc = self.font.getmetrics()
bbox = self.font.getbbox("M")
self.cw = bbox[2] - bbox[0] # character cell width
self.ch = asc + desc # CRITICAL: not textbbox height
self.cols = VW // self.cw
self.rows = VH // self.ch
self.ox = (VW - self.cols * self.cw) // 2 # centering
self.oy = (VH - self.rows * self.ch) // 2
self.cols = vw // self.cw
self.rows = vh // self.ch
self.ox = (vw - self.cols * self.cw) // 2 # centering
self.oy = (vh - self.rows * self.ch) // 2
# Aspect ratio metadata
self.aspect = vw / vh # >1 = landscape, <1 = portrait, 1 = square
self.is_portrait = vw < vh
self.is_landscape = vw > vh
# Index arrays
self.rr = np.arange(self.rows, dtype=np.float32)[:, None]
@ -219,9 +303,11 @@ PAL_ARABIC = " \u0627\u0628\u062a\u062b\u062c\u062d\u062e\u062f\u0630\u0631\u0
#### Dot / Point Progressions
```python
PAL_DOTS = " \u22c5\u2218\u2219\u25cf\u25c9\u25ce\u25c6\u2726\u2605" # dot size progression
PAL_BRAILLE = " \u2801\u2802\u2803\u2804\u2805\u2806\u2807\u2808\u2809\u280a\u280b\u280c\u280d\u280e\u280f\u2810\u2811\u2812\u2813\u2814\u2815\u2816\u2817\u2818\u2819\u281a\u281b\u281c\u281d\u281e\u281f\u283f" # braille patterns
PAL_STARS = " \u00b7\u2727\u2726\u2729\u2728\u2605\u2736\u2733\u2738" # star progression
PAL_DOTS = " ⋅∘∙●◉◎◆✦★" # dot size progression
PAL_BRAILLE = " ⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠿" # braille patterns
PAL_STARS = " ·✧✦✩✨★✶✳✸" # star progression
PAL_HALFFILL = " ◔◑◕◐◒◓◖◗◙" # directional half-fill progression
PAL_HATCH = " ▣▤▥▦▧▨▩" # crosshatch density ramp
```
#### Project-Specific (examples -- invent new ones per project)
@ -353,6 +439,202 @@ def rgb_palette_map(val, mask, palette):
return R, G, B
```
### OKLAB Color Space (Perceptually Uniform)
HSV hue is perceptually non-uniform: green occupies far more visual range than blue. OKLAB / OKLCH provide perceptually even color steps — hue increments of 0.1 look equally different regardless of starting hue. Use OKLAB for:
- Gradient interpolation (no unwanted intermediate hues)
- Color harmony generation (perceptually balanced palettes)
- Smooth color transitions over time
```python
# --- sRGB <-> Linear sRGB ---
def srgb_to_linear(c):
"""Convert sRGB [0,1] to linear light. c: float32 array."""
return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
def linear_to_srgb(c):
"""Convert linear light to sRGB [0,1]."""
return np.where(c <= 0.0031308, c * 12.92, 1.055 * np.power(np.maximum(c, 0), 1/2.4) - 0.055)
# --- Linear sRGB <-> OKLAB ---
def linear_rgb_to_oklab(r, g, b):
"""Linear sRGB to OKLAB. r,g,b: float32 arrays [0,1].
Returns (L, a, b) where L=[0,1], a,b=[-0.4, 0.4] approx."""
l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
l_c = np.cbrt(l_); m_c = np.cbrt(m_); s_c = np.cbrt(s_)
L = 0.2104542553 * l_c + 0.7936177850 * m_c - 0.0040720468 * s_c
a = 1.9779984951 * l_c - 2.4285922050 * m_c + 0.4505937099 * s_c
b_ = 0.0259040371 * l_c + 0.7827717662 * m_c - 0.8086757660 * s_c
return L, a, b_
def oklab_to_linear_rgb(L, a, b):
"""OKLAB to linear sRGB. Returns (r, g, b) float32 arrays [0,1]."""
l_ = L + 0.3963377774 * a + 0.2158037573 * b
m_ = L - 0.1055613458 * a - 0.0638541728 * b
s_ = L - 0.0894841775 * a - 1.2914855480 * b
l_c = l_ ** 3; m_c = m_ ** 3; s_c = s_ ** 3
r = +4.0767416621 * l_c - 3.3077115913 * m_c + 0.2309699292 * s_c
g = -1.2684380046 * l_c + 2.6097574011 * m_c - 0.3413193965 * s_c
b_ = -0.0041960863 * l_c - 0.7034186147 * m_c + 1.7076147010 * s_c
return np.clip(r, 0, 1), np.clip(g, 0, 1), np.clip(b_, 0, 1)
# --- Convenience: sRGB uint8 <-> OKLAB ---
def rgb_to_oklab(R, G, B):
"""sRGB uint8 arrays to OKLAB."""
r = srgb_to_linear(R.astype(np.float32) / 255.0)
g = srgb_to_linear(G.astype(np.float32) / 255.0)
b = srgb_to_linear(B.astype(np.float32) / 255.0)
return linear_rgb_to_oklab(r, g, b)
def oklab_to_rgb(L, a, b):
"""OKLAB to sRGB uint8 arrays."""
r, g, b_ = oklab_to_linear_rgb(L, a, b)
R = np.clip(linear_to_srgb(r) * 255, 0, 255).astype(np.uint8)
G = np.clip(linear_to_srgb(g) * 255, 0, 255).astype(np.uint8)
B = np.clip(linear_to_srgb(b_) * 255, 0, 255).astype(np.uint8)
return R, G, B
# --- OKLCH (cylindrical form of OKLAB) ---
def oklab_to_oklch(L, a, b):
"""OKLAB to OKLCH. Returns (L, C, H) where H is in [0, 1] (normalized)."""
C = np.sqrt(a**2 + b**2)
H = (np.arctan2(b, a) / (2 * np.pi)) % 1.0
return L, C, H
def oklch_to_oklab(L, C, H):
"""OKLCH to OKLAB. H in [0, 1]."""
angle = H * 2 * np.pi
a = C * np.cos(angle)
b = C * np.sin(angle)
return L, a, b
```
### Gradient Interpolation (OKLAB vs HSV)
Interpolating colors through OKLAB avoids the hue detours that HSV produces:
```python
def lerp_oklab(color_a, color_b, t_array):
"""Interpolate between two sRGB colors through OKLAB.
color_a, color_b: (R, G, B) tuples 0-255
t_array: float32 array [0,1] — interpolation parameter per pixel.
Returns (R, G, B) uint8 arrays."""
La, aa, ba = rgb_to_oklab(
np.full_like(t_array, color_a[0], dtype=np.uint8),
np.full_like(t_array, color_a[1], dtype=np.uint8),
np.full_like(t_array, color_a[2], dtype=np.uint8))
Lb, ab, bb = rgb_to_oklab(
np.full_like(t_array, color_b[0], dtype=np.uint8),
np.full_like(t_array, color_b[1], dtype=np.uint8),
np.full_like(t_array, color_b[2], dtype=np.uint8))
L = La + (Lb - La) * t_array
a = aa + (ab - aa) * t_array
b = ba + (bb - ba) * t_array
return oklab_to_rgb(L, a, b)
def lerp_oklch(color_a, color_b, t_array, short_path=True):
"""Interpolate through OKLCH (preserves chroma, smooth hue path).
short_path: take the shorter arc around the hue wheel."""
La, aa, ba = rgb_to_oklab(
np.full_like(t_array, color_a[0], dtype=np.uint8),
np.full_like(t_array, color_a[1], dtype=np.uint8),
np.full_like(t_array, color_a[2], dtype=np.uint8))
Lb, ab, bb = rgb_to_oklab(
np.full_like(t_array, color_b[0], dtype=np.uint8),
np.full_like(t_array, color_b[1], dtype=np.uint8),
np.full_like(t_array, color_b[2], dtype=np.uint8))
L1, C1, H1 = oklab_to_oklch(La, aa, ba)
L2, C2, H2 = oklab_to_oklch(Lb, ab, bb)
# Shortest hue path
if short_path:
dh = H2 - H1
dh = np.where(dh > 0.5, dh - 1.0, np.where(dh < -0.5, dh + 1.0, dh))
H = (H1 + dh * t_array) % 1.0
else:
H = H1 + (H2 - H1) * t_array
L = L1 + (L2 - L1) * t_array
C = C1 + (C2 - C1) * t_array
Lout, aout, bout = oklch_to_oklab(L, C, H)
return oklab_to_rgb(Lout, aout, bout)
```
### Color Harmony Generation
Auto-generate harmonious palettes from a seed color:
```python
def harmony_complementary(seed_rgb):
"""Two colors: seed + opposite hue."""
L, a, b = rgb_to_oklab(np.array([seed_rgb[0]]), np.array([seed_rgb[1]]), np.array([seed_rgb[2]]))
_, C, H = oklab_to_oklch(L, a, b)
return [seed_rgb, _oklch_to_srgb_tuple(L[0], C[0], (H[0] + 0.5) % 1.0)]
def harmony_triadic(seed_rgb):
"""Three colors: seed + two at 120-degree offsets."""
L, a, b = rgb_to_oklab(np.array([seed_rgb[0]]), np.array([seed_rgb[1]]), np.array([seed_rgb[2]]))
_, C, H = oklab_to_oklch(L, a, b)
return [seed_rgb,
_oklch_to_srgb_tuple(L[0], C[0], (H[0] + 0.333) % 1.0),
_oklch_to_srgb_tuple(L[0], C[0], (H[0] + 0.667) % 1.0)]
def harmony_analogous(seed_rgb, spread=0.08, n=5):
"""N colors spread evenly around seed hue."""
L, a, b = rgb_to_oklab(np.array([seed_rgb[0]]), np.array([seed_rgb[1]]), np.array([seed_rgb[2]]))
_, C, H = oklab_to_oklch(L, a, b)
offsets = np.linspace(-spread * (n-1)/2, spread * (n-1)/2, n)
return [_oklch_to_srgb_tuple(L[0], C[0], (H[0] + off) % 1.0) for off in offsets]
def harmony_split_complementary(seed_rgb, split=0.08):
"""Three colors: seed + two flanking the complement."""
L, a, b = rgb_to_oklab(np.array([seed_rgb[0]]), np.array([seed_rgb[1]]), np.array([seed_rgb[2]]))
_, C, H = oklab_to_oklch(L, a, b)
comp = (H[0] + 0.5) % 1.0
return [seed_rgb,
_oklch_to_srgb_tuple(L[0], C[0], (comp - split) % 1.0),
_oklch_to_srgb_tuple(L[0], C[0], (comp + split) % 1.0)]
def harmony_tetradic(seed_rgb):
"""Four colors: two complementary pairs at 90-degree offset."""
L, a, b = rgb_to_oklab(np.array([seed_rgb[0]]), np.array([seed_rgb[1]]), np.array([seed_rgb[2]]))
_, C, H = oklab_to_oklch(L, a, b)
return [seed_rgb,
_oklch_to_srgb_tuple(L[0], C[0], (H[0] + 0.25) % 1.0),
_oklch_to_srgb_tuple(L[0], C[0], (H[0] + 0.5) % 1.0),
_oklch_to_srgb_tuple(L[0], C[0], (H[0] + 0.75) % 1.0)]
def _oklch_to_srgb_tuple(L, C, H):
"""Helper: single OKLCH -> sRGB (R,G,B) int tuple."""
La = np.array([L]); Ca = np.array([C]); Ha = np.array([H])
Lo, ao, bo = oklch_to_oklab(La, Ca, Ha)
R, G, B = oklab_to_rgb(Lo, ao, bo)
return (int(R[0]), int(G[0]), int(B[0]))
```
### OKLAB Hue Fields
Drop-in replacements for `hf_*` generators that produce perceptually uniform hue variation:
```python
def hf_oklch_angle(offset=0.0, chroma=0.12, lightness=0.7):
"""OKLCH hue mapped to angle from center. Perceptually uniform rainbow.
Returns (R, G, B) uint8 color array instead of a float hue.
NOTE: Use with _render_vf_rgb() variant, not standard _render_vf()."""
def fn(g, f, t, S):
H = (g.angle / (2 * np.pi) + offset + t * 0.05) % 1.0
L = np.full_like(H, lightness)
C = np.full_like(H, chroma)
Lo, ao, bo = oklch_to_oklab(L, C, H)
R, G, B = oklab_to_rgb(Lo, ao, bo)
return mkc(R, G, B, g.rows, g.cols)
return fn
```
### Compositing Helpers
```python
@ -458,7 +740,7 @@ subprocess.run(["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_path,
### v2 Protocol (Current)
Every scene function: `(renderer, features_dict, time_float, state_dict) -> canvas_uint8`
Every scene function: `(r, f, t, S) -> canvas_uint8` — where `r` = Renderer, `f` = features dict, `t` = time float, `S` = persistent state dict
```python
def fx_example(r, f, t, S):