theme.go

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