From 064eaaee3dbe4210fda3c45a09ac33e673d432ee Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Thu, 19 Mar 2026 00:32:52 -0700 Subject: [PATCH] docs: add figment mode design spec Hybrid plugin + overlay architecture for periodic SVG glyph display with theme-aware coloring and extensible input abstraction. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + .../specs/2026-03-19-figment-mode-design.md | 225 ++++++++++++++++++ ...of-mexico-antique-cultures-svgrepo-com.svg | 32 +++ figments/mayan-mask-of-mexico-svgrepo-com.svg | 60 +++++ .../mayan-symbol-of-mexico-svgrepo-com.svg | 110 +++++++++ 5 files changed, 428 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-19-figment-mode-design.md create mode 100644 figments/animal-head-symbol-of-mexico-antique-cultures-svgrepo-com.svg create mode 100644 figments/mayan-mask-of-mexico-svgrepo-com.svg create mode 100644 figments/mayan-symbol-of-mexico-svgrepo-com.svg diff --git a/.gitignore b/.gitignore index 590c496..573af53 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ htmlcov/ .coverage .pytest_cache/ *.egg-info/ +.DS_Store diff --git a/docs/superpowers/specs/2026-03-19-figment-mode-design.md b/docs/superpowers/specs/2026-03-19-figment-mode-design.md new file mode 100644 index 0000000..d75c346 --- /dev/null +++ b/docs/superpowers/specs/2026-03-19-figment-mode-design.md @@ -0,0 +1,225 @@ +# Figment Mode Design Spec + +> Periodic full-screen SVG glyph overlay with flickery animation, theme-aware coloring, and extensible physical device control. + +## Overview + +Figment mode displays a randomly selected SVG from the `figments/` directory as a flickery, glitchy half-block terminal overlay on top of the running ticker. It appears once per minute (configurable), holds for ~4.5 seconds with a three-phase animation (progressive reveal, strobing hold, dissolve), then fades back to the ticker. Colors are randomly chosen from the existing theme gradients. + +The feature is designed for extensibility: a generic input protocol allows MQTT, ntfy, serial, or any other control surface to trigger figments and adjust parameters in real time. + +## Architecture: Hybrid Plugin + Overlay + +The figment is an **EffectPlugin** for lifecycle, discovery, and configuration, but delegates rendering to a **layers-style overlay helper**. This avoids stretching the `EffectPlugin.process()` contract (which transforms line buffers) while still benefiting from the plugin system for C&C, auto-discovery, and config management. + +### Component Diagram + +``` + +-------------------+ + | FigmentTrigger | (Protocol) + | - NtfyTrigger | (v1) + | - MqttTrigger | (future) + | - SerialTrigger | (future) + +--------+----------+ + | + | FigmentCommand + v ++------------------+ +-----------------+ +----------------------+ +| figment_render |<---| FigmentPlugin |--->| render_figment_ | +| .py | | (EffectPlugin) | | overlay() in | +| | | | | layers.py | +| SVG -> PIL -> | | Timer, state | | | +| half-block cache | | machine, SVG | | ANSI cursor-position | +| | | selection | | commands for overlay | ++------------------+ +-----------------+ +----------------------+ + | + | get_figment_state() + v + +-------------------+ + | scroll.py | + | (5-line block) | + +-------------------+ +``` + +## Section 1: SVG Rasterization + +**File: `engine/figment_render.py`** + +Reuses the same PIL-based half-block encoding that `engine/render.py` uses for OTF fonts. + +### Pipeline + +1. **Load**: `cairosvg.svg2png()` converts SVG to PNG bytes in memory (no temp files) +2. **Resize**: PIL scales to fit terminal — width = `tw()`, height = `th() * 2` pixels (each terminal row encodes 2 pixel rows via half-blocks) +3. **Threshold**: Convert to greyscale ("L" mode), apply binary threshold to get visible/not-visible +4. **Half-block encode**: Walk pixel pairs top-to-bottom. For each 2-row pair, emit `█` (both lit), `▀` (top only), `▄` (bottom only), or space (neither) +5. **Cache**: Results cached per `(svg_path, terminal_width, terminal_height)` — invalidated on terminal resize + +### Dependency + +`cairosvg` added as an optional dependency in `pyproject.toml` (like `sounddevice`). Figment mode gracefully degrades if unavailable. + +### Key Function + +```python +def rasterize_svg(svg_path: str, width: int, height: int) -> list[str]: + """Convert SVG file to list of half-block terminal rows (uncolored).""" +``` + +## Section 2: Figment Overlay Rendering + +**Integration point: `engine/layers.py`** + +New function following the `render_message_overlay()` pattern. + +### Function Signature + +```python +def render_figment_overlay(figment_state: FigmentState, w: int, h: int) -> list[str]: + """Return ANSI cursor-positioning commands for the current figment frame.""" +``` + +### Animation Phases (~4.5 seconds total) + +| Phase | Duration | Behavior | +|-------|----------|----------| +| **Reveal** | ~1.5s | Progressive scanline fill. Each frame, a percentage of the figment's non-empty cells become visible in random block order. | +| **Hold** | ~1.5s | Full figment visible. Strobes between full brightness and dimmed/partial visibility every few frames. | +| **Dissolve** | ~1.5s | Inverse of reveal. Cells randomly drop out, replaced by spaces. | + +### Color + +A random theme gradient is selected from `THEME_REGISTRY` at trigger time. Applied via `lr_gradient()` — the same function that colors headlines and messages. + +### Positioning + +Figment is centered in the viewport. Each visible row is an ANSI `\033[row;colH` command appended to the buffer, identical to how the message overlay works. + +## Section 3: FigmentPlugin (Effect Plugin) + +**File: `effects_plugins/figment.py`** + +An `EffectPlugin(ABC)` subclass that owns the figment lifecycle. + +### Responsibilities + +- **Timer**: Tracks elapsed frames. At the configured interval (default 60s), triggers a new figment. +- **SVG selection**: Randomly picks from `figments/*.svg`. Avoids repeating the last shown. +- **State machine**: `idle -> reveal -> hold -> dissolve -> idle`. Tracks phase progress (0.0 to 1.0). +- **Color selection**: Picks a random theme key (`"green"`, `"orange"`, `"purple"`) at trigger time. +- **Rasterization**: Calls `rasterize_svg()` on trigger, caches result for the display duration. + +### State Machine + +``` +idle ──(timer fires or trigger received)──> reveal +reveal ──(progress >= 1.0)──> hold +hold ──(progress >= 1.0)──> dissolve +dissolve ──(progress >= 1.0)──> idle +``` + +### Interface + +The `process()` method is a no-op (returns buffer unchanged) since rendering is handled by the overlay. The plugin exposes state via: + +```python +def get_figment_state(self, frame_number: int) -> FigmentState | None: + """Tick the state machine and return current state, or None if idle.""" +``` + +This mirrors the `ntfy_poller.get_active_message()` pattern. + +### EffectConfig + +- `enabled`: bool (default `False` — opt-in) +- `intensity`: float — scales strobe frequency and reveal/dissolve speed +- `params`: + - `interval_secs`: 60 (time between figments) + - `display_secs`: 4.5 (total animation duration) + - `figment_dir`: "figments" (SVG source directory) + +Controllable via C&C: `/effects figment on`, `/effects figment intensity 0.7`. + +## Section 4: Input Abstraction (FigmentTrigger) + +**File: `engine/figment_trigger.py`** + +### Protocol + +```python +class FigmentTrigger(Protocol): + def poll(self) -> FigmentCommand | None: ... +``` + +### FigmentCommand + +```python +@dataclass +class FigmentCommand: + action: str # "trigger" | "set_intensity" | "set_interval" | "set_color" | "stop" + value: float | str | None = None +``` + +### Adapters + +| Adapter | Transport | Dependency | Status | +|---------|-----------|------------|--------| +| `NtfyTrigger` | Existing C&C ntfy topic | None (reuses ntfy) | v1 | +| `MqttTrigger` | MQTT broker | `paho-mqtt` (optional) | Future | +| `SerialTrigger` | USB serial | `pyserial` (optional) | Future | + +### Integration + +The `FigmentPlugin` accepts a list of triggers. Each frame, it polls all triggers and acts on commands. Triggers are optional — if none are configured, the plugin runs on its internal timer alone. + +### EventBus Bridge + +Any trigger can publish a `FIGMENT_TRIGGER` event to the EventBus, allowing other components to react (logging, multi-display sync). + +## Section 5: Scroll Loop Integration + +Minimal change to `engine/scroll.py` — 5 lines following the ntfy overlay pattern: + +```python +# In stream() setup: +figment_plugin = registry.get("figment") + +# In frame loop, after effects processing, before display.show(): +if figment_plugin and figment_plugin.config.enabled: + figment_state = figment_plugin.get_figment_state(frame_number) + if figment_state is not None: + figment_overlay = render_figment_overlay(figment_state, w, h) + buf.extend(figment_overlay) +``` + +### Overlay Priority + +Figment overlay appends **after** effects processing but **before** the ntfy message overlay. This means: +- Ntfy messages always appear on top of figments (higher priority) +- Existing glitch/noise effects run over the ticker underneath the figment + +## Section 6: File Summary + +### New Files + +| File | Purpose | +|------|---------| +| `effects_plugins/figment.py` | FigmentPlugin — lifecycle, timer, state machine, SVG selection | +| `engine/figment_render.py` | SVG to half-block rasterization pipeline | +| `engine/figment_trigger.py` | FigmentTrigger protocol, FigmentCommand, NtfyTrigger adapter | +| `tests/test_figment.py` | Plugin lifecycle, state machine, timer, overlay rendering | +| `tests/fixtures/test.svg` | Minimal SVG for testing rasterization | + +### Modified Files + +| File | Change | +|------|--------| +| `engine/scroll.py` | 5-line figment overlay integration | +| `engine/layers.py` | Add `render_figment_overlay()` function | +| `pyproject.toml` | Add `cairosvg` as optional dependency | + +## Testing Strategy + +- **Unit**: State machine transitions, timer accuracy, SVG rasterization output, FigmentCommand parsing +- **Integration**: Plugin discovery, overlay rendering with mock terminal dimensions, C&C command handling +- **Fixture**: Minimal `test.svg` (simple shape) for deterministic rasterization tests diff --git a/figments/animal-head-symbol-of-mexico-antique-cultures-svgrepo-com.svg b/figments/animal-head-symbol-of-mexico-antique-cultures-svgrepo-com.svg new file mode 100644 index 0000000..264eb08 --- /dev/null +++ b/figments/animal-head-symbol-of-mexico-antique-cultures-svgrepo-com.svg @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/figments/mayan-mask-of-mexico-svgrepo-com.svg b/figments/mayan-mask-of-mexico-svgrepo-com.svg new file mode 100644 index 0000000..75fca60 --- /dev/null +++ b/figments/mayan-mask-of-mexico-svgrepo-com.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/figments/mayan-symbol-of-mexico-svgrepo-com.svg b/figments/mayan-symbol-of-mexico-svgrepo-com.svg new file mode 100644 index 0000000..a396536 --- /dev/null +++ b/figments/mayan-symbol-of-mexico-svgrepo-com.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file