"""Fire animation — Fire2012 algorithm port from FastLED.""" import random from lightsync.animations.base import AnimationBase def _fire_step( heat: list[int], led_count: int, cooling: int, sparking: int, ) -> None: """Run one Fire2012 step, mutating heat array in-place. Index 0 = bottom of strip (heat source), index N-1 = top. """ # Step 1: Cool every cell for i in range(led_count): cooldown = random.randint(0, (cooling * 10 // led_count) + 2) heat[i] = max(0, heat[i] - cooldown) # Step 2: Heat drifts upward (toward index N-1) for i in range(led_count - 1, 1, -1): heat[i] = (heat[i - 1] + heat[i - 2] + heat[i - 2]) // 3 # Step 3: Randomly ignite new sparks near bottom if random.randint(0, 255) < sparking: y = random.randint(0, min(6, led_count - 1)) heat[y] = min(255, heat[y] + random.randint(160, 255)) def _heat_to_rgb(h: int) -> tuple[int, int, int]: """Map heat value 0-255 to black -> red -> yellow -> white palette.""" h = max(0, min(255, h)) if h < 85: return (h * 3, 0, 0) elif h < 170: h2 = h - 85 return (255, h2 * 3, 0) else: h2 = h - 170 return (255, 255, min(255, h2 * 3)) class FireAnimation(AnimationBase): """Fire2012 algorithm — heat diffusion with cooling, sparking, and color mapping. Index 0 = bottom of strip (heat source). Flames rise toward index N-1. State is per-instance (heat array) — each instance maintains its own fire. """ def __init__( self, cooling: int = 55, sparking: int = 120, speed: float = 0.5, ) -> None: self.cooling = cooling self.sparking = sparking self.speed = speed self._heat: list[int] = [] # lazy-init per led_count def render(self, t: float, led_count: int) -> list[tuple]: # Lazy-init or resize heat array if len(self._heat) != led_count: self._heat = [0] * led_count # Speed multiplier: higher speed -> more simulation steps per render steps = max(1, int(self.speed * 3)) for _ in range(steps): _fire_step(self._heat, led_count, self.cooling, self.sparking) return [_heat_to_rgb(h) for h in self._heat] @classmethod def from_params(cls, params: dict) -> "FireAnimation": return cls( cooling=params.get("cooling", 55), sparking=params.get("sparking", 120), speed=params.get("speed", 0.5), )