-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: color conversion utilities for (hsv, rgb, and hex)
- Loading branch information
1 parent
a4978c8
commit 5445456
Showing
5 changed files
with
214 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package color | ||
|
||
import ( | ||
"fmt" | ||
"image/color" | ||
"math" | ||
) | ||
|
||
// Color is convenience type that wraps an RGBA color with additional methods | ||
// for converion. | ||
type Color struct { | ||
v color.RGBA | ||
} | ||
|
||
// RGBA returns the RGBA values of the color. | ||
func (c Color) RGBA() (r, g, b, a uint32) { | ||
return c.v.RGBA() | ||
} | ||
|
||
// Hex returns the hex value of the color as a string. | ||
func (c Color) Hex() string { | ||
return ColorToHex(c) | ||
} | ||
|
||
// HSV returns the HSV values of the color. | ||
func (c Color) HSV() (h, s, v float64) { | ||
return ColorToHSV(c) | ||
} | ||
|
||
// FromHSV sets the color from HSV values. | ||
func (c Color) FromHSV(h, s, v float64) { | ||
c.v = HSVToRGBA(h, s, v) | ||
} | ||
|
||
// FromRGB sets the color from RGB values. | ||
func (c Color) FromRGB(r, g, b uint8) { | ||
c.v.R = r | ||
c.v.G = g | ||
c.v.B = b | ||
c.v.A = 255 | ||
} | ||
|
||
// ColorToHex converts a color to a hex string. | ||
func ColorToHex(c color.Color) string { | ||
r, g, b, _ := c.RGBA() | ||
return fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8) | ||
} | ||
|
||
// HSVToRGBA converts HSV values to an RGBA color. HSV values should be in the | ||
// ranges [0, 360], [0, 1], and [0, 1] respectively. | ||
func HSVToRGBA(h, s, v float64) color.RGBA { | ||
h = math.Mod(h, 360) // Ensure h is in the range [0, 360] | ||
s = math.Max(0, math.Min(1, s)) // Clamp s to [0, 1] | ||
v = math.Max(0, math.Min(1, v)) // Clamp v to [0, 1] | ||
|
||
c := v * s | ||
x := c * (1 - math.Abs(math.Mod(h/60, 2)-1)) | ||
m := v - c | ||
|
||
var r, g, b float64 | ||
|
||
switch { | ||
case h < 60: | ||
r, g, b = c, x, 0 | ||
case h < 120: | ||
r, g, b = x, c, 0 | ||
case h < 180: | ||
r, g, b = 0, c, x | ||
case h < 240: | ||
r, g, b = 0, x, c | ||
case h < 300: | ||
r, g, b = x, 0, c | ||
default: | ||
r, g, b = c, 0, x | ||
} | ||
|
||
r = math.Round((r + m) * 255) | ||
g = math.Round((g + m) * 255) | ||
b = math.Round((b + m) * 255) | ||
|
||
return color.RGBA{ | ||
R: uint8(r), | ||
G: uint8(g), | ||
B: uint8(b), | ||
A: 255, // Full opacity | ||
} | ||
} | ||
|
||
// ColorToHSV converts a color to HSV. | ||
// | ||
// A few things to note: | ||
// | ||
// - Due to rounding errors, we mayget a slightly different color when | ||
// converting back to RGB. | ||
// - The hue will be rounded to the nearest degree, while the saturation and | ||
// value will be rounded to two decimal places. | ||
func ColorToHSV(c color.Color) (h, s, v float64) { | ||
r, g, b, _ := c.RGBA() | ||
|
||
// Convert from uint32 (0-MaxUint32) to float64 (0-1). | ||
rf := float64(r) / float64(0xFFFF) | ||
gf := float64(g) / float64(0xFFFF) | ||
bf := float64(b) / float64(0xFFFF) | ||
|
||
minimum := math.Min(rf, math.Min(gf, bf)) | ||
maximum := math.Max(rf, math.Max(gf, bf)) | ||
|
||
v = maximum | ||
delta := maximum - minimum | ||
|
||
if maximum == 0 { | ||
// Black. | ||
s = 0 | ||
h = 0 | ||
return | ||
} | ||
|
||
s = delta / maximum | ||
|
||
if delta == 0 { | ||
// Gray. | ||
h = 0 | ||
return | ||
} | ||
|
||
switch maximum { | ||
case rf: | ||
h = (gf - bf) / delta | ||
if gf < bf { | ||
h += 6 | ||
} | ||
case gf: | ||
h = 2 + (bf-rf)/delta | ||
case bf: | ||
h = 4 + (rf-gf)/delta | ||
} | ||
|
||
h *= 60 | ||
|
||
h = math.Round(h) // Round to nearest degree. | ||
s = math.Round(s*100) / 100 // Round to two decimal places. | ||
v = math.Round(v*100) / 100 // Round to two decimal places. | ||
|
||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package color | ||
|
||
import ( | ||
"image/color" | ||
"testing" | ||
) | ||
|
||
func TestColorToHex(t *testing.T) { | ||
for i, test := range []struct { | ||
c color.Color | ||
want string | ||
}{ | ||
{color.RGBA{0, 0, 0, 255}, "#000000"}, | ||
{color.RGBA{255, 255, 255, 255}, "#FFFFFF"}, | ||
{color.RGBA{255, 0, 0, 255}, "#FF0000"}, | ||
{color.RGBA{0, 255, 0, 255}, "#00FF00"}, | ||
{color.RGBA{0, 0, 255, 255}, "#0000FF"}, | ||
{color.RGBA{107, 80, 255, 255}, "#6B50FF"}, | ||
} { | ||
got := ColorToHex(test.c) | ||
if got != test.want { | ||
t.Errorf("Test %d: ColorToHex(%v)\nGot: %v\nWant: %v", i, test.c, got, test.want) | ||
} | ||
} | ||
} | ||
|
||
func TestHSVToRGBA(t *testing.T) { | ||
for i, test := range []struct { | ||
h, s, v float64 | ||
want color.RGBA | ||
}{ | ||
{0, 0, 0, color.RGBA{0, 0, 0, 255}}, | ||
{0, 0, 1, color.RGBA{255, 255, 255, 255}}, | ||
{0, 1, 1, color.RGBA{255, 0, 0, 255}}, | ||
{120, 1, 1, color.RGBA{0, 255, 0, 255}}, | ||
{240, 1, 1, color.RGBA{0, 0, 255, 255}}, | ||
{249, 0.69, 1, color.RGBA{105, 79, 255, 255}}, | ||
} { | ||
got := HSVToRGBA(test.h, test.s, test.v) | ||
if got != test.want { | ||
t.Errorf("Test %d: HSVToRGBA(%v, %v, %v)\nGot: %v\nWant: %v", i, test.h, test.s, test.v, got, test.want) | ||
} | ||
} | ||
} | ||
|
||
func TestColorToHSV(t *testing.T) { | ||
for i, test := range []struct { | ||
c color.Color | ||
want struct { | ||
h, s, v float64 | ||
} | ||
}{ | ||
{color.RGBA{0, 0, 0, 255}, struct{ h, s, v float64 }{0, 0, 0}}, | ||
{color.RGBA{255, 255, 255, 255}, struct{ h, s, v float64 }{0, 0, 1}}, | ||
{color.RGBA{255, 0, 0, 255}, struct{ h, s, v float64 }{0, 1, 1}}, | ||
{color.RGBA{0, 255, 0, 255}, struct{ h, s, v float64 }{120, 1, 1}}, | ||
{color.RGBA{0, 0, 255, 255}, struct{ h, s, v float64 }{240, 1, 1}}, | ||
{color.RGBA{105, 79, 255, 255}, struct{ h, s, v float64 }{249, 0.69, 1}}, | ||
} { | ||
h, s, v := ColorToHSV(test.c) | ||
if h != test.want.h || s != test.want.s || v != test.want.v { | ||
t.Errorf("Test %d: ColorToHSV(%v)\nGot: %v\nWant: %v", i, test.c, struct{ h, s, v float64 }{h, s, v}, test.want) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/charmbracelet/x/exp/color | ||
|
||
go 1.18 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ use ( | |
./editor | ||
./errors | ||
./examples | ||
./exp/color | ||
./exp/golden | ||
./exp/higherorder | ||
./exp/maps | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters