-
-
Notifications
You must be signed in to change notification settings - Fork 26
/
generator.go
172 lines (142 loc) · 4.27 KB
/
generator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package gamut
import (
"image/color"
"math"
colorful "github.com/lucasb-eyer/go-colorful"
"github.com/muesli/clusters"
"github.com/muesli/kmeans"
)
// A ColorGenerator checks whether a point in the three dimensional CIELab space
// is suitable for color generation.
type ColorGenerator interface {
Valid(col colorful.Color) bool
Granularity() (l, c float64)
}
// BroadGranularity is used for wider color spaces, e.g. by the PastelGenerator
type BroadGranularity struct {
}
// FineGranularity is used for tighter color spaces, e.g. by the SimilarHueGenerator
type FineGranularity struct {
}
// SimilarHueGenerator produces colors with a similar hue as the given color
type SimilarHueGenerator struct {
FineGranularity
Color color.Color
}
// WarmGenerator produces "warm" colors
type WarmGenerator struct {
BroadGranularity
}
// HappyGenerator produces "happy" colors
type HappyGenerator struct {
BroadGranularity
}
// PastelGenerator produces "pastel" colors
type PastelGenerator struct {
BroadGranularity
}
// Granularity returns BroadGranularity's default values
func (g BroadGranularity) Granularity() (l, c float64) {
return 0.05, 0.1
}
// Granularity returns FineGranularity's default values
func (g FineGranularity) Granularity() (l, c float64) {
return 0.01, 0.01
}
// distanceDegrees returns the distance between two angles on a circle
// e.g. the distance between 5 degrees and 355 degress is 10, not 350
func distanceDegrees(a1, a2 float64) float64 {
mod := math.Mod(math.Abs(a1-a2), 360.0)
if mod > 180.0 {
return 360.0 - mod
}
return mod
}
// Valid returns true if the given color has a similar hue as the original color
func (gen SimilarHueGenerator) Valid(col colorful.Color) bool {
cf, _ := colorful.MakeColor(gen.Color)
h, c, l := cf.Hcl()
hc, cc, lc := col.Hcl()
if cc < c-0.35 || cc > c+0.35 {
return false
}
if lc < l-0.6 || lc > l+0.6 {
return false
}
if distanceDegrees(h, hc) > 7 {
return false
}
if cf.DistanceCIE94(col) > 0.2 {
return false
}
return true
}
// Valid returns true if the color is considered a "warm" color
func (cc WarmGenerator) Valid(col colorful.Color) bool {
_, c, l := col.Hcl()
return 0.1 <= c && c <= 0.4 && 0.2 <= l && l <= 0.5
}
// Valid returns true if the color is considered a "happy" color
func (cc HappyGenerator) Valid(col colorful.Color) bool {
_, c, l := col.Hcl()
return 0.3 <= c && 0.4 <= l && l <= 0.8
}
// Valid returns true if the color is considered a "pastel" color
func (cc PastelGenerator) Valid(col colorful.Color) bool {
_, s, v := col.Hsv()
return 0.2 <= s && s <= 0.4 && 0.7 <= v && v <= 1.0
}
// ColorObservation is a wrapper around colorful.Color, implementing the
// clusters.Observation interface
type ColorObservation struct {
colorful.Color
}
// Coordinates returns the data points of a Lab color value
func (c ColorObservation) Coordinates() clusters.Coordinates {
l, a, b := c.Lab()
return clusters.Coordinates{l, a, b}
}
// Distance calculates the distance between two ColorObservations in the Lab
// color space
func (c ColorObservation) Distance(pos clusters.Coordinates) float64 {
c2 := colorful.Lab(pos[0], pos[1], pos[2])
return c.DistanceLab(c2)
}
// Generate returns a slice with the requested amount of colors, generated by
// the provided ColorGenerator.
func Generate(count int, generator ColorGenerator) ([]color.Color, error) {
// Create data points in the CIE L*a*b color space
// l for lightness channel
// a, b for color channels
var cc []color.Color
dl, dab := generator.Granularity()
var d clusters.Observations
for l := 0.0; l <= 1.0; l += dl {
for a := -1.0; a < 1.0; a += dab {
for b := -1.0; b < 1.0; b += dab {
col := colorful.Lab(l, a, b)
// col = colorful.Hcl(a*360.0, b, c)
if !col.IsValid() || !generator.Valid(col) {
continue
}
d = append(d, ColorObservation{col})
}
}
}
// Enable graph generation (.png files) for each iteration
// km, _ := kmeans.NewWithOptions(0.02, Plotter{})
km, err := kmeans.NewWithOptions(0.02, nil)
if err != nil {
return cc, err
}
// Partition the color space into multiple clusters
clusters, err := km.Partition(d, count)
if err != nil {
return cc, err
}
for _, c := range clusters {
col := colorful.Lab(c.Center[0], c.Center[1], c.Center[2]).Clamped()
cc = append(cc, col)
}
return cc, nil
}