theme.go

  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}