feat: provide a color gradient generator

This adds a new dependency, `colorful`.
Hope it's worth it!
All our projects that use this lib need gradients as well.
We could perhaps embark our own HSL-space interpolator, but…  I gotta go.
main v0.3.0
Dominique Merle 2 years ago
parent a5e3ef1a98
commit cb99a2290d

@ -2,4 +2,7 @@ module github.com/mieuxvoter/majority-judgment-library-go
go 1.12
require github.com/stretchr/testify v1.7.0
require (
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/stretchr/testify v1.7.0
)

@ -1,5 +1,7 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

@ -0,0 +1,212 @@
package judgment
// Some code below is taken directly from colorful's doc examples.
// https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go
// May be useful later:
// c, err := colorful.Hex(s)
import (
"errors"
"fmt"
"github.com/lucasb-eyer/go-colorful"
"image/color"
)
// CreateDefaultPalette returns a Palette of amountOfColors colors.
// 7 colors we use, red to green:
// "#df3222", "#ed6f01", "#fab001", "#c5d300", "#7bbd3e", "#00a249", "#017a36"
// When requiring more than 7, we interpolate in HSV space.
// This tries to be fault-tolerant, and returns an empty palette upon trouble.
func CreateDefaultPalette(amountOfColors int) color.Palette {
const Color0 = 0xdf3222
const Color1 = 0xed6f01
const Color2 = 0xfab001
const Color3 = 0xc5d300
const Color4 = 0x7bbd3e
const Color5 = 0x00a249
const Color6 = 0x017a36
color0 := hexToRGB(Color0)
color1 := hexToRGB(Color1)
color2 := hexToRGB(Color2)
color3 := hexToRGB(Color3)
color4 := hexToRGB(Color4)
color5 := hexToRGB(Color5)
color6 := hexToRGB(Color6)
if amountOfColors < 0 {
amountOfColors = amountOfColors * -1
}
switch amountOfColors {
case 0:
return []color.Color{}
case 1:
return []color.Color{
color5,
}
case 2:
return []color.Color{
color0,
color5,
}
case 3:
return []color.Color{
color0,
color2,
color5,
}
case 4:
return []color.Color{
color0,
color2,
color4,
color6,
}
case 5:
return []color.Color{
color0,
color1,
color2,
color4,
color5,
}
case 6:
return []color.Color{
color0,
color1,
color2,
color4,
color5,
color6,
}
case 7:
return []color.Color{
color0,
color1,
color2,
color3,
color4,
color5,
color6,
}
default:
palette, err := bakePalette(amountOfColors, []color.Color{
color0,
color1,
color2,
color3,
color4,
color5,
color6,
})
if err != nil {
//panic("CreateDefaultPalette: failed to bake: "+err.Error())
return []color.Color{}
}
return palette
}
}
// DumpPaletteHexString dumps the provided palette as a string
// Looks like: "#df3222", "#ed6f01", "#fab001", "#c5d300", "#7bbd3e", "#00a249", "#017a36"
func DumpPaletteHexString(palette color.Palette, separator string, quote string) string {
out := ""
for colorIndex, colorRgba := range palette {
if colorIndex > 0 {
out += separator
}
out += quote
out += DumpColorHexString(colorRgba, "#", false)
out += quote
}
return out
}
// DumpColorHexString outputs strings like #ff3399 or #ff3399ff with alpha
// Be mindful that PRECISION IS LOST because hex format has less bits
func DumpColorHexString(c color.Color, prefix string, withAlpha bool) string {
out := prefix
r, g, b, a := c.RGBA()
out += fmt.Sprintf("%02x", r>>8)
out += fmt.Sprintf("%02x", g>>8)
out += fmt.Sprintf("%02x", b>>8)
if withAlpha {
out += fmt.Sprintf("%02x", a>>8)
}
return out
}
// there probably is a colorful way to do this
func hexToRGB(hexColor int) color.Color {
rgba := color.RGBA{
R: uint8((hexColor & 0xff0000) >> 16),
G: uint8((hexColor & 0x00ff00) >> 8),
B: uint8((hexColor & 0x0000ff) >> 0),
A: 0xff,
}
c, success := colorful.MakeColor(rgba)
if !success {
panic("hexToRgb")
}
return c
}
// This table contains the "key" colors of the color gradient we want to generate.
// The position of each key has to live in the range [0,1]
type keyColor struct {
Color colorful.Color
Position float64
}
type gradientTable []keyColor
// This is the meat of the gradient computation. It returns a HCL-blend between
// the two colors around `t`.
// Note: It relies heavily on the fact that the gradient keypoints are sorted.
func (gt gradientTable) getInterpolatedColorFor(t float64) colorful.Color {
for i := 0; i < len(gt)-1; i++ {
c1 := gt[i]
c2 := gt[i+1]
if c1.Position <= t && t <= c2.Position {
// We are in between c1 and c2. Go blend them!
t := (t - c1.Position) / (c2.Position - c1.Position)
return c1.Color.BlendHcl(c2.Color, t).Clamped()
}
}
// Nothing found? Means we're at (or past) the last gradient keypoint.
return gt[len(gt)-1].Color
}
func bakePalette(toLength int, keyColors color.Palette) (color.Palette, error) {
if toLength < 2 {
return nil, errors.New("bakePalette: the length of the palette must be > 1")
}
keyPoints := gradientTable{}
paletteLen := len(keyColors)
for colorIndex, colorObject := range keyColors {
colorfulColor, success := colorful.MakeColor(colorObject)
if !success {
panic("Bad palette color: alpha channel is probably 0.")
}
keyPoints = append(keyPoints, keyColor{
Color: colorfulColor,
Position: float64(colorIndex) / (float64(paletteLen) - 1.0),
})
}
outPalette := make([]color.Color, 0, 7)
for i := 0; i < toLength; i++ {
c := keyPoints.getInterpolatedColorFor(float64(i) / (float64(toLength) - 1))
outPalette = append(outPalette, c)
}
return outPalette, nil
}

@ -0,0 +1,133 @@
package judgment
import (
"github.com/lucasb-eyer/go-colorful"
"github.com/stretchr/testify/assert"
"image/color"
"testing"
)
func hex(s string) color.Color {
c, err := colorful.Hex(s)
if err != nil {
panic("hex: " + err.Error())
}
return c
}
//func TestBakePalette(t *testing.T) {
// type args struct {
// toLength int
// keyColors color.Palette
// }
// tests := []struct {
// name string
// args args
// want color.Palette
// wantErr bool
// }{
// // TODO: Add test cases.
// }
// for _tt := range tests {
// t.Run(tt.namefunc(t *testing.T) {
// goterr := bakePalette(tt.args.toLengthtt.args.keyColors)
// if (err != nil) != tt.wantErr {
// t.Errorf("bakePalette() error = %vwantErr %v"errtt.wantErr)
// return
// }
// if !reflect.DeepEqual(gottt.want) {
// t.Errorf("bakePalette() got = %vwant %v"gottt.want)
// }
// })
// }
//}
func TestCreatePalette(t *testing.T) {
type args struct {
amountOfColors int
}
tests := []struct {
name string
args args
want color.Palette
}{
{
name: "Palette of 2",
args: args{
amountOfColors: 2,
},
want: []color.Color{
hex("#df3222"),
hex("#00a249"),
},
},
{
name: "Palette of 7",
args: args{
amountOfColors: 7,
},
},
{
name: "Palette of 32",
args: args{
amountOfColors: 32,
},
want: []color.Color{
hex("#df3222"),
hex("#e3401d"),
hex("#e64c18"),
hex("#e95712"),
hex("#eb630b"),
hex("#ed6d02"),
hex("#f07a00"),
hex("#f38700"),
hex("#f69300"),
hex("#f8a000"),
hex("#faac00"),
hex("#f5b500"),
hex("#ecbc00"),
hex("#e2c300"),
hex("#d7ca00"),
hex("#cbd000"),
hex("#bdd20c"),
hex("#aece1d"),
hex("#a0ca28"),
hex("#92c531"),
hex("#84c039"),
hex("#76bc3e"),
hex("#65b740"),
hex("#54b142"),
hex("#40ac44"),
hex("#28a747"),
hex("#00a148"),
hex("#009944"),
hex("#009141"),
hex("#00893d"),
hex("#008239"),
hex("#017a36"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := CreateDefaultPalette(tt.args.amountOfColors)
print(DumpPaletteHexString(actual, ", ", "\"") + "\n")
if nil == tt.want {
return
}
for i, expectedColor := range tt.want {
// our test values are not as precise as colorful's colors
//assert.Equal(t, expectedColor, actual[i])
// so we use equalish comparisons
p := 300.0
er, eg, eb, ea := expectedColor.RGBA()
ar, ag, ab, aa := actual[i].RGBA()
assert.InDelta(t, er, ar, p)
assert.InDelta(t, eg, ag, p)
assert.InDelta(t, eb, ab, p)
assert.InDelta(t, ea, aa, p)
}
//assert.Equal(t, true, true)
})
}
}
Loading…
Cancel
Save