Skip to content

feat: ambient (never-completing) effects and a noise module#97

Open
s-celles wants to merge 3 commits into
ratatui:developmentfrom
s-celles:feat/ambient-noise-effects
Open

feat: ambient (never-completing) effects and a noise module#97
s-celles wants to merge 3 commits into
ratatui:developmentfrom
s-celles:feat/ambient-noise-effects

Conversation

@s-celles

@s-celles s-celles commented Jul 18, 2026

Copy link
Copy Markdown

Implements what I described in #96. Opening it as a concrete proposal rather
than a finished thing.

What's here

pub mod noise — value noise, fbm, and two shaping curves. Deterministic
pseudo-noise seeded by position, sitting next to wave as its aperiodic
counterpart. Independent of everything below; useful on its own.

Seven effects. Three that paint cell backgrounds (noise_field,
color_wave, glow) and four that modulate existing colours (breathe,
flicker, shimmer, drift). All registered with the default DSL.

An example (cargo run -p organic-effects) cycling through them on generic
content, each compiled from its DSL expression rather than constructed in Rust.

The three commits are independent in that order — noise can be taken alone if
the ambient question needs more discussion.

The design decisions worth arguing about

The code takes a position but I'm not attached to any of them.

Never-completing effects. done() is always false, timer() returns
None, and execute accumulates its own elapsed time from the tick duration.
EffectTimer simply doesn't apply. Wrapping a transition in fx::repeating
isn't an alternative: looping restarts the field and leaves a visible seam once
a cycle.

Constructors set a default cell filter. A departure from the rest of fx::,
which leaves the filter unset. These can't: a painting effect restricted to
glyphs, or a modulating one restricted to blanks, runs, reports itself as
running, and does nothing whatsoever. The failure mode is silence, so the
constructor chooses. with_filter still overrides.

One Ambient shader with a Kind enum rather than seven structs — keeps
the elapsed-time bookkeeping in one place and stays Clone without boxing
closures. Seven files would match the existing layout better; happy to switch.

Notes for reviewers

Not(NonEmpty) is the filter for blank cells, not Not(Text)Text
matches the space character, so Not(Text) selects neither blanks nor glyphs.
This is the first of the two gotchas I mentioned in #96 and it cost me a while
to find.

math::rem_euclid is added because f32::rem_euclid is std-only and this has
to build under no_std.

Testing

Full CI matrix locally, including --no-default-features. Clippy adds no new
warnings over the 3 already on development.

Beyond the usual, the tests pin two things specific to ambient effects:

  • that each effect is actually visible — measured as mean RGB change per
    channel per second, at the parameters used in each doc example. This caught a
    real bug: noise_field originally shipped with a documented speed so low it
    drifted about one RGB unit per second, which compiles, paints, reports itself
    as running, and looks like a still image.
  • that each targets cells it can act on, for the reason above.

Written with AI assistance.

s-celles added 3 commits July 19, 2026 00:40
`wave` provides periodic waveforms; this adds aperiodic ones. The distinction
matters for effects meant to run continuously: a field driven by a sine visibly
repeats and the eye picks the period out quickly, where noise does not.

Deterministic pseudo-noise seeded by position rather than randomness, so a field
is reproducible, testable, and stable across a resize.

Also adds `math::rem_euclid`, since `f32::rem_euclid` is std-only and this has
to build under no_std.

Refs ratatui#96

Assisted-by: AI
Every existing effect is a transition: it runs once against a timer and
finishes. These are meant to sit under a UI for as long as it is open, which
changes two things.

They never complete. `done()` is always false and there is no timer, so
`execute` accumulates its own elapsed time from the tick duration. Driving them
from a timer's alpha would mean looping it, which restarts the field and leaves
a visible seam once a cycle.

They each set the cell filter they can act on. An effect that paints an absolute
colour needs blank cells; one that modulates an existing colour needs cells that
carry a glyph. Applied to the wrong ones an effect runs, reports itself as
running, and does nothing at all -- the failure mode is silence, so the
constructors choose rather than leaving it to the caller. `with_filter` still
overrides.

Note for reviewers: the correct filter for blank cells is `Not(NonEmpty)`, not
the more obvious `Not(Text)` -- `Text` matches the space character, so
`Not(Text)` selects neither blanks nor glyphs.

One `Ambient` shader with a `Kind` enum rather than seven structs: it keeps the
elapsed-time bookkeeping in one place and stays `Clone` without boxing closures.

All seven are registered with the default DSL, so applications can expose them
as configuration rather than code.

Refs ratatui#96

Assisted-by: AI
Cycles through the seven ambient effects on generic content that mixes text
with blank space, so painting and modulating effects are both visible at once.

Each is compiled from its DSL expression, shown on screen beneath the
animation, rather than constructed in Rust -- what is running is exactly what a
user could write in a config file.

Reports frame rate alongside draw time, mean and peak. Frame rate alone would be
uninformative: the loop blocks on an input poll, so it reads the target rate
whatever the effect costs. Draw time is what reflects the effect's expense, and
the peak is what is felt as stutter where the mean hides it. The target rate is
adjustable with +/- so the difference between effects can be seen.

Refs ratatui#96

Assisted-by: AI
@s-celles

Copy link
Copy Markdown
Author

This is my first PR here.

It have been done with AI assistance and as a reimplementation of some effects found in Tachikoma.jl from @kahliburke) so it may be imperfect.

Here a some screencaptures
image

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant