-
Notifications
You must be signed in to change notification settings - Fork 331
v2: feat(progress): support multiple stops and improved blend algorithm #838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v2-exp
Are you sure you want to change the base?
v2: feat(progress): support multiple stops and improved blend algorithm #838
Conversation
4a0af78
to
f5d9f69
Compare
Migrating from v1 to v2 seems like s good opportunity for this optimization:
I do agree your solution is the most user-friendly but not providing a gradientFunc or FillFunc interface seems like a missed opportunity for unique use cases:
Otherwise the work you did in this PR looks amazing! |
I can implement this as part of this PR, should be quite simple, though would you prefer to bundle that as part of your work (and maybe shift to v2 for your PR, given v2 will be out semi-soon)? Also worth noting that we would likely have to break the API for WithFillCharacters given it doesn't take into account this type of scenario.
I do think there is still room to support a custom blend function for unique usecases (at least, I don't think this PR will conflict with that). Do you happen to have some example code of what you're thinking for such a function? |
done. |
I'm all for this +1. What do you all think @meowgorithm @raphamorim @andreynering @caarlos0 ? |
+1 , I may think WithColors with 0 uses default a bit confusing tbh (I would rather do nothing or have a different behaviour), but no strong opinions. |
My thought was a user could pass in a function with this signature as an // ProgressFillFunc defines a function that returns which color
// to use for a given *current* part of the progress bar.
// totalCompletion: Overall completion of the progress bar (0.0 to 1.0)
// current: Position within the progress bar where the color is needed (0.0 to 1.0)
type ProgressFillFunc func(totalCompletion, current float64) color.Color Then take the following example: Say a user wants to split their progress bar into sections representing 3 package downloads (5Mb, 2Mb, and 3Mb), they could do implement something along those lines: // Example sequential download
func DowloadsProgressFill(totalCompletion, current float64) color.Color {
if total < 0.5 {
return lg.Color("#FF0000")
}
if total < 0.7 {
return lg.Color("#00FF00")
}
return lg.Color("#0000FF")
} Another example would be a stipe effect that animates as if it were a drawbar like so: // CreateStripedLuminanceGradient creates a moving striped progress bar using HSL luminance variations.
// The stripes move as the progress advances, creating an animated effect.
// hue: base hue (0-360), saturation: base saturation (0-1), stripeWidth: width of each stripe (0-1)
func CreateStripedLuminanceGradient(hue, saturation, stripeWidth float64) ProgressFillFunc {
// Clamp parameters to valid ranges
h := math.Mod(hue, 360)
if h < 0 {
h += 360
}
sat := math.Max(0, math.Min(1, saturation))
width := math.Max(0.01, math.Min(1, stripeWidth)) // Minimum stripe width to avoid division by zero
return func(totalCompletion, current float64) color.Color {
// Create moving stripe pattern based on current position and total completion
// The totalCompletion acts as the "x offset" making stripes appear to move
stripePosition := (current - totalCompletion) / width
// Use modulo to create repeating pattern (0 to 1)
stripe := math.Mod(stripePosition, 1.0)
// Create luminance variation: oscillate between 0.2 and 0.8
// Using sine wave for smooth transitions
luminance := 0.5 + 0.3*math.Sin(stripe*2*math.Pi)
// Clamp luminance to valid range
luminance = math.Max(0.1, math.Min(0.9, luminance))
// Create HSL color and return
return colorful.Hsl(h, sat, luminance) // <- would need to be refactored to avoid using legacy colorful but you get the picture ;)
}
} Simple functions as such could give the user visuals like the following: Screen.Recording.2025-09-08.at.18.28.23.movEven the Blend function could implement this function operation in the background if we want to keep the interface uniform for everything. |
+1, this is a great call. |
7c318af
to
f5dd3fb
Compare
Ok, I think it's ready for review again, @meowgorithm @ChausseBenjamin @aymanbagabas @raphamorim. The new changes include:
I did look at including the multi-step-block from 680 (or even just supporting rendering the last cell with a half block for partial percentages), but the experience by default isn't great, as there is unstyled space between the start of the block on the last cell, and the start of rendering the empty block, and seems only to work well when someone overrides the background + doesn't set (or uses the default) empty character. Updated the main description with all of the new gifs, more examples, etc. |
f5dd3fb
to
b76ab88
Compare
@lrstanley This is insanely good!!! 🤯 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code wise looks good, will leave to @meowgorithm check as well
It just occured to me that a similar mechanism to your half-block check could be applied here. Something like this could enable the behaviour: if Model.Empty == ' ' && slices.Contains([]rune{'▌', '█'}, Model.Full){
// Use smooth animation blocks for the intercecting character between full and empty
} I don't mind writing my own PR for it once this one gets merged if you want to avoid scope-creep ;) |
I think the main problem with the more advanced block percentage logic is this:
Given that, I think it will need a lot more care and thorough brainstorming in terms of API and out-of-box behavior that probably should be a separate PR. |
Signed-off-by: Liam Stanley <[email protected]>
b76ab88
to
3b2b23b
Compare
Describe your changes
WithColors(color.Colors...)
which supports 2+ color stops.WithDefaultBlend()
which sets a default blend of colors (replacesWithDefaultGradient
).WithScaled(bool)
toggles scaling blended colors based off filled vs full progress bar.WithDefaultGradient()
WithGradient()
WithDefaultScaledGradient()
WithScaledGradient()
WithSolidFill()
WithColorFunc(ColorFunc)
which allows completely dynamic styling for the progress bar (see below).Dependency updates
lipgloss/v2
to latest v2-exp head charmbracelet/lipgloss@71dd8ee to get access tolipgloss.Blend1D()
.bubbletea/v2
tov2.0.0-beta.4
-- this is what caused many of the table snapshots to need to be updated, asv2.0.0-beta.1
->v2.0.0-beta.4
moved away from the nbsp-style spaces.Related issue/discussion
lipgloss.Blend1D()
method, and also constrain the resulting colors.Examples
Animated with scaled blending
Animated with blending
New half-block implementation which adds better resolution
New (the default):

Old:

For those who still want the old solution, they can override the fill character to the block character, which restores the old logic:
Custom
ColorFunc
: using total percentageCustom
ColorFunc
: using rendered percentageCustom
ColorFunc
: advancedusing the code from @ChausseBenjamin (thanks!).
Checklist before requesting a review
CONTRIBUTING.md