Feedback effects
Shake rejected input, show skeleton placeholders that shimmer while loading, roll changing numbers, burst confetti, glitch a badge, spotlight a tour step, and scroll text that doesn't fit.
Some effects aren't entrances. They respond to something happening: input rejected, data loading, a number
changing, a deploy succeeding. Each takes its own progress, usually from a one-shot
anim.Value or a looping
anim.Sequence.
The pattern for one-shot effects is the same each time:
- Keep an
anim.Valuethat starts at 1, with a duration and linear easing. - When the event happens,
Play(0, 1)and return its command. - In
View, wrap the node withbloom.Transitiononly while the value is animating.
Shake rejected input
fx.Shake moves a node side to side with a decaying wobble. It's still at 0 and at 1.
shake: anim.NewValue(1, anim.Duration(450*time.Millisecond), anim.Ease(anim.Linear)),// in Update, when the input is wrong
cmd = m.shake.Play(0, 1)
// in View
field := bloom.Child(input)
if m.shake.Animating() {
field = bloom.Transition(m.shake.Float(), field, fx.Shake{Amount: 3, Times: 3})
}Amount is how many cells it moves at most (2 by default), and Times is how many times it swings each way (4 by
default).
Skeleton loading placeholders
bloom.Skeleton draws placeholder bars while data
loads. Each number is a line: a bar that many cells wide, or 0 for a blank line. Give it the same number of lines
as the content it stands in for, so nothing moves when the content arrives.
Shimmer sweeps a band of light across the bars. Its phase comes from a looping sequence in your model:
shimmer: anim.NewLoop(1400 * time.Millisecond),func (m *model) Init() tea.Cmd {
// fetchStats is your command that loads the data.
return tea.Batch(m.ui.Init(), m.shimmer.Loop(), fetchStats)
}
// when the data arrives
m.loaded = true
m.shimmer.Stop()
// in View: four lines, as the loaded card has four
if !m.loaded {
return bloom.Box(card, bloom.Skeleton(8, 0, 5, 11).Shimmer(m.shimmer.Float()))
}
return bloom.Box(card, bloom.Column(
bloom.Text("Requests").Style(heading),
bloom.Fixed(1, nil),
bloom.Text(strconv.Itoa(m.requests)).Style(big),
bloom.Text("per minute").Style(label),
))Bars are drawn in the terminal's foreground faded most of the way into its background, so they suit any theme.
Style colors them yourself, and bars wider than the space they're given are cut to fit.
The band starts just off one side and ends just off the other, so the loop restarts without a visible jump. It's
fx.Shimmer, which works on any node. For a band of
another color or width, or to shimmer a whole card, draw it in a transition instead:
bloom.Transition(m.shimmer.Float(), placeholder, fx.Shimmer{Color: accent, Width: 10})Roll changing numbers
fx.Roll turns the digits of a number over like an odometer. Draw the new number, and give the effect the number
shown before:
roll: anim.NewValue(1, anim.Duration(700*time.Millisecond), anim.Ease(anim.Linear)),// when the number changes
m.previous, m.requests = m.requests, latest
cmd = m.roll.Play(0, 1)
// in View
num := bloom.Child(bloom.Text(strconv.Itoa(m.requests)))
if m.roll.Animating() {
num = bloom.Transition(m.roll.Float(), num, fx.Roll{From: strconv.Itoa(m.previous)})
}Digits are matched from the right and separators like commas are skipped, so going from 1,021 to 1,058 only turns
the last two digits over. Roll works on the text, so format the number however you like, as long as From is
formatted the same way.
Burst confetti
fx.Burst throws particles out from a point. Its progress is the burst's age, and nothing is drawn at 1. Apply it
to the whole screen so particles can fly anywhere, starting from a button's position in the last frame:
burst: anim.NewValue(1, anim.Duration(1800*time.Millisecond), anim.Ease(anim.Linear)),screen := bloom.Child(layout)
if m.burst.Animating() {
if r, ok := m.ui.Frame().Rect(buttonDeploy); ok {
screen = bloom.Transition(m.burst.Float(), screen, fx.Burst{
At: image.Pt((r.Min.X+r.Max.X)/2, r.Min.Y),
Count: 90,
Seed: m.deploys,
})
}
}Particles only change the glyph and color of cells they land on, and stay inside the node the effect is applied to.
Glitch a badge
fx.Glitch makes a node look like a breaking-up signal: rows jitter, colors split, and cells garble, fading to
nothing at 1. It works well as an entrance for a status that just changed:
badge := bloom.Child(bloom.Text("LIVE").Style(live))
if m.badge.Animating() {
badge = bloom.Transition(m.badge.Float(), badge, fx.Glitch{Seed: m.deploys})
}Spotlight a tour
fx.Spotlight dims everything except a rectangle. As a layer's backdrop, it walks the user through a screen one
part at a time. Put the explanation in a layer, and light the part it explains:
note := bloom.Layer(bloom.Box(tourStyle, bloom.Text(steps[m.step]))).
Anchor(bloom.Center, bloom.End).
Margin(2).
Presence(m.tour, fx.Fade{})
if r, ok := m.ui.Frame().Rect(cardID(m.step)); ok {
note = note.Backdrop(fx.Spotlight{Rect: r.Inset(-1), Soft: 5})
}Soft is how many cells the dimming takes to reach full strength, for a soft edge. fx.Decode suits the
explanation text, revealing it like digital rain settling into place.
Scroll text that doesn't fit
bloom.Marquee(s, phase) shows one line of text. When it fits, it's shown as is. When it doesn't, it's cut short
with "…" while phase is 0, and scrolls around as phase runs from 0 to 1. That suits a long title in a list
item, scrolling only while the item is selected:
name: anim.NewSequence(0,
anim.Jump(0),
anim.Hold(900*time.Millisecond), // wait a moment before scrolling
anim.To(1, anim.Duration(7*time.Second)),
).With(anim.Ease(anim.Linear)),// when an item is selected
cmd = m.name.Loop()
// in View
phase := 0.0
if i == m.selected {
phase = m.name.Float()
}
bloom.Fill(bloom.Marquee(item.title, phase).Style(itemStyle))For a news ticker that always scrolls, use anim.NewLoop(24 * time.Second) as the phase, and Gap to set what's
shown between the end of the text and its start: bloom.Marquee(headlines, phase).Gap(" ◆ ").
Related
- Effects reference
- The
launchexample, which uses all of these effects in a deploy console, and thetickerexample for marquees
Last updated on