"""Afterimage effect using previous frame.""" from engine.effects.types import EffectConfig, EffectContext, EffectPlugin class AfterimageEffect(EffectPlugin): """Show a faint ghost of the previous frame. This effect requires a FrameBufferStage to be present in the pipeline. It shows a dimmed version of the previous frame super-imposed on the current frame. Attributes: name: "afterimage" config: EffectConfig with intensity parameter (0.0-1.0) param_bindings: Optional sensor bindings for intensity modulation Example: >>> effect = AfterimageEffect() >>> effect.configure(EffectConfig(intensity=0.3)) >>> result = effect.process(buffer, ctx) """ name = "afterimage" config: EffectConfig = EffectConfig(enabled=True, intensity=0.3) param_bindings: dict[str, dict[str, str | float]] = {} supports_partial_updates = False def process(self, buf: list[str], ctx: EffectContext) -> list[str]: """Apply afterimage effect using the previous frame. Args: buf: Current text buffer (list of strings) ctx: Effect context with access to framebuffer history Returns: Buffer with ghost of previous frame overlaid """ if not buf: return buf # Get framebuffer history from context history = None for key in ctx.state: if key.startswith("framebuffer.") and key.endswith(".history"): history = ctx.state[key] break if not history or len(history) < 1: # No previous frame available return buf # Get intensity from config intensity = self.config.params.get("intensity", self.config.intensity) intensity = max(0.0, min(1.0, intensity)) if intensity <= 0.0: return buf # Get the previous frame (index 1, since index 0 is current) prev_frame = history[1] if len(history) > 1 else None if not prev_frame: return buf # Blend current and previous frames viewport_height = ctx.terminal_height - ctx.ticker_height result = [] for row in range(len(buf)): if row >= viewport_height: result.append(buf[row]) continue current_line = buf[row] prev_line = prev_frame[row] if row < len(prev_frame) else "" if not prev_line: result.append(current_line) continue # Apply dimming effect by reducing ANSI color intensity or adding transparency # For a simple text version, we'll use a blend strategy blended = self._blend_lines(current_line, prev_line, intensity) result.append(blended) return result def _blend_lines(self, current: str, previous: str, intensity: float) -> str: """Blend current and previous line with given intensity. For text with ANSI codes, true blending is complex. This is a simplified version that uses color averaging when possible. A more sophisticated implementation would: 1. Parse ANSI color codes from both lines 2. Blend RGB values based on intensity 3. Reconstruct the line with blended colors For now, we'll use a heuristic: if lines are similar, return current. If they differ, we alternate or use the previous as a faint overlay. """ if current == previous: return current # Simple blending: intensity determines mix # intensity=1.0 => fully current # intensity=0.3 => 70% previous ghost, 30% current if intensity > 0.7: return current elif intensity < 0.3: # Show previous but dimmed (simulate by adding faint color/gray) return previous # Would need to dim ANSI colors else: # For medium intensity, alternate based on character pattern # This is a placeholder for proper blending return current def configure(self, config: EffectConfig) -> None: """Configure the effect.""" self.config = config