"""
Copyright 2023 Ethan Christensen
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Type
if TYPE_CHECKING:
from ..bruhutil.bruhffer import Buffer
from .base_effect import BaseEffect
[docs]
@dataclass
class EffectEntry:
"""Metadata for a single registered effect."""
name: str
effect_cls: Type
settings_cls: Type | None
description: str
presets: dict[str, Any] = field(default_factory=dict)
[docs]
class EffectRegistry:
"""
Central registry that maps effect names to their implementation class,
settings class, description, and named presets.
Usage::
from bruhanimate import effect_registry
# list all registered effects
for name, entry in effect_registry.entries().items():
print(name, "—", entry.description)
# create an effect by name (returns a BaseEffect instance)
effect = effect_registry.create("snow", buffer, " ")
# create with a named preset
effect = effect_registry.create("snow", buffer, " ", preset="blizzard")
# create with a custom settings object
from bruhanimate import SnowSettings
effect = effect_registry.create("snow", buffer, " ", settings=SnowSettings(wind=0.8))
# register your own effect
effect_registry.register(
"myeffect",
MyEffect,
settings_cls=MySettings,
description="Does something cool",
presets={"fast": MySettings(speed=10)},
)
"""
[docs]
def __init__(self):
self._registry: dict[str, EffectEntry] = {}
# ------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------
[docs]
def register(
self,
name: str,
effect_cls: Type,
settings_cls: Type | None = None,
description: str = "",
presets: dict[str, Any] | None = None,
) -> None:
"""Register an effect under *name*."""
self._registry[name] = EffectEntry(
name=name,
effect_cls=effect_cls,
settings_cls=settings_cls,
description=description,
presets=presets or {},
)
# ------------------------------------------------------------------
# Discovery
# ------------------------------------------------------------------
[docs]
def get(self, name: str) -> EffectEntry:
"""Return the :class:`EffectEntry` for *name*, or raise ``KeyError``."""
if name not in self._registry:
raise KeyError(
f"'{name}' is not registered. "
f"Available effects: {sorted(self._registry)}"
)
return self._registry[name]
[docs]
def names(self) -> list[str]:
"""Return a sorted list of all registered effect names."""
return sorted(self._registry)
[docs]
def entries(self) -> dict[str, EffectEntry]:
"""Return a copy of the full registry dict."""
return dict(self._registry)
[docs]
def presets(self, name: str) -> dict[str, Any]:
"""Return the preset dict for the named effect."""
return dict(self.get(name).presets)
def __contains__(self, name: str) -> bool:
return name in self._registry
def __repr__(self) -> str:
names = ", ".join(sorted(self._registry))
return f"EffectRegistry([{names}])"
# ------------------------------------------------------------------
# Factory
# ------------------------------------------------------------------
[docs]
def create(
self,
name: str,
buffer: "Buffer",
background: str,
*,
settings: Any = None,
preset: str | None = None,
) -> "BaseEffect":
"""
Instantiate the named effect.
Priority: *settings* > *preset* > effect defaults.
Args:
name: Registered effect name (e.g. ``"snow"``).
buffer: The :class:`Buffer` the effect will draw into.
background: Background fill character.
settings: A settings dataclass instance to pass directly.
preset: Name of a registered preset to use when *settings*
is not provided.
Returns:
A ready-to-use ``BaseEffect`` instance.
"""
entry = self.get(name)
if settings is None and preset is not None:
if preset not in entry.presets:
raise KeyError(
f"Preset '{preset}' not found for '{name}'. "
f"Available: {sorted(entry.presets)}"
)
settings = entry.presets[preset]
if settings is not None and entry.settings_cls is not None:
return entry.effect_cls(buffer, background, settings=settings)
return entry.effect_cls(buffer, background)
# ---------------------------------------------------------------------------
# Module-level singleton — populated below
# ---------------------------------------------------------------------------
effect_registry = EffectRegistry()
# Deferred imports to avoid circular deps (registry.py is imported by __init__)
def _populate():
from .audio_effect import AudioEffect
from .automaton_effect import AutomatonEffect
from .boids_effect import BoidsEffect
from .diffusion_effect import DiffusionEffect
from .draw_lines_effect import DrawLinesEffect
from .fire_effect import FireEffect
from .firework_effect import FireworkEffect
from .game_of_life_effect import GameOfLifeEffect
from .julia_effect import JuliaEffect
from .matrix_effect import MatrixEffect
from .noise_effect import NoiseEffect
from .offset_effect import OffsetEffect
from .perlin_effect import PerlinEffect
from .plasma_effect import PlasmaEffect
from .rain_effect import RainEffect
from .sand_effect import SandEffect
from .settings import (
AudioSettings,
AutomatonSettings,
BoidsSettings,
DiffusionSettings,
DrawLinesSettings,
FireSettings,
FireworkSettings,
GameOfLifeSettings,
MatrixSettings,
NoiseSettings,
OffsetSettings,
PerlinSettings,
PlasmaSettings,
RainSettings,
SandSettings,
SnowSettings,
StarSettings,
TwinkleSettings,
VoronoiSettings,
)
from .snow_effect import SnowEffect
from .star_effect import StarEffect
from .static_effect import StaticEffect
from .twinkle_effect import TwinkleEffect
from .voronoi_effect import VoronoiEffect
from .water_effect import WaterEffect
effect_registry.register(
"static",
StaticEffect,
settings_cls=None,
description="Fills the screen with a static background character.",
)
effect_registry.register(
"offset",
OffsetEffect,
settings_cls=OffsetSettings,
description="Scrolling offset background.",
presets={
"right": OffsetSettings(direction="right"),
"left": OffsetSettings(direction="left"),
"up": OffsetSettings(direction="up"),
"down": OffsetSettings(direction="down"),
},
)
effect_registry.register(
"noise",
NoiseEffect,
settings_cls=NoiseSettings,
description="Random noise pixels.",
presets={
"sparse": NoiseSettings(intensity=50, color=False),
"dense": NoiseSettings(intensity=200, color=False),
"color": NoiseSettings(intensity=150, color=True),
},
)
effect_registry.register(
"stars",
StarEffect,
settings_cls=StarSettings,
description="Blinking star field.",
presets={
"greyscale": StarSettings(color_type="GREYSCALE"),
"color": StarSettings(color_type="COLOR"),
},
)
effect_registry.register(
"plasma",
PlasmaEffect,
settings_cls=PlasmaSettings,
description="Animated sine-wave plasma.",
presets={
"greyscale": PlasmaSettings(color=False),
"color": PlasmaSettings(color=True, characters=True),
"blocks": PlasmaSettings(color=True, characters=False),
"random": PlasmaSettings(color=True, random_colors=True),
},
)
effect_registry.register(
"gol",
GameOfLifeEffect,
settings_cls=GameOfLifeSettings,
description="Conway's Game of Life with optional color decay.",
presets={
"plain": GameOfLifeSettings(decay=False, color=False),
"decay": GameOfLifeSettings(decay=True, color=False),
"color": GameOfLifeSettings(decay=True, color=True, color_type="COLOR"),
},
)
effect_registry.register(
"rain",
RainEffect,
settings_cls=RainSettings,
description="Falling rain with wind direction and collision.",
presets={
"drizzle": RainSettings(intensity=1, wind_direction="none"),
"storm": RainSettings(intensity=3, wind_direction="east", swells=True),
"thunderstorm": RainSettings(
intensity=3,
wind_direction="east",
swells=True,
lightning=True,
lightning_chance=0.03,
),
"monsoon": RainSettings(
intensity=5,
wind_direction="east",
swells=True,
collision=True,
lightning=True,
lightning_chance=0.05,
),
},
)
effect_registry.register(
"matrix",
MatrixEffect,
settings_cls=MatrixSettings,
description="Cascading random-character digital rain.",
presets={
"default": MatrixSettings(),
"fast": MatrixSettings(
character_halt_range=(1, 1), color_halt_range=(1, 1), gradient_length=3
),
},
)
effect_registry.register(
"drawlines",
DrawLinesEffect,
settings_cls=DrawLinesSettings,
description="Bresenham line drawing onto the buffer.",
presets={
"thin": DrawLinesSettings(thin=True),
"thick": DrawLinesSettings(thin=False),
},
)
effect_registry.register(
"snow",
SnowEffect,
settings_cls=SnowSettings,
description="Falling snow with wind and ground accumulation.",
presets={
"light": SnowSettings(intensity=0.005, wind=0.0),
"moderate": SnowSettings(intensity=0.015, wind=0.2),
"blizzard": SnowSettings(intensity=0.04, wind=0.7),
"windy": SnowSettings(intensity=0.01, wind=0.9),
},
)
effect_registry.register(
"twinkle",
TwinkleEffect,
settings_cls=TwinkleSettings,
description="Characters that pulse in brightness.",
presets={
"sparse": TwinkleSettings(density=0.02),
"dense": TwinkleSettings(density=0.15),
},
)
effect_registry.register(
"firework",
FireworkEffect,
settings_cls=FireworkSettings,
description="Firework explosions with multiple burst patterns.",
presets={
"plain": FireworkSettings(color_enabled=False),
"color": FireworkSettings(color_enabled=True, color_type="rainbow"),
"random": FireworkSettings(
firework_type="random", color_enabled=True, color_type="random"
),
},
)
effect_registry.register(
"fire",
FireEffect,
settings_cls=FireSettings,
description="Particle-based fire simulation.",
presets={
"campfire": FireSettings(intensity=0.15, turbulence=0.05),
"inferno": FireSettings(intensity=0.5, turbulence=0.2, swell=True),
"windy": FireSettings(
intensity=0.3, wind_direction=90.0, wind_strength=0.6
),
},
)
effect_registry.register(
"julia",
JuliaEffect,
settings_cls=None,
description="Animated Julia-set fractal.",
)
effect_registry.register(
"water",
WaterEffect,
settings_cls=None,
description="Rippling water surface simulation.",
)
effect_registry.register(
"boids",
BoidsEffect,
settings_cls=BoidsSettings,
description="Reynolds flocking simulation: separation, alignment, and cohesion rules.",
presets={
"default": BoidsSettings(),
"dense": BoidsSettings(num_boids=120),
"fast": BoidsSettings(num_boids=60, max_speed=3.0),
},
)
effect_registry.register(
"sand",
SandEffect,
settings_cls=SandSettings,
description="Falling-sand cellular automaton; particles spawn at the top and pile up.",
presets={
"light": SandSettings(spawn_rate=0.05),
"normal": SandSettings(spawn_rate=0.20),
"heavy": SandSettings(spawn_rate=0.50),
},
)
effect_registry.register(
"diffusion",
DiffusionEffect,
settings_cls=DiffusionSettings,
description="Gray-Scott reaction-diffusion; produces evolving organic spots and stripes.",
presets={
"coral": DiffusionSettings(f=0.0545, k=0.062),
"spots": DiffusionSettings(f=0.035, k=0.065),
"stripes": DiffusionSettings(f=0.026, k=0.051),
},
)
effect_registry.register(
"automaton",
AutomatonEffect,
settings_cls=AutomatonSettings,
description="Wolfram 1-D elementary cellular automaton scrolling downward.",
presets={
"rule30": AutomatonSettings(rule=30),
"rule90": AutomatonSettings(rule=90),
"rule110": AutomatonSettings(rule=110),
"rule184": AutomatonSettings(rule=184),
},
)
effect_registry.register(
"voronoi",
VoronoiEffect,
settings_cls=VoronoiSettings,
description="Animated Voronoi diagram with drifting seed points.",
presets={
"slow": VoronoiSettings(seed_speed=0.1),
"fast": VoronoiSettings(seed_speed=0.8),
"dense": VoronoiSettings(num_seeds=24, seed_speed=0.3),
},
)
effect_registry.register(
"perlin",
PerlinEffect,
settings_cls=PerlinSettings,
description="Smooth animated noise field built from multiple sine-wave octaves.",
presets={
"sparse": PerlinSettings(threshold=0.55),
"dense": PerlinSettings(threshold=0.20),
"fast": PerlinSettings(speed=0.05),
},
)
effect_registry.register(
"audio",
AudioEffect,
settings_cls=AudioSettings,
description="System audio visualizer (bars, mirror, waveform, spectrum, radial, rain, tunnel, ripple, vortex, wave, starfield, fireworks, interference, bounce, lightning, aurora, orbit, scope, grid, helix, lissajous, sunburst, mandala, comet, weave).",
presets={
"bars": AudioSettings(mode="bars", color=True, smoothing=0.25),
"mirror": AudioSettings(mode="mirror", color=True, smoothing=0.25),
"waveform": AudioSettings(mode="waveform", color=True, smoothing=0.1),
"spectrum": AudioSettings(mode="spectrum", color=True, smoothing=0.2),
"radial": AudioSettings(mode="radial", color=True, smoothing=0.3),
"rain": AudioSettings(mode="rain", color=True, smoothing=0.2),
"minimal": AudioSettings(mode="bars", color=False, smoothing=0.6),
"tunnel": AudioSettings(mode="tunnel", color=True, smoothing=0.2),
"ripple": AudioSettings(
mode="ripple", color=True, smoothing=0.15, sensitivity=1.5
),
"vortex": AudioSettings(mode="vortex", color=True, smoothing=0.2),
"wave": AudioSettings(mode="wave", color=True, smoothing=0.25),
"starfield": AudioSettings(mode="starfield", color=True, smoothing=0.2),
"fireworks": AudioSettings(
mode="fireworks", color=True, smoothing=0.15, sensitivity=2.0
),
"interference": AudioSettings(
mode="interference", color=True, smoothing=0.2
),
"bounce": AudioSettings(mode="bounce", color=True, smoothing=0.2),
"lightning": AudioSettings(
mode="lightning", color=True, smoothing=0.1, sensitivity=1.5
),
"aurora": AudioSettings(mode="aurora", color=True, smoothing=0.3),
"orbit": AudioSettings(mode="orbit", color=True, smoothing=0.2),
"orbit_gradient": AudioSettings(
mode="orbit_gradient", color=True, smoothing=0.2
),
"scope": AudioSettings(
mode="scope", color=True, smoothing=0.0, sensitivity=3.0
),
"grid": AudioSettings(mode="grid", color=True, smoothing=0.2),
"helix": AudioSettings(mode="helix", color=True, smoothing=0.25),
"lissajous": AudioSettings(mode="lissajous", color=True, smoothing=0.2),
"sunburst": AudioSettings(mode="sunburst", color=True, smoothing=0.2),
"mandala": AudioSettings(mode="mandala", color=True, smoothing=0.25),
"comet": AudioSettings(
mode="comet", color=True, smoothing=0.15, sensitivity=1.5
),
"weave": AudioSettings(mode="weave", color=True, smoothing=0.2),
},
)
_populate()