diff --git a/go.mod b/go.mod index e50eb14..17c330f 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum index acb88a4..133ad64 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/judgment/colors.go b/judgment/colors.go new file mode 100644 index 0000000..7481c46 --- /dev/null +++ b/judgment/colors.go @@ -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 +} diff --git a/judgment/colors_test.go b/judgment/colors_test.go new file mode 100644 index 0000000..53e50a5 --- /dev/null +++ b/judgment/colors_test.go @@ -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) + }) + } +}