- 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)
7.2 KiB
Scene Design Patterns
Cross-references:
- Scene protocol, SCENES table:
scenes.md - Blend modes, multi-grid composition, tonemap:
composition.md - Effect building blocks (value fields, noise, SDFs):
effects.md - Shader pipeline, feedback buffer:
shaders.md - Complete scene examples:
examples.md
Higher-order patterns for composing scenes that feel intentional rather than random. These patterns use the existing building blocks (value fields, blend modes, shaders, feedback) but organize them with compositional intent.
Layer Hierarchy
Every scene should have clear visual layers with distinct roles:
| Layer | Grid | Brightness | Purpose |
|---|---|---|---|
| Background | xs or sm (dense) | 0.1–0.25 | Atmosphere, texture. Never competes with content. |
| Content | md (balanced) | 0.4–0.8 | The main visual idea. Carries the scene's concept. |
| Accent | lg or sm (sparse) | 0.5–1.0 (sparse coverage) | Highlights, punctuation, sparse bright points. |
The background sets mood. The content layer is what the scene is about. The accent adds visual interest without overwhelming.
def fx_example(r, f, t, S):
local = t
progress = min(local / 5.0, 1.0)
g_bg = r.get_grid("sm")
g_main = r.get_grid("md")
g_accent = r.get_grid("lg")
# --- Background: dim atmosphere ---
bg_val = vf_smooth_noise(g_bg, f, t * 0.3, S, octaves=2, bri=0.15)
# ... render bg to canvas
# --- Content: the main visual idea ---
content_val = vf_spiral(g_main, f, t, S, n_arms=n_arms, tightness=tightness)
# ... render content on top of canvas
# --- Accent: sparse highlights ---
accent_val = vf_noise_static(g_accent, f, t, S, density=0.05)
# ... render accent on top
return canvas
Directional Parameter Arcs
Parameters should go somewhere over the scene's duration — not oscillate aimlessly with sin(t * N).
Bad: twist = 3.0 + 2.0 * math.sin(t * 0.6) — wobbles back and forth, feels aimless.
Good: twist = 2.0 + progress * 5.0 — starts gentle, ends intense. The scene builds.
Use progress = min(local / duration, 1.0) (0→1 over the scene) to drive directional change:
| Pattern | Formula | Feel |
|---|---|---|
| Linear ramp | progress * range |
Steady buildup |
| Ease-out | 1 - (1 - progress) ** 2 |
Fast start, gentle finish |
| Ease-in | progress ** 2 |
Slow start, accelerating |
| Step reveal | np.clip((progress - 0.5) / 0.25, 0, 1) |
Nothing until 50%, then fades in |
| Build + plateau | min(1.0, progress * 1.5) |
Reaches full at 67%, holds |
Oscillation is fine for secondary parameters (saturation shimmer, hue drift). But the defining parameter of the scene should have a direction.
Examples of Directional Arcs
| Scene concept | Parameter | Arc |
|---|---|---|
| Emergence | Ring radius | 0 → max (ease-out) |
| Shatter | Voronoi cell count | 8 → 38 (linear) |
| Descent | Tunnel speed | 2.0 → 10.0 (linear) |
| Mandala | Shape complexity | ring → +polygon → +star → +rosette (step reveals) |
| Crescendo | Layer count | 1 → 7 (staggered entry) |
| Entropy | Geometry visibility | 1.0 → 0.0 (consumed) |
Scene Concepts
Each scene should be built around a visual idea, not an effect name.
Bad: "fx_plasma_cascade" — named after the effect. No concept. Good: "fx_emergence" — a point of light expands into a field. The name tells you what happens.
Good scene concepts have:
- A visual metaphor (emergence, descent, collision, entropy)
- A directional arc (things change from A to B, not oscillate)
- Motivated layer choices (each layer serves the concept)
- Motivated feedback (transform direction matches the metaphor)
| Concept | Metaphor | Feedback transform | Why |
|---|---|---|---|
| Emergence | Birth, expansion | zoom-out | Past frames expand outward |
| Descent | Falling, acceleration | zoom-in | Past frames rush toward center |
| Inferno | Rising fire | shift-up | Past frames rise with the flames |
| Entropy | Decay, dissolution | none | Clean, no persistence — things disappear |
| Crescendo | Accumulation | zoom + hue_shift | Everything compounds and shifts |
Compositional Techniques
Counter-Rotating Dual Systems
Two instances of the same effect rotating in opposite directions create visual interference:
# Primary spiral (clockwise)
s1_val = vf_spiral(g_main, f, t * 1.5, S, n_arms=n_arms_1, tightness=tightness_1)
# Counter-rotating spiral (counter-clockwise via negative time)
s2_val = vf_spiral(g_accent, f, -t * 1.2, S, n_arms=n_arms_2, tightness=tightness_2)
# Screen blend creates bright interference at crossing points
canvas = blend_canvas(canvas_with_s1, c2, "screen", 0.7)
Works with spirals, vortexes, rings. The counter-rotation creates constantly shifting interference patterns.
Wave Collision
Two wave fronts converging from opposite sides, meeting at a collision point:
collision_phase = abs(progress - 0.5) * 2 # 1→0→1 (0 at collision)
# Wave A approaches from left
offset_a = (1 - progress) * g.cols * 0.4
wave_a = np.sin((g.cc + offset_a) * 0.08 + t * 2) * 0.5 + 0.5
# Wave B approaches from right
offset_b = -(1 - progress) * g.cols * 0.4
wave_b = np.sin((g.cc + offset_b) * 0.08 - t * 2) * 0.5 + 0.5
# Interference peaks at collision
combined = wave_a * 0.5 + wave_b * 0.5 + np.abs(wave_a - wave_b) * (1 - collision_phase) * 0.5
Progressive Fragmentation
Voronoi with cell count increasing over time — visual shattering:
n_pts = int(8 + progress * 30) # 8 cells → 38 cells
# Pre-generate enough points, slice to n_pts
px = base_x[:n_pts] + np.sin(t * 0.3 + np.arange(n_pts) * 0.7) * (3 + progress * 3)
The edge glow width can also increase with progress to emphasize the cracks.
Entropy / Consumption
A clean geometric pattern being overtaken by an organic process:
# Geometry fades out
geo_val = clean_pattern * max(0.05, 1.0 - progress * 0.9)
# Organic process grows in
rd_val = vf_reaction_diffusion(g, f, t, S) * min(1.0, progress * 1.5)
# Render geometry first, organic on top — organic consumes geometry
Staggered Layer Entry (Crescendo)
Layers enter one at a time, building to overwhelming density:
def layer_strength(enter_t, ramp=1.5):
"""0.0 until enter_t, ramps to 1.0 over ramp seconds."""
return max(0.0, min(1.0, (local - enter_t) / ramp))
# Layer 1: always present
s1 = layer_strength(0.0)
# Layer 2: enters at 2s
s2 = layer_strength(2.0)
# Layer 3: enters at 4s
s3 = layer_strength(4.0)
# ... etc
# Each layer uses a different effect, grid, palette, and blend mode
# Screen blend between layers so they accumulate light
For a 15-second crescendo, 7 layers entering every 2 seconds works well. Use different blend modes (screen for most, add for energy, colordodge for the final wash).
Scene Ordering
For a multi-scene reel or video:
- Vary mood between adjacent scenes — don't put two calm scenes next to each other
- Randomize order rather than grouping by type — prevents "effect demo" feel
- End on the strongest scene — crescendo or something with a clear payoff
- Open with energy — grab attention in the first 2 seconds