diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46b8fc5..553dffa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/color.go b/color.go new file mode 100644 index 0000000..aa1da4b --- /dev/null +++ b/color.go @@ -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 +} diff --git a/color_test.go b/color_test.go new file mode 100644 index 0000000..c79c3eb --- /dev/null +++ b/color_test.go @@ -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) + }) + } +} diff --git a/demo.go b/demo.go index 307e38c..20f4818 100644 --- a/demo.go +++ b/demo.go @@ -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() { @@ -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{ @@ -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) }, } diff --git a/go.mod b/go.mod index 9928a7a..6ad1ee8 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index fcd8203..0fd14f6 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/gosexy/to v0.0.0-20141221203644-c20e083e3123/go.mod h1:oQuuq9ZkoRpy+2 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= diff --git a/main_test.go b/main_test.go index a154364..80dd42e 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,13 @@ package main import ( + "bytes" + "io" + "log" + "os" "testing" + + "k8s.io/klog" ) // workaround for some weird go 1.13 testing thing with flags @@ -16,3 +22,43 @@ func init() { minBrightness = 30 maxBrightness = 200 } + +const chunkSize = 64000 + +func deepCompare(file1, file2 string) bool { + // Check file size ... + + f1, err := os.Open(file1) + if err != nil { + klog.Fatal(err) + } + defer f1.Close() + + f2, err := os.Open(file2) + if err != nil { + klog.Fatal(err) + } + defer f2.Close() + + for { + b1 := make([]byte, chunkSize) + _, err1 := f1.Read(b1) + + b2 := make([]byte, chunkSize) + _, err2 := f2.Read(b2) + + if err1 != nil || err2 != nil { + if err1 == io.EOF && err2 == io.EOF { + return true + } else if err1 == io.EOF || err2 == io.EOF { + return false + } else { + log.Fatal(err1, err2) + } + } + + if !bytes.Equal(b1, b2) { + return false + } + } +} diff --git a/neopixel.go b/neopixel.go index 878f5a8..b72e2f6 100644 --- a/neopixel.go +++ b/neopixel.go @@ -16,10 +16,9 @@ var colors = map[string]uint32{ "teal": uint32(0x33ffd1), "pink": uint32(0xff08c7), "white": uint32(0xffffff), + "black": uint32(0x000000), // This basically equates to off. } -const off = uint32(0x000000) - type wsEngine interface { Init() error Render() error @@ -74,6 +73,7 @@ func (led *LEDArray) display(color uint32, delay int, brightness int) error { } for i := 0; i < len(led.ws.Leds(0)); i++ { led.ws.Leds(0)[i] = color + led.color = color klog.V(10).Infof("setting led %d", i) if err := led.ws.Render(); err != nil { klog.Error(err) diff --git a/testdata/1024x1024-gradient-1.png b/testdata/1024x1024-gradient-1.png new file mode 100644 index 0000000..38aa3cb Binary files /dev/null and b/testdata/1024x1024-gradient-1.png differ diff --git a/testdata/1024x40-gradient-1.png b/testdata/1024x40-gradient-1.png new file mode 100644 index 0000000..9cf69f4 Binary files /dev/null and b/testdata/1024x40-gradient-1.png differ diff --git a/testdata/2048x40-gradient-2.png b/testdata/2048x40-gradient-2.png new file mode 100644 index 0000000..239479e Binary files /dev/null and b/testdata/2048x40-gradient-2.png differ