diff --git a/animator/__init__.py b/animator/__init__.py index dd4e7eb..5f5626a 100644 --- a/animator/__init__.py +++ b/animator/__init__.py @@ -13,7 +13,9 @@ import neopixel_emu -from animator import light_funcs +from . import light_funcs +from . import _firework +from ._firework import FireworkArgs COLORS = [ (255, 0, 0), # Red @@ -75,6 +77,7 @@ class AnimationArgs: fade: FadeArgs = field(default_factory=FadeArgs) flash: FlashArgs = field(default_factory=FlashArgs) wipe: WipeArgs = field(default_factory=WipeArgs) + firework: FireworkArgs = field(default_factory=FireworkArgs) # Set the desired FPS for your animation @@ -265,6 +268,13 @@ def cycle(self) -> None: else: self.pixels[last_pixel + 1] = self.animation_args.wipe.colorb + self.pixels.brightness = self.animation_state.brightness / 255.0 + time.sleep(1 / FAST_FPS) + elif ( + self.animation_state.effect == "Firework" + and self.animation_state.state == "ON" + ): + _firework.firework_step(self.animation_args.firework, self.pixels) self.pixels.brightness = self.animation_state.brightness / 255.0 time.sleep(1 / FAST_FPS) elif ( diff --git a/animator/_firework.py b/animator/_firework.py new file mode 100644 index 0000000..dc8b286 --- /dev/null +++ b/animator/_firework.py @@ -0,0 +1,93 @@ +import random +from dataclasses import dataclass + +try: + import neopixel +except NotImplementedError: + pass + +import neopixel_emu + +@dataclass +class FireworkArgs: + num_sparks: int = 60 + gravity: float = -0.004 + brightness_decay: float = 0.985 + flare_min_vel: float = 0.5 + flare_max_vel: float = 0.9 + c1: float = 120 + c2: float = 50 + +def firework_step(settings: FireworkArgs, pixels: 'neopixel_emu.NeoPixel | neopixel.NeoPixel'): + # Reference: http://www.anirama.com/1000leds/1d-fireworks/ + + sparkPos = [0.0] * settings.num_sparks + sparkVel = [0.0] * settings.num_sparks + sparkCol = [0.0] * settings.num_sparks + + flarePos = 0 + flareVel = random.uniform(settings.flare_min_vel, settings.flare_max_vel) + brightness = 1.0 + + # Initialize launch sparks + for i in range(5): + sparkPos[i] = 0 + sparkVel[i] = (random.uniform(0, 1) / 255) * (flareVel / 5) + sparkCol[i] = sparkVel[i] * 1000 + sparkCol[i] = max(0, min(255, sparkCol[i])) + + # Launch + pixels.fill((0, 0, 0)) + while flareVel >= -0.2: + # Sparks + for i in range(5): + sparkPos[i] += sparkVel[i] + sparkPos[i] = max(0, min(pixels.n, sparkPos[i])) + sparkVel[i] += settings.gravity + sparkCol[i] += -0.8 + sparkCol[i] = max(0, min(255, sparkCol[i])) + if 0 <= int(sparkPos[i]) < pixels.n: + color = (int(sparkCol[i]), int(sparkCol[i] * 0.5), 0) # Using a warm color similar to HeatColor + pixels[int(sparkPos[i])] = (color[0] // 5, color[1] // 5, color[2] // 5) # Reduce brightness + + # Flare + if 0 <= int(flarePos) < pixels.n: + pixels[int(flarePos)] = (int(brightness * 255), int(brightness * 255), int(brightness * 255)) + pixels.show() + pixels.fill((0, 0, 0)) + flarePos += flareVel + flareVel += settings.gravity + brightness *= settings.brightness_decay + + # Explode! + nSparks = int(flarePos / 2) + for i in range(nSparks): + sparkPos[i] = flarePos + sparkVel[i] = (random.uniform(0, 2) - 1) + sparkCol[i] = abs(sparkVel[i]) * 500 + sparkCol[i] = max(0, min(255, sparkCol[i])) + sparkVel[i] *= flarePos / pixels.n + + sparkCol[0] = 255 # This will be our known spark + dying_gravity = settings.gravity + while sparkCol[0] > settings.c2 / 128: + pixels.fill((0, 0, 0)) + for i in range(nSparks): + sparkPos[i] += sparkVel[i] + sparkPos[i] = max(0, min(pixels.n, sparkPos[i])) + sparkVel[i] += dying_gravity + sparkCol[i] *= 0.99 + sparkCol[i] = max(0, min(255, sparkCol[i])) + + if 0 <= int(sparkPos[i]) < pixels.n: + if sparkCol[i] > settings.c1: + pixels[int(sparkPos[i])] = (255, 255, int(255 * (sparkCol[i] - settings.c1) / (255 - settings.c1))) + elif sparkCol[i] < settings.c2: + pixels[int(sparkPos[i])] = (int(255 * sparkCol[i] / settings.c2), 0, 0) + else: + pixels[int(sparkPos[i])] = (255, int(255 * (sparkCol[i] - settings.c2) / (settings.c1 - settings.c2)), 0) + + dying_gravity *= 0.995 + pixels.show() + pixels.fill((0, 0, 0)) + pixels.show() diff --git a/mqtt_animator.py b/mqtt_animator.py index 558634a..54daaae 100644 --- a/mqtt_animator.py +++ b/mqtt_animator.py @@ -189,7 +189,8 @@ def publish_state(cli): json.dumps({"state": animation_state.state, "brightness": animation_state.brightness, "animation": animation_state.effect, - "args": json.dumps(dataclasses.asdict(animation_args)) + "args": json.dumps(dataclasses.asdict(animation_args)), + "num_leds": pixels.n }) )