commit 3e9725f4adf29e53a5c7e9bf4c1919dc06bfaa42
parent fe14b9ea6e4ca110a0f79adab9d248f20d377e0c
Author: Brian C. Lane <bcl@brianlane.com>
Date: Sun, 4 Dec 2022 11:52:16 -0800
Add bezier gradient color function
This allows for multiple control points with a smooth transition between
colors.
Diffstat:
M | main.go | | | 73 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
M | main_test.go | | | 24 | ++++++++++++++++++++++++ |
2 files changed, 92 insertions(+), 5 deletions(-)
diff --git a/main.go b/main.go
@@ -7,6 +7,7 @@ import (
"flag"
"fmt"
"log"
+ "math"
"math/rand"
"net/http"
"os"
@@ -113,12 +114,15 @@ type Gradient struct {
}
// NewLinearGradient returns a Linear Gradient with pre-computed colors for every age
-func NewLinearGradient(controls []RGBAColor, maxAge int) Gradient {
+// from https://bsouthga.dev/posts/color-gradients-with-python
+//
+// Only uses the first and last color passed in
+func NewLinearGradient(colors []RGBAColor, maxAge int) Gradient {
// Use the first and last color in controls as start and end
- gradient := Gradient{controls: []RGBAColor{controls[0], controls[len(controls)-1]}, points: make([]RGBAColor, maxAge)}
+ gradient := Gradient{controls: []RGBAColor{colors[0], colors[len(colors)-1]}, points: make([]RGBAColor, maxAge)}
- start := controls[0]
- end := controls[1]
+ start := gradient.controls[0]
+ end := gradient.controls[1]
for t := 1; t <= maxAge; t++ {
r := uint8(start.r + uint8((float64(t)/float64(maxAge))*float64(end.r-start.r)))
@@ -129,6 +133,64 @@ func NewLinearGradient(controls []RGBAColor, maxAge int) Gradient {
return gradient
}
+// FactorialCache saves the results for factorial calculations for faster access
+type FactorialCache struct {
+ cache map[int]float64
+}
+
+// NewFactorialCache returns a new empty cache
+func NewFactorialCache() *FactorialCache {
+ return &FactorialCache{cache: make(map[int]float64)}
+}
+
+// Fact calculates the n! and caches the results
+func (fc *FactorialCache) Fact(n int) float64 {
+ f, ok := fc.cache[n]
+ if ok {
+ return f
+ }
+ var result float64
+ if n == 1 || n == 0 {
+ result = 1
+ } else {
+ result = float64(n) * fc.Fact(n-1)
+ }
+
+ fc.cache[n] = result
+ return result
+}
+
+// Bernstein calculates the bernstein coefficient
+//
+// t runs from 0 -> 1 and is the 'position' on the curve (age / maxAge-1)
+// n is the number of control colors -1
+// i is the current control color from 0 -> n
+func (fc *FactorialCache) Bernstein(t float64, n, i int) float64 {
+ b := fc.Fact(n) / (fc.Fact(i) * fc.Fact(n-i))
+ b = b * math.Pow(1-t, float64(n-i)) * math.Pow(t, float64(i))
+ return b
+}
+
+// NewBezierGradient returns pre-computed colors using control colors and bezier curve
+// from https://bsouthga.dev/posts/color-gradients-with-python
+func NewBezierGradient(controls []RGBAColor, maxAge int) Gradient {
+ gradient := Gradient{controls: controls, points: make([]RGBAColor, maxAge)}
+ fc := NewFactorialCache()
+
+ for t := 0; t < maxAge; t++ {
+ color := RGBAColor{}
+ for i, c := range controls {
+ color.r += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.r))
+ color.g += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.g))
+ color.b += uint8(fc.Bernstein(float64(t)/float64(maxAge-1), len(controls)-1, i) * float64(c.b))
+ }
+ color.a = 255
+ gradient.points[t] = color
+ }
+
+ return gradient
+}
+
// Cell describes the location and state of a cell
type Cell struct {
alive bool
@@ -816,7 +878,8 @@ func InitializeGame() *LifeGame {
}
// Build the color gradient
- game.gradient = NewLinearGradient(colors, maxColorAge)
+ // game.gradient = NewLinearGradient(colors, maxColorAge)
+ game.gradient = NewBezierGradient(colors, maxColorAge)
return game
}
diff --git a/main_test.go b/main_test.go
@@ -34,3 +34,27 @@ func TestParseColorTriplets(t *testing.T) {
}
}
}
+
+func TestFactorialCache(t *testing.T) {
+ var matrix = []struct {
+ input int
+ output float64
+ }{
+ {0, 1},
+ {1, 1},
+ {2, 2},
+ {3, 6},
+ {4, 24},
+ {18, 6402373705728000},
+ {19, 121645100408832000},
+ }
+
+ fc := NewFactorialCache()
+
+ for _, tt := range matrix {
+ result := fc.Fact(tt.input)
+ if result != tt.output {
+ t.Errorf("expected %0.0f, got %0.0f", tt.output, result)
+ }
+ }
+}