chore(ui): use Lip Gloss for color blends

Christian Rocha created

Change summary

internal/ui/common/elements.go   |  2 
internal/ui/logo/example/main.go | 23 +++++++++++
internal/ui/logo/logo.go         | 10 ++--
internal/ui/styles/grad.go       | 65 ++++-----------------------------
4 files changed, 37 insertions(+), 63 deletions(-)

Detailed changes

internal/ui/common/elements.go 🔗

@@ -193,7 +193,7 @@ func DialogTitle(t *styles.Styles, title string, width int, fromColor, toColor c
 	remainingWidth := width - length
 	if remainingWidth > 0 {
 		lines := strings.Repeat(char, remainingWidth)
-		lines = styles.ApplyForegroundGrad(t, lines, fromColor, toColor)
+		lines = styles.ApplyForegroundGrad(t.Base, lines, fromColor, toColor)
 		title = title + " " + lines
 	}
 	return title

internal/ui/logo/example/main.go 🔗

@@ -3,10 +3,13 @@ package main
 import (
 	"fmt"
 	"math/rand/v2"
+	"os"
 
 	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/ui/logo"
+	"github.com/charmbracelet/crush/internal/ui/styles"
 	"github.com/charmbracelet/x/exp/slice"
+	"github.com/charmbracelet/x/term"
 )
 
 func renderLetterforms(stretch bool) string {
@@ -42,8 +45,26 @@ func renderLetterforms(stretch bool) string {
 }
 
 func main() {
+	w, _, err := term.GetSize(os.Stdout.Fd())
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Could not get terminal size: %s", err)
+	}
+
+	s := styles.DefaultStyles()
+	opts := logo.Opts{
+		FieldColor:   s.LogoFieldColor,
+		TitleColorA:  s.LogoTitleColorA,
+		TitleColorB:  s.LogoTitleColorB,
+		CharmColor:   s.LogoCharmColor,
+		VersionColor: s.LogoVersionColor,
+		Width:        w,
+	}
+
+	lipgloss.Println(logo.Render(s.Base, "v1.0.0", false, opts))
+	lipgloss.Println(logo.Render(s.Base, "v1.0.0", true, opts))
+
 	fmt.Println(renderLetterforms(false))
-	for range 10 {
+	for range 5 {
 		fmt.Println(renderLetterforms(true))
 	}
 }

internal/ui/logo/logo.go 🔗

@@ -23,8 +23,9 @@ type Opts struct {
 	TitleColorA  color.Color // left gradient ramp point
 	TitleColorB  color.Color // right gradient ramp point
 	CharmColor   color.Color // Charm™ text color
-	VersionColor color.Color // Version text color
+	VersionColor color.Color // version text color
 	Width        int         // width of the rendered logo, used for truncation
+	Hyper        bool        // whether it is Crush or Hypercrush
 }
 
 // Render renders the Crush logo. Set the argument to true to render the narrow
@@ -32,7 +33,7 @@ type Opts struct {
 //
 // The compact argument determines whether it renders compact for the sidebar
 // or wider for the main pane.
-func Render(s *styles.Styles, version string, compact bool, o Opts) string {
+func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
 	const charm = " Charm™"
 
 	fg := func(c color.Color, s string) string {
@@ -52,12 +53,11 @@ func Render(s *styles.Styles, version string, compact bool, o Opts) string {
 	if !compact {
 		stretchIndex = cachedRandN(len(letterforms))
 	}
-
 	crush := renderWord(spacing, stretchIndex, letterforms...)
 	crushWidth := lipgloss.Width(crush)
 	b := new(strings.Builder)
 	for r := range strings.SplitSeq(crush, "\n") {
-		fmt.Fprintln(b, styles.ApplyForegroundGrad(s, r, o.TitleColorA, o.TitleColorB))
+		fmt.Fprintln(b, styles.ApplyForegroundGrad(base, r, o.TitleColorA, o.TitleColorB))
 	}
 	crush = b.String()
 
@@ -117,7 +117,7 @@ func Render(s *styles.Styles, version string, compact bool, o Opts) string {
 // smaller windows or sidebar usage.
 func SmallRender(t *styles.Styles, width int) string {
 	title := t.Base.Foreground(t.Secondary).Render("Charm™")
-	title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad(t, "Crush", t.Secondary, t.Primary))
+	title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad(t.Base, "Crush", t.Secondary, t.Primary))
 	remainingWidth := width - lipgloss.Width(title) - 1 // 1 for the space after "Crush"
 	if remainingWidth > 0 {
 		lines := strings.Repeat("╱", remainingWidth)

internal/ui/styles/grad.go 🔗

@@ -5,7 +5,7 @@ import (
 	"image/color"
 	"strings"
 
-	"github.com/lucasb-eyer/go-colorful"
+	"charm.land/lipgloss/v2"
 	"github.com/rivo/uniseg"
 )
 
@@ -13,12 +13,12 @@ import (
 // rendered with a horizontal gradient foreground from color1 to color2. Each
 // string in the returned slice corresponds to a grapheme cluster in the input
 // string. If bold is true, the rendered strings will be bolded.
-func ForegroundGrad(t *Styles, input string, bold bool, color1, color2 color.Color) []string {
+func ForegroundGrad(base lipgloss.Style, input string, bold bool, color1, color2 color.Color) []string {
 	if input == "" {
 		return []string{""}
 	}
 	if len(input) == 1 {
-		style := t.Base.Foreground(color1)
+		style := base.Foreground(color1)
 		if bold {
 			style.Bold(true)
 		}
@@ -30,9 +30,9 @@ func ForegroundGrad(t *Styles, input string, bold bool, color1, color2 color.Col
 		clusters = append(clusters, string(gr.Runes()))
 	}
 
-	ramp := blendColors(len(clusters), color1, color2)
+	ramp := lipgloss.Blend1D(len(clusters), color1, color2)
 	for i, c := range ramp {
-		style := t.Base.Foreground(c)
+		style := base.Foreground(c)
 		if bold {
 			style.Bold(true)
 		}
@@ -43,12 +43,12 @@ func ForegroundGrad(t *Styles, input string, bold bool, color1, color2 color.Col
 
 // ApplyForegroundGrad renders a given string with a horizontal gradient
 // foreground.
-func ApplyForegroundGrad(t *Styles, input string, color1, color2 color.Color) string {
+func ApplyForegroundGrad(base lipgloss.Style, input string, color1, color2 color.Color) string {
 	if input == "" {
 		return ""
 	}
 	var o strings.Builder
-	clusters := ForegroundGrad(t, input, false, color1, color2)
+	clusters := ForegroundGrad(base, input, false, color1, color2)
 	for _, c := range clusters {
 		fmt.Fprint(&o, c)
 	}
@@ -57,61 +57,14 @@ func ApplyForegroundGrad(t *Styles, input string, color1, color2 color.Color) st
 
 // ApplyBoldForegroundGrad renders a given string with a horizontal gradient
 // foreground.
-func ApplyBoldForegroundGrad(t *Styles, input string, color1, color2 color.Color) string {
+func ApplyBoldForegroundGrad(base lipgloss.Style, input string, color1, color2 color.Color) string {
 	if input == "" {
 		return ""
 	}
 	var o strings.Builder
-	clusters := ForegroundGrad(t, input, true, color1, color2)
+	clusters := ForegroundGrad(base, input, true, color1, color2)
 	for _, c := range clusters {
 		fmt.Fprint(&o, c)
 	}
 	return o.String()
 }
-
-// blendColors returns a slice of colors blended between the given keys.
-// Blending is done in Hcl to stay in gamut.
-func blendColors(size int, stops ...color.Color) []color.Color {
-	if len(stops) < 2 {
-		return nil
-	}
-
-	stopsPrime := make([]colorful.Color, len(stops))
-	for i, k := range stops {
-		stopsPrime[i], _ = colorful.MakeColor(k)
-	}
-
-	numSegments := len(stopsPrime) - 1
-	blended := make([]color.Color, 0, size)
-
-	// Calculate how many colors each segment should have.
-	segmentSizes := make([]int, numSegments)
-	baseSize := size / numSegments
-	remainder := size % numSegments
-
-	// Distribute the remainder across segments.
-	for i := range numSegments {
-		segmentSizes[i] = baseSize
-		if i < remainder {
-			segmentSizes[i]++
-		}
-	}
-
-	// Generate colors for each segment.
-	for i := range numSegments {
-		c1 := stopsPrime[i]
-		c2 := stopsPrime[i+1]
-		segmentSize := segmentSizes[i]
-
-		for j := range segmentSize {
-			var t float64
-			if segmentSize > 1 {
-				t = float64(j) / float64(segmentSize-1)
-			}
-			c := c1.BlendHcl(c2, t)
-			blended = append(blended, c)
-		}
-	}
-
-	return blended
-}