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
parent
a5e3ef1a98
commit
cb99a2290d
@ -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…
Reference in new issue