Skip to content
led-ticker

Tutorial 5: Polish & deploy

What you'll have by the end of this chapter — wipe transitions between widgets, a dissolve between sections, and an animated rainbow border on the headline widget.
What you'll have by the end of this chapter — wipe transitions between widgets, a dissolve between sections, and an animated rainbow border on the headline widget.

Chapter 4 left you with a multi-section config: a scrolling welcome strip, a swap section with per-day hours, a two-row hi-res brand card, and an image widget with a text overlay. This chapter adds two polish layers — transitions and border effects — then covers getting the config onto a Pi.

~10 min · no hardware needed

Picks up from Chapter 4 — continue with the config you built there (or use the reset block below). No new prerequisites; the deploy section is read-only and needs no Pi.

Reset block — start of Chapter 5

If you followed chapters 2–4 step by step, your config will have MORE sections than this (the ticker from chapter 2, the standalone two_row, and so on) — that’s expected. This reset block is a deliberately consolidated starting point for the polish work, not a literal copy of your chapter-4 end state. Either continue with your own config or start from this cleaner baseline:

[display]
rows = 64
cols = 256
chain_length = 1
default_scale = 4
brightness = 60
[transitions]
default = "push_left"
duration = 0.5
[[playlist.section]]
mode = "slideshow"
loop_count = 1
hold_time = 2.5
[[playlist.section.widget]]
type = "message"
text = "Welcome to Firebird Yoga"
font_color = [225, 48, 108]
[[playlist.section.widget]]
type = "image"
path = "assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "center"
hold_time = 8.0
scroll_speed_ms = 35
top_text = "Firebird"
top_font = "5x8"
top_color = [225, 48, 108]
bottom_text = "Vinyasa · yin · hot power · restorative · slow flow · all levels"
bottom_font = "AtkinsonHyperlegible-Bold"
bottom_font_size = 22
bottom_color = [255, 240, 200]
[[playlist.section.widget]]
type = "message"
text = "Open daily 10am – 8pm"
font_color = [120, 220, 255]
[[playlist.section]]
mode = "slideshow"
loop_count = 1
hold_time = 2.5
[[playlist.section.widget]]
type = "message"
text = "Summer Camps start June 15"
font_color = [180, 220, 180]
[[playlist.section.widget]]
type = "message"
text = "All levels · All ages"
font_color = [255, 220, 0]

So far the config has used push_left for everything. The [transitions] block has two separate knobs — default controls transitions between widgets inside a section, and between_sections controls the transition when the playlist moves from one section to the next. You can set them independently.

Replace the [transitions] block:

[transitions]
default = "wipe_left"
duration = 0.5
between_sections = "dissolve"
  • default = "wipe_left" — a vertical sweep line erases the outgoing widget, then the incoming widget appears behind it. More visual punch than push_left; the sweeping line makes each widget boundary feel intentional.
  • between_sections = "dissolve" — when the playlist moves from Section 1 to Section 2 (or back), pixels scatter randomly rather than sliding. The contrast with the within-section wipes gives viewers a clear signal that the topic has changed.
  • duration applies to both, unless overridden per-section with transition_duration.
wipe_left between widgets, dissolve between sections. The wipe sweep line is visible mid-transition; the dissolve scatter marks each section boundary.
wipe_left between widgets, dissolve between sections. The wipe sweep line is visible mid-transition; the dissolve scatter marks each section boundary.

The full transition catalogue covers 30+ options organised into families:

  • Push (push_left, push_right, push_up, push_down, push_alternating, push_random) — outgoing and incoming slide together as one unit.
  • Wipe (wipe_left, wipe_right, wipe_up, wipe_down, wipe_alternating, wipe_random) — outgoing stays fixed while a coloured sweep line erases it.
  • Sprite trailsnyancat.forward, pokeball.forward, pacman.forward, sailor_moon.forward (each has .reverse and .alternating variants; the first two have bigsign hi-res variants). These ship as four packages in the led-ticker-plugins monorepo. A rolling-baseball.roll family ships in the baseball package.
  • Specialcut (instant), color_flash, dissolve, split, scroll.

See the Transitions catalogue for per-family knobs and examples of each.

The border field paints an animated ring around the panel perimeter. It accepts "rainbow" for a chasing hue sweep, or an RGB array like [255, 100, 200] for a static colour.

Add border = "rainbow" to the headline widget:

[[playlist.section.widget]]
type = "message"
text = "Welcome to Firebird Yoga"
font_color = "rainbow"
border = "rainbow"

font_color = "rainbow" was also added here — the per-character hue sweep across the text and the chasing perimeter ring use the same hue formula, so they feel like one coherent effect rather than two competing ones.

For the second section’s headline:

[[playlist.section.widget]]
type = "message"
text = "Summer Camps start June 15"
font_color = [180, 220, 180]
border = "rainbow"

A few things worth knowing about borders:

  • The border paints at physical resolution — at default_scale = 4 it traces the real 256×64 panel edge, not the logical 64×16 wrapper. The ring is always 1 pixel wide at real-panel scale.
  • Border, transitions, and font effects all compose. The wipe transition runs cleanly over a bordered widget; the rainbow font sweep continues animating during the hold that the border is also chasing.
  • border is accepted on message, countdown, two_row, gif, and image widgets. It raises at config-load for other types.

The image widget currently shows the studio name in BDF 5x8 at default_scale = 4, which gives 64 logical columns. That’s enough for the short name but not for the full social handle: @firebirdyoga.demo (18 chars × 5 px = 90 px) would overflow the canvas by 26 px. To show the full handle and the hi-res Instagram sprite alongside it, the brand card needs its own section at scale = 2.

Two changes — restructure the section layout, then upgrade the handle.

Restructure. Pull the image widget out of Section 1 into its own section. This lets the brand card use scale = 2 without affecting the welcome and info widgets, which look right at the default scale = 4:

# Section 1 — welcome card
[[playlist.section]]
mode = "slideshow"
loop_count = 1
hold_time = 2.5
[[playlist.section.widget]]
type = "message"
text = "Welcome to Firebird Yoga"
font_color = "rainbow"
border = "rainbow"
# Section 2 — brand card (new, scale = 2)
[[playlist.section]]
mode = "slideshow"
scale = 2
content_height = 32
loop_count = 1
hold_time = 5.0
[[playlist.section.widget]]
type = "image"
path = "assets/phoenix_transparent.png"
fit = "pillarbox"
image_align = "center"
hold_time = 4.0
scroll_speed_ms = 35
top_text = ":instagram: @firebirdyoga.demo"
top_row_height = 16
top_font = "Inter-Bold"
top_font_size = 16
top_color = [225, 48, 108]
bottom_text = "Vinyasa · yin · hot power · restorative · slow flow · all levels"
bottom_font = "Inter-Bold"
bottom_font_size = 22
bottom_color = [255, 240, 200]
# Section 3 — info messages (the old Summer Camps + All levels section,
# plus the "Open daily 10am – 8pm" message moved out of Section 1)
[[playlist.section]]
mode = "slideshow"
loop_count = 1
hold_time = 2.5
[[playlist.section.widget]]
type = "message"
text = "Summer Camps start June 15"
font_color = [180, 220, 180]
border = "rainbow"
[[playlist.section.widget]]
type = "message"
text = "Open daily 10am – 8pm"
font_color = [120, 220, 255]
[[playlist.section.widget]]
type = "message"
text = "All levels · All ages"
font_color = [255, 220, 0]

What changed on the brand card:

  • scale = 2 on Section 2 — overrides default_scale = 4 for this section. Logical canvas widens from 64 to 128 px, leaving room for the longer handle and the hi-res Instagram sprite.
  • content_height = 32 — fills the full panel height at scale 2 (32 × 2 = 64 real px). The hard ceiling. This matters because the phoenix image uses fit = "pillarbox" and looks best filling the panel.
  • top_row_height = 16 — gives the top band 16 logical rows, which matches the hi-res Instagram sprite’s 16 logical px height (32 real px / scale = 16 logical). Without this, the sprite falls back to the 8×8 lo-res variant.
  • top_text = ":instagram: @firebirdyoga.demo" — the :instagram: inline emoji renders as the full-colour hi-res sprite now that the band can fit it. The remaining text is the full handle, no shortening needed.
  • top_font = "Inter-Bold", top_font_size = 16 — switched from 5x8 BDF to Inter-Bold for visual consistency with the bottom row.
  • bottom_font = "Inter-Bold" — switched from Atkinson Hyperlegible to the bundled Inter-Bold. Atkinson works great if you’re keeping a personal copy in config/fonts/, but the canonical destination config uses only bundled fonts so a fresh git clone can render it without the font download.
The final config. Welcome card with rainbow font + chasing border; brand card with the hi-res Instagram sprite and full @firebirdyoga.demo handle; info section with Summer Camps + Open daily + All levels. wipe_left transitions within sections, dissolve between.
The final config. Welcome card with rainbow font + chasing border; brand card with the hi-res Instagram sprite and full @firebirdyoga.demo handle; info section with Summer Camps + Open daily + All levels. wipe_left transitions within sections, dissolve between.

Your config now has:

  • A three-section playlist in slideshow mode with wipe_left transitions between widgets and dissolve between sections.
  • A welcome card with per-character rainbow font_color and a chasing rainbow border around the panel perimeter.
  • A brand card running at per-section scale = 2 so the hi-res Instagram sprite renders at the full 32×32 next to the @firebirdyoga.demo handle. The phoenix image fills the panel; promo text scrolls across the bottom row.
  • An info section with three message widgets — Summer Camps (rainbow border), Open daily, and All levels — held briefly so each is readable.

That matches the destination config you rendered in Chapter 1 (config/config.bigsign.firebird.example.toml). The next step is getting it onto a Pi.

Your config is ready — this is what the five chapters were building toward.

Quickstart B is the complete deploy path — it takes a Pi from bare OS to a running sign, and it ends with your panels lit. If the hardware itself isn’t built yet, Building your own covers panels, wiring, and power first.

One difference from a stock Quickstart B run: at its “copy the right example config” step, use the config you just built here instead — copy your config/ folder (with config.toml, fonts/, and assets/) into the cloned repo on the Pi. Already have the stack running? Copy the folder over and run make restart.

The hardware pages below have the full bill of materials, wiring diagrams, and Pi-specific tuning for gpio_slowdown, rp1_pio, and pixel_mapper_config.

With a working panel config as a foundation, a few directions worth exploring:

  • Try other transitions. Swap wipe_left for nyancat.forward or pokeball.forward (the nyancat / pokeball packages in the led-ticker-plugins monorepo) to see what the sprite-trail families look like on your hardware. The Transitions catalogue has a rendered preview for every option.
  • Add live data widgets. Plugin widgets pull live content and scroll it on the same canvas — rss.feed for headlines and weather.current for current conditions (the rss / weather packages), and crypto.coingecko (the crypto package) for crypto prices, all from led-ticker-plugins. See the Widgets reference for setup details and required API keys.
  • Validate before deploying. led-ticker validate config/config.toml catches typos, unknown widget types, and constraint violations before you restart the panel. The Validation rules page lists the most common mistakes and their fix.
  • Let Claude author configs for you. The creating-a-config skill in Claude Code knows the widget catalogue, bigsign vs smallsign tradeoffs, and the constraint list from CLAUDE.md. Describe what you want displayed and it builds the TOML.