Skip to content
led-ticker

Animations

led-ticker has one animation right now: typewriter — a character-by-character reveal. Set animation = "typewriter" on a supported widget and the text types out one character at a time the moment the widget appears. The characters land in their final positions (no shifting), so the message assembles itself in place rather than scrolling in.

Animations are a separate axis from color providers. A widget can carry both simultaneously — for example animation = "typewriter" and font_color = "rainbow" — and each effect tracks its own frame counter. The characters type out in rainbow with no interference between the two systems.

Animations may also emit a per-frame rotation (degrees, clockwise) that the message widget renders through an offscreen rotate around the text block’s center — the flair plugin’s propeller effect uses this seam. On scaled displays (bigsign), the spin runs at half physical resolution from a pre-rendered snapshot that captures hires fonts and emoji at their correct size; the resting frame always renders at full detail. Animated font_color providers (rainbow, color_cycle, shimmer) pause for the duration of the spin and resume at settle — the color phase keeps advancing in the background, so on settle the sweep continues exactly where it left off.

`message` with animation = typewriter and font_color = rainbow
`message` with animation = typewriter and font_color = rainbow

Minimal

[[playlist.section.widget]]
type = "message"
text = "Hello, world!"
animation = "typewriter"

The cadence is frames_per_char × 50 ms per character — the engine ticks at 50 ms and advances the typewriter counter once per tick. The default of 3 gives about 150 ms per character, which feels snappy but still readable at roughly 7 characters per second. A value of 5 or 6 reads as deliberate and is a good choice when you want the audience to register each character dropping in. Values above 10 read as comically slow. There is no separate animation clock — everything runs on the same 50 ms engine tick.

To override frames_per_char, switch from the string shorthand to an inline TOML table with a style key:

[[playlist.section.widget]]
type = "message"
text = "Slow type"
animation = {style = "typewriter", frames_per_char = 6} # ~300ms per char

Typing takes frames_per_char × 50 ms per character — so a 20-character message at the default frames_per_char = 3 takes roughly 3 seconds to finish. If hold_time is shorter than the typing duration, the transition fires mid-reveal and the message never fully appears. led-ticker validate catches this (rule 61) and prints the computed typing duration alongside the effective hold time, so you can raise hold_time to cover the full reveal — or shorten the text.

When a reveal is nearly done at transition time, the engine waits up to roughly one second for the final characters to land before switching scenes, so a message that’s one or two characters short doesn’t cut away abruptly.

animation and font_color are independent axes on the same widget. Both can be set at the same time, and each effect tracks its own counter. The result is that characters type out AND each character’s color comes from the provider — a rainbow sweeps across the text as it appears, rather than the whole message flashing in at once.

[[playlist.section.widget]]
type = "message"
text = "Now Enrolling"
animation = "typewriter"
font_color = "rainbow"

Because rainbow is a continuous-phase provider (its counter does not reset between loop_count repetitions), the color sweep keeps flowing even as the typewriter retypes from scratch on each loop.

Typewriter has restart_on_visit = true — the reveal counter resets every time the section shows the widget. With loop_count = 2, the widget retypes from scratch on each loop rather than resuming mid-word. With loop_count = 1 and a multi-widget section in slideshow mode, the widget retypes fresh each time it appears in the rotation. This is the intentional, expected behavior — typewriter is designed to be a reveal, not a persistent animation state. See frame counters for the full model of restart-on-visit versus continuous-phase behavior.

Widget typeanimation = "typewriter"?animation = "flair.propeller"?animation = "flair.fisheye"?
messageyesyesyes
countdownnonono
gifyes — single-row only (see note below)accepted but rotation is ignored 1accepted but the lens is ignored 2
imageyes — single-row only (see note below)accepted but rotation is ignored 1accepted but the lens is ignored 2
two_rownonono
weather.current, rss.feednonono

On gif and image widgets, typewriter is restricted to single-row text overlays. It will raise a configuration error if bottom_text is also set (which activates two-row mode), if text_align is "scroll" or "scroll_over" (incompatible with a fixed-position reveal), or if text is empty.

animation is only valid on message, gif, and image

Section titled “animation is only valid on message, gif, and image”

Other widget types (two_row, the crypto and feeds widgets) have their own draw paths and ignore animation silently — the validator rejects it explicitly to make the no-op visible. For dynamic color on those widgets, use font_color = "rainbow" or a gradient.

Typewriter is single-row only on gif and image

Section titled “Typewriter is single-row only on gif and image”

On top of the widget-type check above, typewriter on gif / image rejects three combinations: bottom_text set, text_align = "scroll" / "scroll_over", or empty text. Typewriter still composes with font_color = "rainbow" and border = "rainbow".


flair.propeller is a plugin animation from the flair pack (led-ticker-flair >= 0.2.0, core >= 4.3). The message widget’s text spins in-plane like a propeller the moment the widget appears — easing out over the configured number of revolutions and settling exactly flat — then holds readable for the rest of hold_time. Transitions wait for the spin to finish before firing.

`message` with `animation = flair.propeller` — text spins in and settles flat
`message` with `animation = flair.propeller` — text spins in and settles flat

Add one line to config/requirements-plugins.txt:

led-ticker-flair

See Plugins for the install flow.

Minimal

[[playlist.section.widget]]
type = "message"
text = "PROPELLER"
center = true
font_color = [255, 200, 0]
animation = "flair.propeller"

Switch to an inline table to control speed and direction:

[[playlist.section.widget]]
type = "message"
text = "PROPELLER"
center = true
font_color = [255, 200, 0]
animation = {style = "flair.propeller", revolutions = 3, spin_seconds = 1.5, direction = "ccw"}
KnobDefaultNotes
revolutions2Integer >= 1. Number of full 360° turns before settling.
spin_seconds1.0Seconds for the full spin (ease-out). Shorter feels snappy; 2–3 s reads as deliberate.
direction"cw""cw" (clockwise) or "ccw" (counter-clockwise) — viewed from the front of the panel.

led-ticker validate rule 62 warns when spin_seconds is longer than the section’s hold_time — a spin that outlasts the hold cuts away before the text settles, so the message never reads cleanly. Raise hold_time to cover spin_seconds plus at least a moment of readable hold. The engine waits up to about a second for the spin to settle before firing a transition; a spin that outlasts the hold by more than that cuts away mid-spin (the rule 62 warning covers this).

flair.propeller composes with font_color providers and border effects using the same independent-axes model as typewriter. Animated color providers (rainbow, color_cycle, shimmer) freeze for the duration of the spin and resume at settle — the color phase keeps advancing in the background, so the sweep continues exactly where it left off the moment the text lands flat. This means a propeller + rainbow combo reads as: spin in (colors frozen), land flat, rainbow resumes sweeping.

On signs with default_scale > 1, the mid-spin frames render from a pre-rendered snapshot at half physical resolution — this captures hires fonts and emoji at their correct size but at reduced detail. The resting (settled) frame always renders at full detail. led-ticker-flair >= 0.2.0 and core >= 4.5 are required for physical-resolution hires spin frames; on core 4.3–4.4 the spin runs but hires fonts draw unrotated (validate rule 63 warns when this condition is detected).

flair.propeller and flair.spinout are designed to pair: the widget spins in at entry and spins out at exit. See the full-cycle demo below for the combined config.

Full cycle — `flair.propeller` entry spin followed by `flair.spinout` transition
Full cycle — `flair.propeller` entry spin followed by `flair.spinout` transition

Full cycle (propeller in + spinout)

[transitions]
default = "flair.spinout"
duration = 1.0
[[playlist.section]]
mode = "slideshow"
hold_time = 3.5
[[playlist.section.widget]]
type = "message"
text = "FULL CYCLE"
center = true
font_color = [255, 200, 0]
animation = "flair.propeller"
[[playlist.section]]
mode = "slideshow"
hold_time = 2.5
[[playlist.section.widget]]
type = "message"
text = "REPEAT"
center = true
font_color = [255, 100, 255]

flair.fisheye is a plugin animation from the flair pack (led-ticker-flair >= 0.4.0, core >= 4.7). A message widget’s scrolling text passes through a stationary “fisheye lens” centered on the panel: letters enter compressed at the edges, swell as they cross the middle, and compress again on exit. The lens is fixed — the scroll provides all the motion, so the effect runs for the whole scroll and a section can cut away at any moment.

`message` with `animation = flair.fisheye` — text scrolls through a stationary squeeze-and-bulge lens
`message` with `animation = flair.fisheye` — text scrolls through a stationary squeeze-and-bulge lens

Add one line to config/requirements-plugins.txt:

led-ticker-flair

See Plugins for the install flow.

Minimal

[[playlist.section.widget]]
type = "message"
text = "OPEN LATE TONIGHT"
animation = "flair.fisheye"

Stronger lens

[[playlist.section.widget]]
type = "message"
text = "FLASH SALE TODAY"
animation = {style = "flair.fisheye", magnify = 1.33, edge_squeeze = 0.45}
`magnify = 1.33, edge_squeeze = 0.45` — a stronger lens: the center swells more and the edges compress harder than the default
`magnify = 1.33, edge_squeeze = 0.45` — a stronger lens: the center swells more and the edges compress harder than the default
KnobDefaultMeaning
magnify1.3Center scale (both axes). There is a ceiling — magnify × font line-height ≤ content_height — above which the bulged text would run off the panel; a widget over the ceiling is skipped and the rest of the sign keeps running. For the default 6×12 font in a 16-row band the ceiling is about 1.33.
edge_squeeze0.6Edge scale (0 < edge_squeeze ≤ magnify). Lower means more edge compression — a more dramatic lens.
profile"cosine"The edge-to-center falloff curve. "cosine" is the only value today.

The lens is width-preserving: a message takes exactly as long to scroll across as it would without the lens. But because the edges are compressed, they show more text than a plain scroll would — roughly two extra characters per side at the default squeeze, more with a stronger one. That is the lens working as intended, not a glitch.

Unlike flair.propeller (whose colors freeze during the spin), flair.fisheye keeps font_color live: a rainbow or color_cycle provider keeps sweeping as the text scrolls through the lens.

`font_color = rainbow` with `flair.fisheye` — colors keep sweeping through the lens
`font_color = rainbow` with `flair.fisheye` — colors keep sweeping through the lens

Held (non-scrolling) text gets a fixed emphasis: the center of the message swells while the ends compress. Center a short message with center = true and skip the scroll — good for a shop sign or a headline.

`center = true`, no scroll — `flair.fisheye` bulges the middle of a held message
`center = true`, no scroll — `flair.fisheye` bulges the middle of a held message

Static bulge

[[playlist.section.widget]]
type = "message"
text = "GRAND OPENING TODAY"
center = true
animation = "flair.fisheye"

:slug: emoji ride the lens with the text — an emoji swells as it crosses the center and compresses at the edges, just like the letters around it.

`NOW OPEN :star: WELCOME IN :star:` — the star bulges through the lens along with the words
`NOW OPEN :star: WELCOME IN :star:` — the star bulges through the lens along with the words

With inline emoji

[[playlist.section.widget]]
type = "message"
text = "NOW OPEN :star: WELCOME IN :star:"
animation = "flair.fisheye"
  • Borders: the border frames the panel un-warped while the text warps inside it.
  • Hi-res fonts: on a scaled sign (bigsign) a hi-res font warps too, subject to the same magnify × font line-height ≤ content_height ceiling — a too-tall hi-res font is skipped (and the rest of the sign keeps running). On a smallsign (scale 1) a hi-res font renders normally (unwarped) and led-ticker validate emits a warning: the pixel-precise warp needs a scaled display.

The lens re-renders the scrolling text every frame at reduced resolution, so the whole scroll shows at half detail on the bigsign — the distortion masks it, and it matches the reduced detail you already see mid-spin with flair.propeller.

A fisheye message scrolls in through the lens; flair.spinout — the flair pack’s text-spin transition, documented on the flair transitions page — can spin that message out as the section changes. Set it as the transition (here as the global default) and the outgoing fisheye text spins away before the next section appears:

Fisheye scroll with a spinout exit

[transitions]
default = "flair.spinout"
duration = 1.0
[[playlist.section]]
mode = "slideshow"
hold_time = 3.0
[[playlist.section.widget]]
type = "message"
text = "GRAND OPENING TODAY"
animation = "flair.fisheye"
[[playlist.section]]
mode = "slideshow"
hold_time = 2.5
[[playlist.section.widget]]
type = "message"
text = "SEE YOU SOON"
center = true
  1. flair.propeller is accepted on gif and image at config-load so a section can carry both a spinning message and an image widget without a validation error, but the rotation has no visible effect — only message widgets render the spin. 2

  2. flair.fisheye is accepted on gif and image at config-load so a section can carry both a lens-scrolling message and an image widget without a validation error, but the lens has no visible effect — only message widgets warp. 2