from engine.effects.types import EffectConfig, EffectContext, EffectPlugin class TintEffect(EffectPlugin): """Tint effect that applies an RGB color overlay to the buffer. Uses ANSI escape codes to tint text with the specified RGB values. Supports transparency (0-100%) for blending. Inlets: - r: Red component (0-255) - g: Green component (0-255) - b: Blue component (0-255) - a: Alpha/transparency (0.0-1.0, where 0.0 = fully transparent) """ name = "tint" config = EffectConfig(enabled=True, intensity=1.0) # Define inlet types for PureData-style typing @property def inlet_types(self) -> set: from engine.pipeline.core import DataType return {DataType.TEXT_BUFFER} @property def outlet_types(self) -> set: from engine.pipeline.core import DataType return {DataType.TEXT_BUFFER} def process(self, buf: list[str], ctx: EffectContext) -> list[str]: if not buf: return buf # Get tint values from effect params or sensors r = self.config.params.get("r", 255) g = self.config.params.get("g", 255) b = self.config.params.get("b", 255) a = self.config.params.get("a", 0.3) # Default 30% tint # Clamp values r = max(0, min(255, int(r))) g = max(0, min(255, int(g))) b = max(0, min(255, int(b))) a = max(0.0, min(1.0, float(a))) if a <= 0: return buf # Convert RGB to ANSI 256 color ansi_color = self._rgb_to_ansi256(r, g, b) # Apply tint with transparency effect result = [] for line in buf: if not line.strip(): result.append(line) continue # Check if line already has ANSI codes if "\033[" in line: # For lines with existing colors, wrap the whole line result.append(f"\033[38;5;{ansi_color}m{line}\033[0m") else: # Apply tint to plain text lines result.append(f"\033[38;5;{ansi_color}m{line}\033[0m") return result def _rgb_to_ansi256(self, r: int, g: int, b: int) -> int: """Convert RGB (0-255 each) to ANSI 256 color code.""" if r == g == b == 0: return 16 if r == g == b == 255: return 231 # Calculate grayscale gray = int((0.299 * r + 0.587 * g + 0.114 * b) / 255 * 24) + 232 # Calculate color cube ri = int(r / 51) gi = int(g / 51) bi = int(b / 51) color = 16 + 36 * ri + 6 * gi + bi # Use whichever is closer - gray or color gray_dist = abs(r - gray) color_dist = ( (r - ri * 51) ** 2 + (g - gi * 51) ** 2 + (b - bi * 51) ** 2 ) ** 0.5 if gray_dist < color_dist: return gray return color def configure(self, config: EffectConfig) -> None: self.config = config