Skip to content

Commit

Permalink
Adding color library. Added a gradient to demo. Testing for color usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Suderman committed Jan 20, 2020
1 parent a8c99d6 commit 9ecd7b5
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ repos:
rev: 1.11.0
hooks:
- id: forbid-binary
exclude: >
(?x)^(
testdata/.+\.png
)$
- id: shellcheck
- id: git-check
- repo: https://github.com/dnephin/pre-commit-golang.git
Expand Down
92 changes: 92 additions & 0 deletions color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"image"
"image/draw"
"image/png"
"os"
"strconv"
"strings"

"github.com/lucasb-eyer/go-colorful"
"k8s.io/klog"
)

// GradientTable contains the "keypoints" of the colorgradient you want to generate.
// The position of each keypoint has to live in the range [0,1]
type GradientTable []struct {
Col colorful.Color
Pos float64
}

// GetInterpolatedColor is the meat of the gradient computation. It returns
// an HCL-blend between the two colors around `t`.
// Note: It relies heavily on the fact that the gradient keypoints are sorted.
func (gt GradientTable) GetInterpolatedColor(t float64) colorful.Color {
for i := 0; i < len(gt)-1; i++ {
c1 := gt[i]
c2 := gt[i+1]
if c1.Pos <= t && t <= c2.Pos {
// We are in between c1 and c2. Go blend them!
t := (t - c1.Pos) / (c2.Pos - c1.Pos)
return c1.Col.BlendHcl(c2.Col, t).Clamped()
}
}

// Nothing found? Means we're at (or past) the last gradient keypoint.
return gt[len(gt)-1].Col
}

// HexToColor converts a hex string to a Color
func HexToColor(s string) colorful.Color {
c, err := colorful.Hex(s)
if err != nil {
klog.Errorf("error converting hex string to color: %v", err)
return colorful.Color{}
}
return c
}

// ColorToUint32 converts a color object to a uint32
// for use by the neopixel
func ColorToUint32(color colorful.Color) uint32 {
hex := color.Hex()
hex = strings.Replace(hex, "#", "", -1)
klog.V(10).Infof("hex value: %s", hex)
value, _ := strconv.ParseUint(hex, 16, 32)

return uint32(value)
}

// GradientPNG generates a gradient PNG as an example
func GradientPNG(gradient GradientTable, h int, w int) {
img := image.NewRGBA(image.Rect(0, 0, w, h))

colorList := GradientColorList(gradient, h)
for vert, color := range colorList {
draw.Draw(img, image.Rect(0, vert, w, vert+1), &image.Uniform{color}, image.Point{}, draw.Src)
}

outpng, err := os.Create("gradient.png")
if err != nil {
klog.Error("Error storing png: " + err.Error())
}
defer outpng.Close()

err = png.Encode(outpng, img)
if err != nil {
klog.Error(err)
}
}

//GradientColorList generates a list of colors for a GradientTable
// length: the number of colors you want
func GradientColorList(gradient GradientTable, length int) []colorful.Color {
var list []colorful.Color
for j := 0; j < length; j++ {
c := gradient.GetInterpolatedColor(float64(j) / float64(length))
klog.V(10).Infof("color: %v", c)
list = append(list, c)
}
return list
}
150 changes: 150 additions & 0 deletions color_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package main

import (
"fmt"
"os"
"testing"

"github.com/lucasb-eyer/go-colorful"
"github.com/stretchr/testify/assert"
)

var testGradient1 = GradientTable{
{HexToColor("#9e0142"), 0.0},
{HexToColor("#d53e4f"), 0.1},
{HexToColor("#f46d43"), 0.2},
{HexToColor("#fdae61"), 0.3},
{HexToColor("#fee090"), 0.4},
{HexToColor("#ffffbf"), 0.5},
{HexToColor("#e6f598"), 0.6},
{HexToColor("#abdda4"), 0.7},
{HexToColor("#66c2a5"), 0.8},
{HexToColor("#3288bd"), 0.9},
{HexToColor("#5e4fa2"), 1.0},
}

var testGradient2 = GradientTable{
{HexToColor("#4e3cec"), 0.0},
{HexToColor("#5b3ee8"), 0.1},
{HexToColor("#6941e5"), 0.2},
{HexToColor("#7643e1"), 0.3},
{HexToColor("#8346dd"), 0.4},
{HexToColor("#9148da"), 0.5},
{HexToColor("#9e4bd6"), 0.6},
{HexToColor("#ab4dd2"), 0.7},
{HexToColor("#b94fcf"), 0.8},
{HexToColor("#c652cb"), 0.9},
{HexToColor("#e157c4"), 1.0},
}

func TestParseHex(t *testing.T) {
tests := []struct {
name string
hex string
want colorful.Color
}{
{"blue", "#0000ff", colorful.Color{R: 0, G: 0, B: 1}},
{"yellow", "#ffff00", colorful.Color{R: 1, G: 1, B: 0}},
{"red", "#ff0000", colorful.Color{R: 1, G: 0, B: 0}},
{"black", "#000000", colorful.Color{R: 0, G: 0, B: 0}},
{"green", "#00ff00", colorful.Color{R: 0, G: 1, B: 0}},
{"white", "#ffffff", colorful.Color{R: 1, G: 1, B: 1}},
{"notacolor", "ff", colorful.Color{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := HexToColor(tt.hex)
assert.Equal(t, tt.want, got)
})
}
}

func TestColorToUint32(t *testing.T) {
tests := []struct {
name string
color colorful.Color
want uint32
}{
{"white", colorful.Color{R: 1, G: 1, B: 1}, uint32(16777215)},
{"black", colorful.Color{R: 0, G: 0, B: 0}, uint32(0)},
{"green", colorful.Color{R: 0, G: 1, B: 0}, uint32(65280)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ColorToUint32(tt.color)
assert.Equal(t, tt.want, got)
})
}
}

func TestGradientTable_GetInterpolatedColor(t *testing.T) {
tests := []struct {
name string
value float64
want colorful.Color
}{
{"one", 1.0, colorful.Color{R: 0.3686274518393889, G: 0.30980394385954535, B: 0.635294122225692}},
{"two", 1.1, colorful.Color{R: 0.3686274509803922, G: 0.30980392156862746, B: 0.6352941176470588}},
{"three", 1.2, colorful.Color{R: 0.3686274509803922, G: 0.30980392156862746, B: 0.6352941176470588}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := testGradient1.GetInterpolatedColor(tt.value)
assert.Equal(t, tt.want, got)
})
}
}

func TestGradientPNG(t *testing.T) {
tests := []struct {
name string
gradient GradientTable
h int
w int
testFile string
}{
{"1024x40 1", testGradient1, 1024, 40, "1024x40-gradient-1.png"},
{"1024x1024 1", testGradient1, 1024, 1024, "1024x1024-gradient-1.png"},
{"2048x40 2", testGradient2, 2048, 40, "2048x40-gradient-2.png"},
}
for _, tt := range tests {
os.Remove("gradient.png")
t.Run(tt.name, func(t *testing.T) {
GradientPNG(tt.gradient, tt.h, tt.w)
assert.FileExistsf(t, "gradient.png", "gradient.png should exist")
match := deepCompare("gradient.png", "testdata/"+tt.testFile)
assert.Truef(t, match, "the files must match")
})
os.Remove("gradient.png")
}
}

func TestGradientColorList(t *testing.T) {
tests := []struct {
name string
gradient GradientTable
length int
want []colorful.Color
}{
{"one", testGradient1, 1, []colorful.Color{{R: 0.6196077933795217, G: 0.003922138953572327, B: 0.2588235191354816}}},
{
"two",
testGradient2,
5,
[]colorful.Color{
{R: 0.3058824116295, G: 0.23529411042695136, B: 0.9254902064503917},
{R: 0.4117647268595018, G: 0.2549019781745598, B: 0.8980392249706234},
{R: 0.5137254867393809, G: 0.2745098482413161, B: 0.866666674535384},
{R: 0.6196078211495166, G: 0.29411772176478884, B: 0.8392156924085546},
{R: 0.72549015896929, G: 0.3098040307126474, B: 0.8117647099046337},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GradientColorList(tt.gradient, tt.length)
fmt.Println(got)
assert.Equal(t, tt.want, got)
})
}
}
41 changes: 35 additions & 6 deletions demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package main
import (
"github.com/spf13/cobra"
"k8s.io/klog"
"time"
)

var (
demoBrightness int
demoDelay int
demoCount int
demoBrightness int
demoDelay int
demoCount int
demoGradientLength int
)

func init() {
Expand All @@ -17,6 +19,21 @@ func init() {
demoCmd.Flags().IntVar(&demoDelay, "delay", 100, "The delay in ms of the demo program.")
demoCmd.Flags().IntVar(&demoCount, "count", 1, "The number of loops to run the demo.")
demoCmd.Flags().IntVar(&demoBrightness, "brightness", 150, "The brightness to run the demo at. Must be between min and max.")
demoCmd.Flags().IntVar(&demoGradientLength, "gradient-count", 2048, "The number of steps in the gradient.")
}

var demoGradient = GradientTable{
{HexToColor("#9e0142"), 0.0},
{HexToColor("#d53e4f"), 0.1},
{HexToColor("#f46d43"), 0.2},
{HexToColor("#fdae61"), 0.3},
{HexToColor("#fee090"), 0.4},
{HexToColor("#ffffbf"), 0.5},
{HexToColor("#e6f598"), 0.6},
{HexToColor("#abdda4"), 0.7},
{HexToColor("#66c2a5"), 0.8},
{HexToColor("#3288bd"), 0.9},
{HexToColor("#5e4fa2"), 1.0},
}

var demoCmd = &cobra.Command{
Expand All @@ -25,19 +42,31 @@ var demoCmd = &cobra.Command{
Long: `Runs a demo.`,
Run: func(cmd *cobra.Command, args []string) {

// Initialize the LEDs
led, err := newLEDArray()
if err != nil {
klog.Fatal(err)
}
defer led.ws.Fini()

for i := 1; i < (demoCount + 1); i++ {
// Loops through our list of pre-defined colors and display them in order.
for i := 0; i < (demoCount); i++ {
for colorName, color := range colors {
klog.Infof("displaying: %s", colorName)
_ = led.display(color, demoDelay, 150)
_ = led.display(color, demoDelay, demoBrightness)
}
_ = led.fade(led.color, minBrightness)
time.Sleep(500 * time.Millisecond)

// Second part of demo - go through a color gradient really fast.
klog.V(3).Infof("starting color gradient")
colorList := GradientColorList(demoGradient, demoGradientLength)
for _, gradColor := range colorList {
_ = led.display(ColorToUint32(gradColor), 0, demoBrightness)
time.Sleep(time.Duration(demoDelay) * time.Nanosecond)
}
}

_ = led.display(off, 0, 0)
_ = led.fade(led.color, minBrightness)
},
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.13

require (
github.com/brutella/hc v1.2.0
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/rpi-ws281x/rpi-ws281x-go v1.0.5
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum