Effects¶
scrollkit.effects adds visual polish: particle systems and colour animations —
all built to respect the device's memory and frame budget.
The old transition effects were removed and replaced
The earlier transitions, basic_transitions, and reveal modules
(fades/slides/wipes/reveal) were broken or fake — they looped over all 2048
pixels in Python and only rendered placeholder content — and have been
removed. Their replacements are the ScrollKit Showcase effect classes,
built on the zero-allocation overlay-mask/painter/easing primitives:
- Characterful scrolling — kinetic marquee, wave-rider, split-flap
- Theatrical transitions — 13 transitions: iris, venetian, mosaic, CRT, light-slit, pixel-dissolve, column-rain, gradual-reveal, scan-fold, horizontal/diagonal wipes, glitch-bars, drop-from-sky
- Palette-animated bitmap text — rainbow, mono, neon, chrome, hazard
See demos/hard/showcase.py for a reel that announces and demonstrates every
one of them.
The showcase demo runs the whole reel — characterful scrolls, theatrical
transitions, and palette-animated bitmap text — back to back:

One effect contract, plus standalone helpers¶
After the effects consolidation there is one content-swap contract —
scrollkit.effects.transitions.Transition (see
Theatrical transitions) — plus standalone splash and particle
helpers. The old Effect / EffectRegistry, SimpleEffect / EffectsEngine, and
EnhancedDisplayContent systems were removed: they overlapped, were unused, and
the enhanced-content family even looped over all 2048 pixels in Python — the exact
anti-pattern the feasibility gate forbids. There is intentionally no shared
per-frame "effect" base class to inherit from.
To add your own effect, write a Transition: see
Adding your own transition and the
heavily-annotated reference in demos/medium/golden_transition.py.
What's available¶
| Module | What it gives you |
|---|---|
scrollkit.effects.transitions |
content-swap transitions on the Transition base (guide) |
scrollkit.effects.scrolling |
characterful scrolling text (guide) |
scrollkit.display.bitmap_text |
palette-animated bitmap text (guide) |
scrollkit.effects.particles |
standalone particle systems (sparkles, rain, embers, snow) |
scrollkit.effects splash helpers |
show_reveal_splash, show_drip_splash, show_swarm_splash |
Effects run with functionally equivalent behaviour on hardware and in the simulator — same effect types and sequencing, though exact pixel timing differs.
Pairing effects to content¶
Some effects read best on static (held) text, some on scrolling text, and
the transitions are full-screen swaps between content. Each effect class carries
a PAIRS_WITH tag ("static" / "scrolling" / "fullscreen") that
scrollkit.dev.capabilities() surfaces as pairs_with (and as_text() prints as
[best on: …]), so app authors and AI agents can pick the right effect for the
content.
| Best on | Effects |
|---|---|
| Scrolling text | KineticMarquee, WaveRider — Class 1, they are the scroll |
| Static / held text | SplitFlap (flips characters in place) |
| Either static or scrolling | the BitmapText palette effects: RainbowChase, MonoChase, NeonTubeCrawl, ChromeSheen, HazardStripes |
| Full screen (swap between content) | every Transition — they all work over any content: IrisSnap, VenetianShutters, MosaicResolve, CRTCollapse, LightSlitRewrite, PixelDissolve, ColumnRain, GradualReveal, ScanFold, HorizontalWipe, GlitchBars, DiagonalWipe, DropFromSky |
The tag is guidance, not a constraint — nothing stops you using an effect elsewhere; it just records what looks good.
One call per category¶
You rarely need the tags by hand. Each category exposes one selector function that
reads the live PAIRS_WITH tags and returns what's available for a given presentation,
so a newly-added effect shows up automatically — no list to keep in sync:
from scrollkit.effects.transitions import transitions_for
from scrollkit.effects.scrolling import scrollers_for
from scrollkit.display.bitmap_text import palette_effects_for, BitmapText
import random
transitions_for() # transition NAMES (all full-screen; for transition_style)
scrollers_for("scrolling") # scroller CLASSES suited to scrolling text
palette_effects_for("scrolling") # palette-effect CLASSES for BitmapText
Pass "scrolling" or "static" to scrollers_for / palette_effects_for to pick by
how the content is presented. transitions_for takes presentation="fullscreen"
(its default) — transitions are all full-screen swaps, so it returns every transition.
Apply each by its category — they are not interchangeable:
# a transition fires BETWEEN screens — it's a setting:
app.settings.set("transition_style", random.choice(transitions_for()))
# a scroller IS the content — add the class to the queue:
cls = random.choice(scrollers_for("scrolling"))
app.content_queue.add(cls("Space Mountain 45 min", y=12))
# a palette effect goes ON bitmap text:
pe = random.choice(palette_effects_for("scrolling"))
app.content_queue.add(BitmapText("OPEN", palette_effect=pe()))
Memory ladder
Effects are the first thing to disable on a memory-starved device. Prefer the lighter transitions and keep splash/particle counts low when targeting the MatrixPortal S3.
Showcase foundation primitives¶
The ScrollKit Showcase Effects work adds a small set of shared, zero-allocation
primitives that the new effects build on. Each runs unchanged on the device
and the simulator, and is budgeted against the calibrated MatrixPortal S3 model
(~20 fps / 50 ms per frame at bit_depth=4).
| Primitive | What it gives you | Hardware budget framing |
|---|---|---|
scrollkit.effects.easing |
Integer easing/tween lookup tables (ease, interp) for 6 curves |
Pure bytes lookups — no floats, no per-call allocation |
display.fill_rect / fill_span / clear_rect |
Bounded span/rect painters | C bulk ops (bitmaptools.fill_region) — never a full 2048-pixel Python loop |
display.measure_text(text) |
Real rendered text width (summed glyph advances) | Replaces the old len(text) * 6 estimate; measured once, off the hot path |
display.gfx + add_layer / remove_layer |
Platform-resolved Bitmap/Palette/TileGrid/bitmaptools, plus a content/layer group split |
Cached once per display; persistent effect layers keep a stable z-order across the per-frame clear |
scrollkit.effects.overlay.OverlayMask |
One preallocated indexed mask (transparent index 0) composited above content | Allocate once; transitions write only dirty spans |
scrollkit.effects.scrolling |
Characterful scrolling: KineticMarquee, WaveRider, SplitFlap |
One / a few small Labels repositioned per frame; bounded rebuilds |
scrollkit.effects.transitions |
Theatrical transitions: 13 in all — IrisSnap, VenetianShutters, MosaicResolve, CRTCollapse, LightSlitRewrite, PixelDissolve, ColumnRain, GradualReveal, ScanFold, HorizontalWipe, GlitchBars, DiagonalWipe, DropFromSky |
Bounded mask spans per frame; swaps content while fully covered |
scrollkit.display.bitmap_text |
Palette-animated bitmap text: BitmapText + RainbowChase/MonoChase/NeonTubeCrawl/ChromeSheen/HazardStripes |
Animation is a few palette writes per frame — zero per-frame pixel work, no glyph rebuild |
Every showcase effect carries an advertised FEASIBILITY dict (hardware_safe,
allocates_per_frame, max_pixel_writes_per_frame, modeled_frame_ms); see the
per-class pages above.
Strict feasibility gate¶
Hardware simulation can run in strict mode: an effect whose modeled per-frame
cost busts the device budget raises scrollkit.exceptions.FeasibilityError instead
of only warning. The gate is steady-state (a rolling median holds ~20 fps) plus a
single-frame transient ceiling, so a legitimate one-off glyph rebuild is tolerated
while a sustained over-budget effect fails.
from scrollkit.dev import run_headless
# strict=True implies hardware modeling; a busting effect -> result.ok is False
result = run_headless(MyApp(), frames=120, hardware=True, strict=True)
assert result.ok and result.advanced # rendered, animated, within budget
print(result.hardware_text) # per-frame budget breakdown
Strict mode is opt-in (strict=True, or SCROLLKIT_HW_STRICT=1); it is a
desktop-simulator concept and a no-op on CircuitPython, where the device runs at
real speed.
For the measured costs behind the budget (C bulk calls vs interpreted Python, the refresh floor, and the swarm rewrite that motivated all of this), see the Performance guide.