1package styles
2
3import (
4 "fmt"
5 "image/color"
6 "strings"
7 "sync"
8
9 "charm.land/bubbles/v2/filepicker"
10 "charm.land/bubbles/v2/help"
11 "charm.land/bubbles/v2/textarea"
12 "charm.land/bubbles/v2/textinput"
13 tea "charm.land/bubbletea/v2"
14 "charm.land/glamour/v2/ansi"
15 "charm.land/lipgloss/v2"
16 "git.secluded.site/crush/internal/tui/exp/diffview"
17 "github.com/charmbracelet/x/exp/charmtone"
18 "github.com/lucasb-eyer/go-colorful"
19 "github.com/rivo/uniseg"
20)
21
22const (
23 defaultListIndent = 2
24 defaultListLevelIndent = 4
25 defaultMargin = 2
26)
27
28type Theme struct {
29 Name string
30 IsDark bool
31
32 Primary color.Color
33 Secondary color.Color
34 Tertiary color.Color
35 Accent color.Color
36
37 BgBase color.Color
38 BgBaseLighter color.Color
39 BgSubtle color.Color
40 BgOverlay color.Color
41
42 FgBase color.Color
43 FgMuted color.Color
44 FgHalfMuted color.Color
45 FgSubtle color.Color
46 FgSelected color.Color
47
48 Border color.Color
49 BorderFocus color.Color
50
51 Success color.Color
52 Error color.Color
53 Warning color.Color
54 Info color.Color
55
56 // Colors
57 // White
58 White color.Color
59
60 // Blues
61 BlueLight color.Color
62 BlueDark color.Color
63 Blue color.Color
64
65 // Yellows
66 Yellow color.Color
67 Citron color.Color
68
69 // Greens
70 Green color.Color
71 GreenDark color.Color
72 GreenLight color.Color
73
74 // Reds
75 Red color.Color
76 RedDark color.Color
77 RedLight color.Color
78 Cherry color.Color
79
80 // Markdown colors (theme-aware for accessibility).
81 MdText color.Color // Document text
82 MdHeading color.Color // Heading color
83 MdH6 color.Color // H6 color
84 MdHRule color.Color // Horizontal rule
85 MdLink color.Color // Link URL
86 MdLinkText color.Color // Link text
87 MdImage color.Color // Image URL
88 MdImageText color.Color // Image alt text
89 MdCodeFg color.Color // Inline code foreground
90 MdCodeBg color.Color // Inline code background
91 MdCodeBlockFg color.Color // Code block text
92 MdCodeBlockBg color.Color // Code block background
93 MdComment color.Color // Code comment
94 MdKeyword color.Color // Keyword
95 MdKeywordAlt color.Color // Keyword reserved/namespace
96 MdKeywordType color.Color // Type keyword
97 MdOperator color.Color // Operator
98 MdPunctuation color.Color // Punctuation
99 MdName color.Color // Name/identifier
100 MdNameBuiltin color.Color // Builtin name
101 MdNameTag color.Color // Tag name
102 MdNameAttr color.Color // Attribute name
103 MdNameClass color.Color // Class name
104 MdNameDecorator color.Color // Decorator
105 MdNameFunc color.Color // Function name
106 MdNumber color.Color // Literal number
107 MdString color.Color // Literal string
108 MdStringEscape color.Color // String escape
109 MdDeleted color.Color // Deleted text
110 MdInserted color.Color // Inserted text
111 MdSubheading color.Color // Subheading
112 MdError color.Color // Error foreground
113 MdErrorBg color.Color // Error background
114
115 // Diff colors (theme-aware).
116 DiffInsertFg color.Color // Insert line number fg
117 DiffInsertBg color.Color // Insert line number bg
118 DiffInsertSymBg color.Color // Insert symbol bg
119 DiffInsertCodeBg color.Color // Insert code bg
120 DiffDeleteFg color.Color // Delete line number fg
121 DiffDeleteBg color.Color // Delete line number bg
122 DiffDeleteSymBg color.Color // Delete symbol bg
123 DiffDeleteCodeBg color.Color // Delete code bg
124
125 // Text selection.
126 TextSelection lipgloss.Style
127
128 // LSP and MCP status indicators.
129 ItemOfflineIcon lipgloss.Style
130 ItemBusyIcon lipgloss.Style
131 ItemErrorIcon lipgloss.Style
132 ItemOnlineIcon lipgloss.Style
133
134 // Editor: Yolo Mode.
135 YoloIconFocused lipgloss.Style
136 YoloIconBlurred lipgloss.Style
137 YoloDotsFocused lipgloss.Style
138 YoloDotsBlurred lipgloss.Style
139
140 // oAuth Chooser.
141 AuthBorderSelected lipgloss.Style
142 AuthTextSelected lipgloss.Style
143 AuthBorderUnselected lipgloss.Style
144 AuthTextUnselected lipgloss.Style
145
146 styles *Styles
147 stylesOnce sync.Once
148}
149
150type Styles struct {
151 Base lipgloss.Style
152 SelectedBase lipgloss.Style
153
154 Title lipgloss.Style
155 Subtitle lipgloss.Style
156 Text lipgloss.Style
157 TextSelected lipgloss.Style
158 Muted lipgloss.Style
159 Subtle lipgloss.Style
160
161 Success lipgloss.Style
162 Error lipgloss.Style
163 Warning lipgloss.Style
164 Info lipgloss.Style
165
166 // Markdown & Chroma
167 Markdown ansi.StyleConfig
168
169 // Inputs
170 TextInput textinput.Styles
171 TextArea textarea.Styles
172
173 // Help
174 Help help.Styles
175
176 // Diff
177 Diff diffview.Style
178
179 // FilePicker
180 FilePicker filepicker.Styles
181}
182
183func (t *Theme) S() *Styles {
184 t.stylesOnce.Do(func() {
185 t.styles = t.buildStyles()
186 })
187 return t.styles
188}
189
190func (t *Theme) buildStyles() *Styles {
191 base := lipgloss.NewStyle().
192 Foreground(t.FgBase)
193 return &Styles{
194 Base: base,
195
196 SelectedBase: base.Background(t.Primary),
197
198 Title: base.
199 Foreground(t.Accent).
200 Bold(true),
201
202 Subtitle: base.
203 Foreground(t.Secondary).
204 Bold(true),
205
206 Text: base,
207 TextSelected: base.Background(t.Primary).Foreground(t.FgSelected),
208
209 Muted: base.Foreground(t.FgMuted),
210
211 Subtle: base.Foreground(t.FgSubtle),
212
213 Success: base.Foreground(t.Success),
214
215 Error: base.Foreground(t.Error),
216
217 Warning: base.Foreground(t.Warning),
218
219 Info: base.Foreground(t.Info),
220
221 TextInput: textinput.Styles{
222 Focused: textinput.StyleState{
223 Text: base,
224 Placeholder: base.Foreground(t.FgSubtle),
225 Prompt: base.Foreground(t.Tertiary),
226 Suggestion: base.Foreground(t.FgSubtle),
227 },
228 Blurred: textinput.StyleState{
229 Text: base.Foreground(t.FgMuted),
230 Placeholder: base.Foreground(t.FgSubtle),
231 Prompt: base.Foreground(t.FgMuted),
232 Suggestion: base.Foreground(t.FgSubtle),
233 },
234 Cursor: textinput.CursorStyle{
235 Color: t.Secondary,
236 Shape: tea.CursorBlock,
237 Blink: true,
238 },
239 },
240 TextArea: textarea.Styles{
241 Focused: textarea.StyleState{
242 Base: base,
243 Text: base,
244 LineNumber: base.Foreground(t.FgSubtle),
245 CursorLine: base,
246 CursorLineNumber: base.Foreground(t.FgSubtle),
247 Placeholder: base.Foreground(t.FgSubtle),
248 Prompt: base.Foreground(t.Tertiary),
249 },
250 Blurred: textarea.StyleState{
251 Base: base,
252 Text: base.Foreground(t.FgMuted),
253 LineNumber: base.Foreground(t.FgMuted),
254 CursorLine: base,
255 CursorLineNumber: base.Foreground(t.FgMuted),
256 Placeholder: base.Foreground(t.FgSubtle),
257 Prompt: base.Foreground(t.FgMuted),
258 },
259 Cursor: textarea.CursorStyle{
260 Color: t.Secondary,
261 Shape: tea.CursorBlock,
262 Blink: true,
263 },
264 },
265
266 Markdown: ansi.StyleConfig{
267 Document: ansi.StyleBlock{
268 StylePrimitive: ansi.StylePrimitive{
269 // BlockPrefix: "\n",
270 // BlockSuffix: "\n",
271 Color: stringPtr(ToHex(t.MdText)),
272 },
273 // Margin: uintPtr(defaultMargin),
274 },
275 BlockQuote: ansi.StyleBlock{
276 StylePrimitive: ansi.StylePrimitive{},
277 Indent: uintPtr(1),
278 IndentToken: stringPtr("│ "),
279 },
280 List: ansi.StyleList{
281 LevelIndent: defaultListIndent,
282 },
283 Heading: ansi.StyleBlock{
284 StylePrimitive: ansi.StylePrimitive{
285 BlockSuffix: "\n",
286 Color: stringPtr(ToHex(t.MdHeading)),
287 Bold: boolPtr(true),
288 },
289 },
290 H1: ansi.StyleBlock{
291 StylePrimitive: ansi.StylePrimitive{
292 Prefix: " ",
293 Suffix: " ",
294 Color: stringPtr(charmtone.Zest.Hex()),
295 BackgroundColor: stringPtr(charmtone.Charple.Hex()),
296 Bold: boolPtr(true),
297 },
298 },
299 H2: ansi.StyleBlock{
300 StylePrimitive: ansi.StylePrimitive{
301 Prefix: "## ",
302 },
303 },
304 H3: ansi.StyleBlock{
305 StylePrimitive: ansi.StylePrimitive{
306 Prefix: "### ",
307 },
308 },
309 H4: ansi.StyleBlock{
310 StylePrimitive: ansi.StylePrimitive{
311 Prefix: "#### ",
312 },
313 },
314 H5: ansi.StyleBlock{
315 StylePrimitive: ansi.StylePrimitive{
316 Prefix: "##### ",
317 },
318 },
319 H6: ansi.StyleBlock{
320 StylePrimitive: ansi.StylePrimitive{
321 Prefix: "###### ",
322 Color: stringPtr(ToHex(t.MdH6)),
323 Bold: boolPtr(false),
324 },
325 },
326 Strikethrough: ansi.StylePrimitive{
327 CrossedOut: boolPtr(true),
328 },
329 Emph: ansi.StylePrimitive{
330 Italic: boolPtr(true),
331 },
332 Strong: ansi.StylePrimitive{
333 Bold: boolPtr(true),
334 },
335 HorizontalRule: ansi.StylePrimitive{
336 Color: stringPtr(ToHex(t.MdHRule)),
337 Format: "\n--------\n",
338 },
339 Item: ansi.StylePrimitive{
340 BlockPrefix: "• ",
341 },
342 Enumeration: ansi.StylePrimitive{
343 BlockPrefix: ". ",
344 },
345 Task: ansi.StyleTask{
346 StylePrimitive: ansi.StylePrimitive{},
347 Ticked: "[✓] ",
348 Unticked: "[ ] ",
349 },
350 Link: ansi.StylePrimitive{
351 Color: stringPtr(ToHex(t.MdLink)),
352 Underline: boolPtr(true),
353 },
354 LinkText: ansi.StylePrimitive{
355 Color: stringPtr(ToHex(t.MdLinkText)),
356 Bold: boolPtr(true),
357 },
358 Image: ansi.StylePrimitive{
359 Color: stringPtr(ToHex(t.MdImage)),
360 Underline: boolPtr(true),
361 },
362 ImageText: ansi.StylePrimitive{
363 Color: stringPtr(ToHex(t.MdImageText)),
364 Format: "Image: {{.text}} →",
365 },
366 Code: ansi.StyleBlock{
367 StylePrimitive: ansi.StylePrimitive{
368 Prefix: " ",
369 Suffix: " ",
370 Color: stringPtr(ToHex(t.MdCodeFg)),
371 BackgroundColor: stringPtr(ToHex(t.MdCodeBg)),
372 },
373 },
374 CodeBlock: ansi.StyleCodeBlock{
375 StyleBlock: ansi.StyleBlock{
376 StylePrimitive: ansi.StylePrimitive{
377 Color: stringPtr(ToHex(t.MdCodeBlockFg)),
378 },
379 Margin: uintPtr(defaultMargin),
380 },
381 Chroma: &ansi.Chroma{
382 Text: ansi.StylePrimitive{
383 Color: stringPtr(ToHex(t.MdText)),
384 },
385 Error: ansi.StylePrimitive{
386 Color: stringPtr(ToHex(t.MdError)),
387 BackgroundColor: stringPtr(ToHex(t.MdErrorBg)),
388 },
389 Comment: ansi.StylePrimitive{
390 Color: stringPtr(ToHex(t.MdComment)),
391 },
392 CommentPreproc: ansi.StylePrimitive{
393 Color: stringPtr(ToHex(t.MdComment)),
394 },
395 Keyword: ansi.StylePrimitive{
396 Color: stringPtr(ToHex(t.MdKeyword)),
397 },
398 KeywordReserved: ansi.StylePrimitive{
399 Color: stringPtr(ToHex(t.MdKeywordAlt)),
400 },
401 KeywordNamespace: ansi.StylePrimitive{
402 Color: stringPtr(ToHex(t.MdKeywordAlt)),
403 },
404 KeywordType: ansi.StylePrimitive{
405 Color: stringPtr(ToHex(t.MdKeywordType)),
406 },
407 Operator: ansi.StylePrimitive{
408 Color: stringPtr(ToHex(t.MdOperator)),
409 },
410 Punctuation: ansi.StylePrimitive{
411 Color: stringPtr(ToHex(t.MdPunctuation)),
412 },
413 Name: ansi.StylePrimitive{
414 Color: stringPtr(ToHex(t.MdName)),
415 },
416 NameBuiltin: ansi.StylePrimitive{
417 Color: stringPtr(ToHex(t.MdNameBuiltin)),
418 },
419 NameTag: ansi.StylePrimitive{
420 Color: stringPtr(ToHex(t.MdNameTag)),
421 },
422 NameAttribute: ansi.StylePrimitive{
423 Color: stringPtr(ToHex(t.MdNameAttr)),
424 },
425 NameClass: ansi.StylePrimitive{
426 Color: stringPtr(ToHex(t.MdNameClass)),
427 Underline: boolPtr(true),
428 Bold: boolPtr(true),
429 },
430 NameDecorator: ansi.StylePrimitive{
431 Color: stringPtr(ToHex(t.MdNameDecorator)),
432 },
433 NameFunction: ansi.StylePrimitive{
434 Color: stringPtr(ToHex(t.MdNameFunc)),
435 },
436 LiteralNumber: ansi.StylePrimitive{
437 Color: stringPtr(ToHex(t.MdNumber)),
438 },
439 LiteralString: ansi.StylePrimitive{
440 Color: stringPtr(ToHex(t.MdString)),
441 },
442 LiteralStringEscape: ansi.StylePrimitive{
443 Color: stringPtr(ToHex(t.MdStringEscape)),
444 },
445 GenericDeleted: ansi.StylePrimitive{
446 Color: stringPtr(ToHex(t.MdDeleted)),
447 },
448 GenericEmph: ansi.StylePrimitive{
449 Italic: boolPtr(true),
450 },
451 GenericInserted: ansi.StylePrimitive{
452 Color: stringPtr(ToHex(t.MdInserted)),
453 },
454 GenericStrong: ansi.StylePrimitive{
455 Bold: boolPtr(true),
456 },
457 GenericSubheading: ansi.StylePrimitive{
458 Color: stringPtr(ToHex(t.MdSubheading)),
459 },
460 Background: ansi.StylePrimitive{
461 BackgroundColor: stringPtr(ToHex(t.MdCodeBlockBg)),
462 },
463 },
464 },
465 Table: ansi.StyleTable{
466 StyleBlock: ansi.StyleBlock{
467 StylePrimitive: ansi.StylePrimitive{},
468 },
469 },
470 DefinitionDescription: ansi.StylePrimitive{
471 BlockPrefix: "\n ",
472 },
473 },
474
475 Help: help.Styles{
476 ShortKey: base.Foreground(t.FgMuted),
477 ShortDesc: base.Foreground(t.FgSubtle),
478 ShortSeparator: base.Foreground(t.Border),
479 Ellipsis: base.Foreground(t.Border),
480 FullKey: base.Foreground(t.FgMuted),
481 FullDesc: base.Foreground(t.FgSubtle),
482 FullSeparator: base.Foreground(t.Border),
483 },
484
485 Diff: diffview.Style{
486 DividerLine: diffview.LineStyle{
487 LineNumber: lipgloss.NewStyle().
488 Foreground(t.FgHalfMuted).
489 Background(t.BgBaseLighter),
490 Code: lipgloss.NewStyle().
491 Foreground(t.FgHalfMuted).
492 Background(t.BgBaseLighter),
493 },
494 MissingLine: diffview.LineStyle{
495 LineNumber: lipgloss.NewStyle().
496 Background(t.BgBaseLighter),
497 Code: lipgloss.NewStyle().
498 Background(t.BgBaseLighter),
499 },
500 EqualLine: diffview.LineStyle{
501 LineNumber: lipgloss.NewStyle().
502 Foreground(t.FgMuted).
503 Background(t.BgBase),
504 Code: lipgloss.NewStyle().
505 Foreground(t.FgMuted).
506 Background(t.BgBase),
507 },
508 InsertLine: diffview.LineStyle{
509 LineNumber: lipgloss.NewStyle().
510 Foreground(t.DiffInsertFg).
511 Background(t.DiffInsertBg),
512 Symbol: lipgloss.NewStyle().
513 Foreground(t.DiffInsertFg).
514 Background(t.DiffInsertSymBg),
515 Code: lipgloss.NewStyle().
516 Background(t.DiffInsertCodeBg),
517 },
518 DeleteLine: diffview.LineStyle{
519 LineNumber: lipgloss.NewStyle().
520 Foreground(t.DiffDeleteFg).
521 Background(t.DiffDeleteBg),
522 Symbol: lipgloss.NewStyle().
523 Foreground(t.DiffDeleteFg).
524 Background(t.DiffDeleteSymBg),
525 Code: lipgloss.NewStyle().
526 Background(t.DiffDeleteCodeBg),
527 },
528 },
529 FilePicker: filepicker.Styles{
530 DisabledCursor: base.Foreground(t.FgMuted),
531 Cursor: base.Foreground(t.FgBase),
532 Symlink: base.Foreground(t.FgSubtle),
533 Directory: base.Foreground(t.Primary),
534 File: base.Foreground(t.FgBase),
535 DisabledFile: base.Foreground(t.FgMuted),
536 DisabledSelected: base.Background(t.BgOverlay).Foreground(t.FgMuted),
537 Permission: base.Foreground(t.FgMuted),
538 Selected: base.Background(t.Primary).Foreground(t.FgBase),
539 FileSize: base.Foreground(t.FgMuted),
540 EmptyDirectory: base.Foreground(t.FgMuted).PaddingLeft(2).SetString("Empty directory"),
541 },
542 }
543}
544
545type Manager struct {
546 themes map[string]*Theme
547 current *Theme
548}
549
550var (
551 defaultManager *Manager
552 defaultManagerOnce sync.Once
553)
554
555func initDefaultManager() *Manager {
556 defaultManagerOnce.Do(func() {
557 defaultManager = newManager()
558 })
559 return defaultManager
560}
561
562func SetDefaultManager(m *Manager) {
563 defaultManager = m
564}
565
566func DefaultManager() *Manager {
567 return initDefaultManager()
568}
569
570func CurrentTheme() *Theme {
571 return initDefaultManager().Current()
572}
573
574func newManager() *Manager {
575 m := &Manager{
576 themes: make(map[string]*Theme),
577 }
578
579 dark := NewCharmtoneTheme()
580 light := NewLightTheme()
581 m.Register(dark)
582 m.Register(light)
583 m.current = dark // default to dark theme
584
585 return m
586}
587
588func (m *Manager) Register(theme *Theme) {
589 m.themes[theme.Name] = theme
590}
591
592func (m *Manager) Current() *Theme {
593 return m.current
594}
595
596func (m *Manager) SetTheme(name string) error {
597 if theme, ok := m.themes[name]; ok {
598 m.current = theme
599 return nil
600 }
601 return fmt.Errorf("theme %s not found", name)
602}
603
604func (m *Manager) List() []string {
605 names := make([]string, 0, len(m.themes))
606 for name := range m.themes {
607 names = append(names, name)
608 }
609 return names
610}
611
612// ParseHex converts hex string to color
613func ParseHex(hex string) color.Color {
614 var r, g, b uint8
615 fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b)
616 return color.RGBA{R: r, G: g, B: b, A: 255}
617}
618
619// ToHex converts a color.Color to hex string.
620func ToHex(c color.Color) string {
621 r, g, b, _ := c.RGBA()
622 return fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8)
623}
624
625// Alpha returns a color with transparency
626func Alpha(c color.Color, alpha uint8) color.Color {
627 r, g, b, _ := c.RGBA()
628 return color.RGBA{
629 R: uint8(r >> 8),
630 G: uint8(g >> 8),
631 B: uint8(b >> 8),
632 A: alpha,
633 }
634}
635
636// Darken makes a color darker by percentage (0-100)
637func Darken(c color.Color, percent float64) color.Color {
638 r, g, b, a := c.RGBA()
639 factor := 1.0 - percent/100.0
640 return color.RGBA{
641 R: uint8(float64(r>>8) * factor),
642 G: uint8(float64(g>>8) * factor),
643 B: uint8(float64(b>>8) * factor),
644 A: uint8(a >> 8),
645 }
646}
647
648// Lighten makes a color lighter by percentage (0-100)
649func Lighten(c color.Color, percent float64) color.Color {
650 r, g, b, a := c.RGBA()
651 factor := percent / 100.0
652 return color.RGBA{
653 R: uint8(min(255, float64(r>>8)+255*factor)),
654 G: uint8(min(255, float64(g>>8)+255*factor)),
655 B: uint8(min(255, float64(b>>8)+255*factor)),
656 A: uint8(a >> 8),
657 }
658}
659
660func ForegroundGrad(input string, bold bool, color1, color2 color.Color) []string {
661 if input == "" {
662 return []string{""}
663 }
664 t := CurrentTheme()
665 if len(input) == 1 {
666 style := t.S().Base.Foreground(color1)
667 if bold {
668 style.Bold(true)
669 }
670 return []string{style.Render(input)}
671 }
672 var clusters []string
673 gr := uniseg.NewGraphemes(input)
674 for gr.Next() {
675 clusters = append(clusters, string(gr.Runes()))
676 }
677
678 ramp := blendColors(len(clusters), color1, color2)
679 for i, c := range ramp {
680 style := t.S().Base.Foreground(c)
681 if bold {
682 style.Bold(true)
683 }
684 clusters[i] = style.Render(clusters[i])
685 }
686 return clusters
687}
688
689// ApplyForegroundGrad renders a given string with a horizontal gradient
690// foreground.
691func ApplyForegroundGrad(input string, color1, color2 color.Color) string {
692 if input == "" {
693 return ""
694 }
695 var o strings.Builder
696 clusters := ForegroundGrad(input, false, color1, color2)
697 for _, c := range clusters {
698 fmt.Fprint(&o, c)
699 }
700 return o.String()
701}
702
703// ApplyBoldForegroundGrad renders a given string with a horizontal gradient
704// foreground.
705func ApplyBoldForegroundGrad(input string, color1, color2 color.Color) string {
706 if input == "" {
707 return ""
708 }
709 var o strings.Builder
710 clusters := ForegroundGrad(input, true, color1, color2)
711 for _, c := range clusters {
712 fmt.Fprint(&o, c)
713 }
714 return o.String()
715}
716
717// blendColors returns a slice of colors blended between the given keys.
718// Blending is done in Hcl to stay in gamut.
719func blendColors(size int, stops ...color.Color) []color.Color {
720 if len(stops) < 2 {
721 return nil
722 }
723
724 stopsPrime := make([]colorful.Color, len(stops))
725 for i, k := range stops {
726 stopsPrime[i], _ = colorful.MakeColor(k)
727 }
728
729 numSegments := len(stopsPrime) - 1
730 blended := make([]color.Color, 0, size)
731
732 // Calculate how many colors each segment should have.
733 segmentSizes := make([]int, numSegments)
734 baseSize := size / numSegments
735 remainder := size % numSegments
736
737 // Distribute the remainder across segments.
738 for i := range numSegments {
739 segmentSizes[i] = baseSize
740 if i < remainder {
741 segmentSizes[i]++
742 }
743 }
744
745 // Generate colors for each segment.
746 for i := range numSegments {
747 c1 := stopsPrime[i]
748 c2 := stopsPrime[i+1]
749 segmentSize := segmentSizes[i]
750
751 for j := range segmentSize {
752 var t float64
753 if segmentSize > 1 {
754 t = float64(j) / float64(segmentSize-1)
755 }
756 c := c1.BlendHcl(c2, t)
757 blended = append(blended, c)
758 }
759 }
760
761 return blended
762}