on main help sign in
chore: improve anim performance
a6c4855Kujtim Hoxha
internal/tui/components/anim/anim.go+114−58
@@ -2,9 +2,12 @@
2package anim
2package anim
3
3
4import (
4import (
5+ "crypto/sha256"
6+ "fmt"
5 "image/color"
7 "image/color"
6 "math/rand/v2"
8 "math/rand/v2"
7 "strings"
9 "strings"
10+ "sync"
8 "sync/atomic"
11 "sync/atomic"
9 "time"
12 "time"
10
13
@@ -58,6 +61,29 @@ func nextID() int {
58 return int(atomic.AddInt64(&lastID, 1))
61 return int(atomic.AddInt64(&lastID, 1))
59}
62}
60
63
64+// Cache for expensive animation calculations
65+type animCache struct {
66+ initialFrames [][]string
67+ cyclingFrames [][]string
68+ width int
69+ labelWidth int
70+ label []string
71+ ellipsisFrames []string
72+}
73+
74+var (
75+ animCacheMutex sync.RWMutex
76+ animCacheMap = make(map[string]*animCache)
77+)
78+
79+// settingsHash creates a hash key for the settings to use for caching
80+func settingsHash(opts Settings) string {
81+ h := sha256.New()
82+ fmt.Fprintf(h, "%d-%s-%v-%v-%v-%t",
83+ opts.Size, opts.Label, opts.LabelColor, opts.GradColorA, opts.GradColorB, opts.CycleColors)
84+ return fmt.Sprintf("%x", h.Sum(nil))
85+}
86+
61// StepMsg is a message type used to trigger the next step in the animation.
87// StepMsg is a message type used to trigger the next step in the animation.
62type StepMsg struct{ id int }
88type StepMsg struct{ id int }
63
89
@@ -109,79 +135,109 @@ func New(opts Settings) (a Anim) {
109 }
135 }
110
136
111 a.id = nextID()
137 a.id = nextID()
112−
113 a.startTime = time.Now()
138 a.startTime = time.Now()
114 a.cyclingCharWidth = opts.Size
139 a.cyclingCharWidth = opts.Size
115− a.labelWidth = lipgloss.Width(opts.Label)
116 a.labelColor = opts.LabelColor
140 a.labelColor = opts.LabelColor
117
141
118− // Total width of anim, in cells.
142+ // Check cache first
119− a.width = opts.Size
143+ cacheKey := settingsHash(opts)
120− if opts.Label != "" {
144+ animCacheMutex.RLock()
121− a.width += labelGapWidth + lipgloss.Width(opts.Label)
145+ cached, exists := animCacheMap[cacheKey]
122− }
146+ animCacheMutex.RUnlock()
123−
147+
124− // Render the label
148+ if exists {
125− a.renderLabel(opts.Label)
149+ // Use cached values
126−
150+ a.width = cached.width
127− // Pre-generate gradient.
151+ a.labelWidth = cached.labelWidth
128− var ramp []color.Color
152+ a.label = cached.label
129− numFrames := prerenderedFrames
153+ a.ellipsisFrames = cached.ellipsisFrames
130− if opts.CycleColors {
154+ a.initialFrames = cached.initialFrames
131− ramp = makeGradientRamp(a.width*3, opts.GradColorA, opts.GradColorB, opts.GradColorA, opts.GradColorB)
155+ a.cyclingFrames = cached.cyclingFrames
132− numFrames = a.width * 2
133 } else {
156 } else {
134− ramp = makeGradientRamp(a.width, opts.GradColorA, opts.GradColorB)
157+ // Generate new values and cache them
135− }
158+ a.labelWidth = lipgloss.Width(opts.Label)
136
159
137− // Pre-render initial characters.
160+ // Total width of anim, in cells.
138− a.initialFrames = make([][]string, numFrames)
161+ a.width = opts.Size
139− offset := 0
162+ if opts.Label != "" {
140− for i := range a.initialFrames {
163+ a.width += labelGapWidth + lipgloss.Width(opts.Label)
141− a.initialFrames[i] = make([]string, a.width+labelGapWidth+a.labelWidth)
142− for j := range a.initialFrames[i] {
143− if j+offset >= len(ramp) {
144− continue // skip if we run out of colors
145− }
164+ }
146
165
147− var c color.Color
166+ // Render the label
148− if j <= a.cyclingCharWidth {
167+ a.renderLabel(opts.Label)
149− c = ramp[j+offset]
150− } else {
151− c = opts.LabelColor
152− }
153
168
154− // Also prerender the initial character with Lip Gloss to avoid
169+ // Pre-generate gradient.
155− // processing in the render loop.
170+ var ramp []color.Color
156− a.initialFrames[i][j] = lipgloss.NewStyle().
171+ numFrames := prerenderedFrames
157− Foreground(c).
158− Render(string(initialChar))
159− }
160 if opts.CycleColors {
172 if opts.CycleColors {
161− offset++
173+ ramp = makeGradientRamp(a.width*3, opts.GradColorA, opts.GradColorB, opts.GradColorA, opts.GradColorB)
174+ numFrames = a.width * 2
175+ } else {
176+ ramp = makeGradientRamp(a.width, opts.GradColorA, opts.GradColorB)
162 }
177 }
163− }
164
178
165− // Prerender scrambled rune frames for the animation.
179+ // Pre-render initial characters.
166− a.cyclingFrames = make([][]string, numFrames)
180+ a.initialFrames = make([][]string, numFrames)
167− offset = 0
181+ offset := 0
168− for i := range a.cyclingFrames {
182+ for i := range a.initialFrames {
169− a.cyclingFrames[i] = make([]string, a.width)
183+ a.initialFrames[i] = make([]string, a.width+labelGapWidth+a.labelWidth)
170− for j := range a.cyclingFrames[i] {
184+ for j := range a.initialFrames[i] {
171− if j+offset >= len(ramp) {
185+ if j+offset >= len(ramp) {
172− continue // skip if we run out of colors
186+ continue // skip if we run out of colors
187+ }
188+
189+ var c color.Color
190+ if j <= a.cyclingCharWidth {
191+ c = ramp[j+offset]
192+ } else {
193+ c = opts.LabelColor
194+ }
195+
196+ // Also prerender the initial character with Lip Gloss to avoid
197+ // processing in the render loop.
198+ a.initialFrames[i][j] = lipgloss.NewStyle().
199+ Foreground(c).
200+ Render(string(initialChar))
201+ }
202+ if opts.CycleColors {
203+ offset++
173 }
204 }
205+ }
174
206
207+ // Prerender scrambled rune frames for the animation.
208+ a.cyclingFrames = make([][]string, numFrames)
209+ offset = 0
210+ for i := range a.cyclingFrames {
211+ a.cyclingFrames[i] = make([]string, a.width)
212+ for j := range a.cyclingFrames[i] {
213+ if j+offset >= len(ramp) {
214+ continue // skip if we run out of colors
215+ }
216+
175− // Also prerender the color with Lip Gloss here to avoid processing
217+ // Also prerender the color with Lip Gloss here to avoid processing
176− // in the render loop.
218+ // in the render loop.
177− r := availableRunes[rand.IntN(len(availableRunes))]
219+ r := availableRunes[rand.IntN(len(availableRunes))]
178− a.cyclingFrames[i][j] = lipgloss.NewStyle().
220+ a.cyclingFrames[i][j] = lipgloss.NewStyle().
179− Foreground(ramp[j+offset]).
221+ Foreground(ramp[j+offset]).
180− Render(string(r))
222+ Render(string(r))
223+ }
224+ if opts.CycleColors {
225+ offset++
226+ }
181 }
227 }
182− if opts.CycleColors {
228+
183− offset++
229+ // Cache the results
230+ cached = &animCache{
231+ initialFrames: a.initialFrames,
232+ cyclingFrames: a.cyclingFrames,
233+ width: a.width,
234+ labelWidth: a.labelWidth,
235+ label: a.label,
236+ ellipsisFrames: a.ellipsisFrames,
184 }
237 }
238+ animCacheMutex.Lock()
239+ animCacheMap[cacheKey] = cached
240+ animCacheMutex.Unlock()
185 }
241 }
186
242
187 // Random assign a birth to each character for a stagged entrance effect.
243 // Random assign a birth to each character for a stagged entrance effect.