"""Motion blur effect using frame history.""" from engine.effects.types import EffectConfig, EffectContext, EffectPlugin class MotionBlurEffect(EffectPlugin): """Apply motion blur by blending current frame with previous frames. This effect requires a FrameBufferStage to be present in the pipeline. The framebuffer provides frame history which is blended with the current frame based on intensity. Attributes: name: "motionblur" config: EffectConfig with intensity parameter (0.0-1.0) param_bindings: Optional sensor bindings for intensity modulation Example: >>> effect = MotionBlurEffect() >>> effect.configure(EffectConfig(intensity=0.5)) >>> result = effect.process(buffer, ctx) """ name = "motionblur" config: EffectConfig = EffectConfig(enabled=True, intensity=0.5) param_bindings: dict[str, dict[str, str | float]] = {} supports_partial_updates = False def process(self, buf: list[str], ctx: EffectContext) -> list[str]: """Apply motion blur by blending with previous frames. Args: buf: Current text buffer (list of strings) ctx: Effect context with access to framebuffer history Returns: Blended buffer with motion blur effect applied """ if not buf: return buf # Get framebuffer history from context # We'll look for the first available framebuffer history history = None for key in ctx.state: if key.startswith("framebuffer.") and key.endswith(".history"): history = ctx.state[key] break if not history: # No framebuffer available, return unchanged 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 decay factor (how quickly older frames fade) decay = self.config.params.get("decay", 0.7) # Build output buffer result = [] viewport_height = ctx.terminal_height - ctx.ticker_height # Determine how many frames to blend (up to history depth) max_frames = min(len(history), 5) # Cap at 5 frames for performance for row in range(len(buf)): if row >= viewport_height: # Beyond viewport, just copy result.append(buf[row]) continue # Start with current frame blended = buf[row] # Blend with historical frames weight_sum = 1.0 if max_frames > 0 and intensity > 0: for i in range(max_frames): frame_weight = intensity * (decay**i) if frame_weight < 0.01: # Skip negligible weights break hist_row = history[i][row] if row < len(history[i]) else "" # Simple string blending: we'll concatenate with space # For a proper effect, we'd need to blend ANSI colors # This is a simplified version that just adds the frames blended = self._blend_strings(blended, hist_row, frame_weight) weight_sum += frame_weight result.append(blended) return result def _blend_strings(self, current: str, historical: str, weight: float) -> str: """Blend two strings with given weight. This is a simplified blending that works with ANSI codes. For proper blending we'd need to parse colors, but for now we use a heuristic: if strings are identical, return one. If they differ, we alternate or concatenate based on weight. """ if current == historical: return current # If weight is high, show current; if low, show historical if weight > 0.5: return current else: return historical def configure(self, config: EffectConfig) -> None: """Configure the effect.""" self.config = config