theme.go 17 KB

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