├── .github
├── FUNDING.yml
└── workflows
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── colorgens.go
├── colorgens_test.go
├── colors.go
├── colors_test.go
├── doc
├── LinearRGB Approximations.ipynb
├── approx-quality.png
├── colorblend
│ ├── clamped.png
│ ├── clamped.xcf
│ ├── colorblend.go
│ ├── colorblend.png
│ ├── colorblend.xcf
│ ├── invalid.png
│ └── invalid.xcf
├── colordist
│ ├── colordist.go
│ ├── colordist.png
│ └── colordist.xcf
├── colorgens
│ ├── colorgens.go
│ └── colorgens.png
├── colorsort
│ ├── colorsort.go
│ └── colorsort.png
├── gradientgen
│ ├── gradientgen.go
│ └── gradientgen.png
└── palettegens
│ ├── palettegens.go
│ └── palettegens.png
├── go.mod
├── happy_palettegen.go
├── hexcolor.go
├── hexcolor_test.go
├── hsluv-snapshot-rev4.json
├── hsluv.go
├── hsluv_test.go
├── rand.go
├── soft_palettegen.go
├── soft_palettegen_test.go
├── sort.go
├── sort_test.go
└── warm_palettegen.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [lucasb-eyer, makew0rld]
4 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - "**.md"
7 | - "**.png"
8 | - "**.ipynb"
9 | - "**.xcf"
10 | - "LICENSE"
11 | pull_request:
12 | paths-ignore:
13 | - "**.md"
14 | - "**.png"
15 | - "**.ipynb"
16 | - "**.xcf"
17 | - "LICENSE"
18 |
19 | jobs:
20 | test:
21 | runs-on: ubuntu-latest
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | go-version: ["1.22", "1.23"]
26 | steps:
27 | - uses: actions/checkout@v4
28 | - name: Install Go
29 | uses: actions/setup-go@v5
30 | with:
31 | go-version: ${{ matrix.go-version }}
32 | - name: Test
33 | run: |
34 | go test -race ./...
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=code,go,linux,macos,windows
3 |
4 | ### Code ###
5 | .vscode/*
6 | !.vscode/tasks.json
7 | !.vscode/launch.json
8 | *.code-workspace
9 |
10 | ### Go ###
11 | # Binaries for programs and plugins
12 | *.exe
13 | *.exe~
14 | *.dll
15 | *.so
16 | *.dylib
17 |
18 | # Test binary, built with `go test -c`
19 | *.test
20 |
21 | # Output of the go coverage tool, specifically when used with LiteIDE
22 | *.out
23 |
24 | # Dependency directories (remove the comment below to include it)
25 | # vendor/
26 |
27 | ### Go Patch ###
28 | /vendor/
29 | /Godeps/
30 |
31 | ### Linux ###
32 | *~
33 |
34 | # temporary files which can be created if a process still has a handle open of a deleted file
35 | .fuse_hidden*
36 |
37 | # KDE directory preferences
38 | .directory
39 |
40 | # Linux trash folder which might appear on any partition or disk
41 | .Trash-*
42 |
43 | # .nfs files are created when an open file is removed but is still being accessed
44 | .nfs*
45 |
46 | ### macOS ###
47 | # General
48 | .DS_Store
49 | .AppleDouble
50 | .LSOverride
51 |
52 | # Icon must end with two \r
53 | Icon
54 |
55 |
56 | # Thumbnails
57 | ._*
58 |
59 | # Files that might appear in the root of a volume
60 | .DocumentRevisions-V100
61 | .fseventsd
62 | .Spotlight-V100
63 | .TemporaryItems
64 | .Trashes
65 | .VolumeIcon.icns
66 | .com.apple.timemachine.donotpresent
67 |
68 | # Directories potentially created on remote AFP share
69 | .AppleDB
70 | .AppleDesktop
71 | Network Trash Folder
72 | Temporary Items
73 | .apdisk
74 |
75 | ### Windows ###
76 | # Windows thumbnail cache files
77 | Thumbs.db
78 | Thumbs.db:encryptable
79 | ehthumbs.db
80 | ehthumbs_vista.db
81 |
82 | # Dump file
83 | *.stackdump
84 |
85 | # Folder config file
86 | [Dd]esktop.ini
87 |
88 | # Recycle Bin used on file shares
89 | $RECYCLE.BIN/
90 |
91 | # Windows Installer files
92 | *.cab
93 | *.msi
94 | *.msix
95 | *.msm
96 | *.msp
97 |
98 | # Windows shortcuts
99 | *.lnk
100 |
101 | # End of https://www.toptal.com/developers/gitignore/api/code,go,linux,macos,windows
102 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5 |
6 | The format of this file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
7 | but only releases after v1.0.3 properly adhere to it.
8 |
9 | ## [Unreleased]
10 | ### Added
11 | - `BlendLinearRgb` (#50)
12 | - `DistanceRiemersma` (#52)
13 | - Introduce a function for sorting colors (#57)
14 | - YAML marshal/unmarshal support (#63)
15 | - Add support for OkLab and OkLch (#66)
16 | - Functions that use randomness now support specifying a custom source (#73)
17 | - Functions BlendOkLab and BlendOkLch (#70)
18 |
19 | ### Fixed
20 | - Fix bug when doing HSV/HCL blending between a gray color and non-gray color (#60)
21 | - Docs for HSV/HSL were updated to note that hue 360 is not allowed (#71)
22 |
23 | ### Deprecated
24 | - `DistanceLinearRGB` is deprecated for the name `DistanceLinearRgb` which is more in-line with the rest of the library
25 |
26 |
27 | ## [1.2.0] - 2021-01-27
28 | This is the same as the v1.1.0 tag.
29 |
30 | ### Added
31 | - HSLuv and HPLuv color spaces (#41, #51)
32 | - CIE LCh(uv) color space, called `LuvLCh` in code (#51)
33 | - JSON and envconfig serialization support for `HexColor` (#42)
34 | - `DistanceLinearRGB` (#53)
35 |
36 | ### Fixed
37 | - RGB to/from XYZ conversion is more accurate (#51)
38 | - A bug in `XYZToLuvWhiteRef` that only applied to very small values was fixed (#51)
39 | - `BlendHCL` output is clamped so that it's not invalid (#46)
40 | - Properly documented `DistanceCIE76` (#40)
41 | - Some small godoc fixes
42 |
43 |
44 | ## [1.0.3] - 2019-11-11
45 | - Remove SQLMock dependency
46 |
47 |
48 | ## [1.0.2] - 2019-04-07
49 | - Fixes SQLMock dependency
50 |
51 |
52 | ## [1.0.1] - 2019-03-24
53 | - Adds support for Go Modules
54 |
55 |
56 | ## [1.0.0] - 2018-05-26
57 | - API Breaking change in `MakeColor`: instead of `panic`ing when alpha is zero, it now returns a secondary, boolean return value indicating success. See [the color.Color interface](#the-colorcolor-interface) section and [this FAQ entry](#q-why-would-makecolor-ever-fail) for details.
58 |
59 |
60 | ## [0.9.0] - 2018-05-26
61 | - Initial version number after having ignored versioning for a long time :)
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Lucas Beyer
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | go-colorful
2 | ===========
3 |
4 | [](https://pkg.go.dev/github.com/lucasb-eyer/go-colorful)
5 | [](https://goreportcard.com/report/github.com/lucasb-eyer/go-colorful)
6 |
7 | A library for playing with colors in Go. Supports Go 1.13 onwards.
8 |
9 | Why?
10 | ====
11 | I love games. I make games. I love detail and I get lost in detail.
12 | One such detail popped up during the development of [Memory Which Does Not Suck](https://github.com/lucasb-eyer/mwdns/),
13 | when we wanted the server to assign the players random colors. Sometimes
14 | two players got very similar colors, which bugged me. The very same evening,
15 | [I want hue](http://tools.medialab.sciences-po.fr/iwanthue/) was the top post
16 | on HackerNews' frontpage and showed me how to Do It Right™. Last but not
17 | least, there was no library for handling color spaces available in go. Colorful
18 | does just that and implements Go's `color.Color` interface.
19 |
20 | What?
21 | =====
22 | Go-Colorful stores colors in RGB and provides methods from converting these to various color-spaces. Currently supported colorspaces are:
23 |
24 | - **RGB:** All three of Red, Green and Blue in [0..1].
25 | - **HSL:** Hue in [0..360], Saturation and Luminance in [0..1]. For legacy reasons; please forget that it exists.
26 | - **HSV:** Hue in [0..360], Saturation and Value in [0..1]. You're better off using HCL, see below.
27 | - **Hex RGB:** The "internet" color format, as in #FF00FF.
28 | - **Linear RGB:** See [gamma correct rendering](http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/).
29 | - **CIE-XYZ:** CIE's standard color space, almost in [0..1].
30 | - **CIE-xyY:** encodes chromacity in x and y and luminance in Y, all in [0..1]
31 | - **CIE-L\*a\*b\*:** A *perceptually uniform* color space, i.e. distances are meaningful. L\* in [0..1] and a\*, b\* almost in [-1..1].
32 | - **CIE-L\*u\*v\*:** Very similar to CIE-L\*a\*b\*, there is [no consensus](http://en.wikipedia.org/wiki/CIELUV#Historical_background) on which one is "better".
33 | - **CIE-L\*C\*h° (HCL):** This is generally the [most useful](http://vis4.net/blog/posts/avoid-equidistant-hsv-colors/) one; CIE-L\*a\*b\* space in polar coordinates, i.e. a *better* HSV. H° is in [0..360], C\* almost in [0..1] and L\* as in CIE-L\*a\*b\*.
34 | - **CIE LCh(uv):** Called `LuvLCh` in code, this is a cylindrical transformation of the CIE-L\*u\*v\* color space. Like HCL above: H° is in [0..360], C\* almost in [0..1] and L\* as in CIE-L\*u\*v\*.
35 | - **HSLuv:** The better alternative to HSL, see [here](https://www.hsluv.org/) and [here](https://www.kuon.ch/post/2020-03-08-hsluv/). Hue in [0..360], Saturation and Luminance in [0..1].
36 | - **HPLuv:** A variant of HSLuv. The color space is smoother, but only pastel colors can be included. Because the valid colors are limited, it's easy to get invalid Saturation values way above 1.0, indicating the color can't be represented in HPLuv because it's not pastel.
37 |
38 | For the colorspaces where it makes sense (XYZ, Lab, Luv, HCl), the
39 | [D65](http://en.wikipedia.org/wiki/Illuminant_D65) is used as reference white
40 | by default but methods for using your own reference white are provided.
41 |
42 | A coordinate being *almost in* a range means that generally it is, but for very
43 | bright colors and depending on the reference white, it might overflow this
44 | range slightly. For example, C\* of #0000ff is 1.338.
45 |
46 | Unit-tests are provided.
47 |
48 | Nice, but what's it useful for?
49 | -------------------------------
50 |
51 | - Converting color spaces. Some people like to do that.
52 | - Blending (interpolating) between colors in a "natural" look by using the right colorspace.
53 | - Generating random colors under some constraints (e.g. colors of the same shade, or shades of one color.)
54 | - Generating gorgeous random palettes with distinct colors of a same temperature.
55 |
56 | So which colorspace should I use?
57 | =================================
58 | It depends on what you want to do. I think the folks from *I want hue* are
59 | on-spot when they say that RGB fits to how *screens produce* color, CIE L\*a\*b\*
60 | fits how *humans perceive* color and HCL fits how *humans think* colors.
61 |
62 | Whenever you'd use HSV, rather go for CIE-L\*C\*h°. for fixed lightness L\* and
63 | chroma C\* values, the hue angle h° rotates through colors of the same
64 | perceived brightness and intensity.
65 |
66 | How?
67 | ====
68 |
69 | ### Installing
70 | Installing the library is as easy as
71 |
72 | ```bash
73 | $ go get github.com/lucasb-eyer/go-colorful
74 | ```
75 |
76 | The package can then be used through an
77 |
78 | ```go
79 | import "github.com/lucasb-eyer/go-colorful"
80 | ```
81 |
82 | ### Basic usage
83 |
84 | Create a beautiful blue color using different source space:
85 |
86 | ```go
87 | // Any of the following should be the same
88 | c := colorful.Color{0.313725, 0.478431, 0.721569}
89 | c, err := colorful.Hex("#517AB8")
90 | if err != nil {
91 | log.Fatal(err)
92 | }
93 | c = colorful.Hsv(216.0, 0.56, 0.722)
94 | c = colorful.Xyz(0.189165, 0.190837, 0.480248)
95 | c = colorful.Xyy(0.219895, 0.221839, 0.190837)
96 | c = colorful.Lab(0.507850, 0.040585,-0.370945)
97 | c = colorful.Luv(0.507849,-0.194172,-0.567924)
98 | c = colorful.Hcl(276.2440, 0.373160, 0.507849)
99 | fmt.Printf("RGB values: %v, %v, %v", c.R, c.G, c.B)
100 | ```
101 |
102 | And then converting this color back into various color spaces:
103 |
104 | ```go
105 | hex := c.Hex()
106 | h, s, v := c.Hsv()
107 | x, y, z := c.Xyz()
108 | x, y, Y := c.Xyy()
109 | l, a, b := c.Lab()
110 | l, u, v := c.Luv()
111 | h, c, l := c.Hcl()
112 | ```
113 |
114 | Note that, because of Go's unfortunate choice of requiring an initial uppercase,
115 | the name of the functions relating to the xyY space are just off. If you have
116 | any good suggestion, please open an issue. (I don't consider XyY good.)
117 |
118 | ### The `color.Color` interface
119 | Because a `colorful.Color` implements Go's `color.Color` interface (found in the
120 | `image/color` package), it can be used anywhere that expects a `color.Color`.
121 |
122 | Furthermore, you can convert anything that implements the `color.Color` interface
123 | into a `colorful.Color` using the `MakeColor` function:
124 |
125 | ```go
126 | c, ok := colorful.MakeColor(color.Gray16{12345})
127 | ```
128 |
129 | **Caveat:** Be aware that this latter conversion (using `MakeColor`) hits a
130 | corner-case when alpha is exactly zero. Because `color.Color` uses pre-multiplied
131 | alpha colors, this means the RGB values are lost (set to 0) and it's impossible
132 | to recover them. In such a case `MakeColor` will return `false` as its second value.
133 |
134 | ### Comparing colors
135 | In the RGB color space, the Euclidean distance between colors *doesn't* correspond
136 | to visual/perceptual distance. This means that two pairs of colors which have the
137 | same distance in RGB space can look much further apart. This is fixed by the
138 | CIE-L\*a\*b\*, CIE-L\*u\*v\* and CIE-L\*C\*h° color spaces.
139 | Thus you should only compare colors in any of these space.
140 | (Note that the distance in CIE-L\*a\*b\* and CIE-L\*C\*h° are the same, since it's the same space but in cylindrical coordinates)
141 |
142 | 
143 |
144 | The two colors shown on the top look much more different than the two shown on
145 | the bottom. Still, in RGB space, their distance is the same.
146 | Here is a little example program which shows the distances between the top two
147 | and bottom two colors in RGB, CIE-L\*a\*b\* and CIE-L\*u\*v\* space. You can find it in `doc/colordist/colordist.go`.
148 |
149 | ```go
150 | package main
151 |
152 | import "fmt"
153 | import "github.com/lucasb-eyer/go-colorful"
154 |
155 | func main() {
156 | c1a := colorful.Color{150.0 / 255.0, 10.0 / 255.0, 150.0 / 255.0}
157 | c1b := colorful.Color{53.0 / 255.0, 10.0 / 255.0, 150.0 / 255.0}
158 | c2a := colorful.Color{10.0 / 255.0, 150.0 / 255.0, 50.0 / 255.0}
159 | c2b := colorful.Color{99.9 / 255.0, 150.0 / 255.0, 10.0 / 255.0}
160 |
161 | fmt.Printf("DistanceRgb: c1: %v\tand c2: %v\n", c1a.DistanceRgb(c1b), c2a.DistanceRgb(c2b))
162 | fmt.Printf("DistanceLab: c1: %v\tand c2: %v\n", c1a.DistanceLab(c1b), c2a.DistanceLab(c2b))
163 | fmt.Printf("DistanceLuv: c1: %v\tand c2: %v\n", c1a.DistanceLuv(c1b), c2a.DistanceLuv(c2b))
164 | fmt.Printf("DistanceCIE76: c1: %v\tand c2: %v\n", c1a.DistanceCIE76(c1b), c2a.DistanceCIE76(c2b))
165 | fmt.Printf("DistanceCIE94: c1: %v\tand c2: %v\n", c1a.DistanceCIE94(c1b), c2a.DistanceCIE94(c2b))
166 | fmt.Printf("DistanceCIEDE2000: c1: %v\tand c2: %v\n", c1a.DistanceCIEDE2000(c1b), c2a.DistanceCIEDE2000(c2b))
167 | }
168 | ```
169 |
170 | Running the above program shows that you should always prefer any of the CIE distances:
171 |
172 | ```bash
173 | $ go run colordist.go
174 | DistanceRgb: c1: 0.3803921568627451 and c2: 0.3858713931171159
175 | DistanceLab: c1: 0.32048458312798056 and c2: 0.24397151758565272
176 | DistanceLuv: c1: 0.5134369614199698 and c2: 0.2568692839860636
177 | DistanceCIE76: c1: 0.32048458312798056 and c2: 0.24397151758565272
178 | DistanceCIE94: c1: 0.19799168128511324 and c2: 0.12207136371167401
179 | DistanceCIEDE2000: c1: 0.17274551120971166 and c2: 0.10665210031428465
180 | ```
181 |
182 | It also shows that `DistanceLab` is more formally known as `DistanceCIE76` and
183 | has been superseded by the slightly more accurate, but much more expensive
184 | `DistanceCIE94` and `DistanceCIEDE2000`.
185 |
186 | Note that `AlmostEqualRgb` is provided mainly for (unit-)testing purposes. Use
187 | it only if you really know what you're doing. It will eat your cat.
188 |
189 | ### Blending colors
190 | Blending is highly connected to distance, since it basically "walks through" the
191 | colorspace thus, if the colorspace maps distances well, the walk is "smooth".
192 |
193 | Colorful comes with blending functions in RGB, HSV and any of the LAB spaces.
194 | Of course, you'd rather want to use the blending functions of the LAB spaces since
195 | these spaces map distances well but, just in case, here is an example showing
196 | you how the blendings (`#fdffcc` to `#242a42`) are done in the various spaces:
197 |
198 | 
199 |
200 | What you see is that HSV is really bad: it adds some green, which is not present
201 | in the original colors at all! RGB is much better, but it stays light a little
202 | too long. LUV and LAB both hit the right lightness but LAB has a little more
203 | color. HCL works in the same vein as HSV (both cylindrical interpolations) but
204 | it does it right in that there is no green appearing and the lightness changes
205 | in a linear manner.
206 |
207 | While this seems all good, you need to know one thing: When interpolating in any
208 | of the CIE color spaces, you might get invalid RGB colors! This is important if
209 | the starting and ending colors are user-input or random. An example of where this
210 | happens is when blending between `#eeef61` and `#1e3140`:
211 |
212 | 
213 |
214 | You can test whether a color is a valid RGB color by calling the `IsValid` method
215 | and indeed, calling IsValid will return false for the redish colors on the bottom.
216 | One way to "fix" this is to get a valid color close to the invalid one by calling
217 | `Clamped`, which always returns a nearby valid color. Doing this, we get the
218 | following result, which is satisfactory:
219 |
220 | 
221 |
222 | The following is the code creating the above three images; it can be found in `doc/colorblend/colorblend.go`
223 |
224 | ```go
225 | package main
226 |
227 | import "fmt"
228 | import "github.com/lucasb-eyer/go-colorful"
229 | import "image"
230 | import "image/draw"
231 | import "image/png"
232 | import "os"
233 |
234 | func main() {
235 | blocks := 10
236 | blockw := 40
237 | img := image.NewRGBA(image.Rect(0,0,blocks*blockw,200))
238 |
239 | c1, _ := colorful.Hex("#fdffcc")
240 | c2, _ := colorful.Hex("#242a42")
241 |
242 | // Use these colors to get invalid RGB in the gradient.
243 | //c1, _ := colorful.Hex("#EEEF61")
244 | //c2, _ := colorful.Hex("#1E3140")
245 |
246 | for i := 0 ; i < blocks ; i++ {
247 | draw.Draw(img, image.Rect(i*blockw, 0,(i+1)*blockw, 40), &image.Uniform{c1.BlendHsv(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
248 | draw.Draw(img, image.Rect(i*blockw, 40,(i+1)*blockw, 80), &image.Uniform{c1.BlendLuv(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
249 | draw.Draw(img, image.Rect(i*blockw, 80,(i+1)*blockw,120), &image.Uniform{c1.BlendRgb(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
250 | draw.Draw(img, image.Rect(i*blockw,120,(i+1)*blockw,160), &image.Uniform{c1.BlendLab(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
251 | draw.Draw(img, image.Rect(i*blockw,160,(i+1)*blockw,200), &image.Uniform{c1.BlendHcl(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
252 |
253 | // This can be used to "fix" invalid colors in the gradient.
254 | //draw.Draw(img, image.Rect(i*blockw,160,(i+1)*blockw,200), &image.Uniform{c1.BlendHcl(c2, float64(i)/float64(blocks-1)).Clamped()}, image.Point{}, draw.Src)
255 | }
256 |
257 | toimg, err := os.Create("colorblend.png")
258 | if err != nil {
259 | fmt.Printf("Error: %v", err)
260 | return
261 | }
262 | defer toimg.Close()
263 |
264 | png.Encode(toimg, img)
265 | }
266 | ```
267 |
268 | #### Generating color gradients
269 | A very common reason to blend colors is creating gradients. There is an example
270 | program in [doc/gradientgen.go](doc/gradientgen/gradientgen.go); it doesn't use any API
271 | which hasn't been used in the previous example code, so I won't bother pasting
272 | the code in here. Just look at that gorgeous gradient it generated in HCL space:
273 |
274 | 
275 |
276 | ### Getting random colors
277 | It is sometimes necessary to generate random colors. You could simply do this
278 | on your own by generating colors with random values. By restricting the random
279 | values to a range smaller than [0..1] and using a space such as CIE-H\*C\*l° or
280 | HSV, you can generate both random shades of a color or random colors of a
281 | lightness:
282 |
283 | ```go
284 | random_blue := colorful.Hcl(180.0+rand.Float64()*50.0, 0.2+rand.Float64()*0.8, 0.3+rand.Float64()*0.7)
285 | random_dark := colorful.Hcl(rand.Float64()*360.0, rand.Float64(), rand.Float64()*0.4)
286 | random_light := colorful.Hcl(rand.Float64()*360.0, rand.Float64(), 0.6+rand.Float64()*0.4)
287 | ```
288 |
289 | Since getting random "warm" and "happy" colors is quite a common task, there
290 | are some helper functions:
291 |
292 | ```go
293 | colorful.WarmColor()
294 | colorful.HappyColor()
295 | colorful.FastWarmColor()
296 | colorful.FastHappyColor()
297 | ```
298 |
299 | The ones prefixed by `Fast` are faster but less coherent since they use the HSV
300 | space as opposed to the regular ones which use CIE-L\*C\*h° space. The
301 | following picture shows the warm colors in the top two rows and happy colors
302 | in the bottom two rows. Within these, the first is the regular one and the
303 | second is the fast one.
304 |
305 | 
306 |
307 | Don't forget to initialize the random seed! You can see the code used for
308 | generating this picture in `doc/colorgens/colorgens.go`.
309 |
310 | ### Getting random palettes
311 | As soon as you need to generate more than one random color, you probably want
312 | them to be distinguishable. Playing against an opponent which has almost the
313 | same blue as I do is not fun. This is where random palettes can help.
314 |
315 | These palettes are generated using an algorithm which ensures that all colors
316 | on the palette are as distinguishable as possible. Again, there is a `Fast`
317 | method which works in HSV and is less perceptually uniform and a non-`Fast`
318 | method which works in CIE spaces. For more theory on `SoftPalette`, check out
319 | [I want hue](http://tools.medialab.sciences-po.fr/iwanthue/theory.php). Yet
320 | again, there is a `Happy` and a `Warm` version, which do what you expect, but
321 | now there is an additional `Soft` version, which is more configurable: you can
322 | give a constraint on the color space in order to get colors within a certain *feel*.
323 |
324 | Let's start with the simple methods first, all they take is the amount of
325 | colors to generate, which could, for example, be the player count. They return
326 | an array of `colorful.Color` objects:
327 |
328 | ```go
329 | pal1, err1 := colorful.WarmPalette(10)
330 | pal2 := colorful.FastWarmPalette(10)
331 | pal3, err3 := colorful.HappyPalette(10)
332 | pal4 := colorful.FastHappyPalette(10)
333 | pal5, err5 := colorful.SoftPalette(10)
334 | ```
335 |
336 | Note that the non-fast methods *may* fail if you ask for way too many colors.
337 | Let's move on to the advanced one, namely `SoftPaletteEx`. Besides the color
338 | count, this function takes a `SoftPaletteSettings` object as argument. The
339 | interesting part here is its `CheckColor` member, which is a boolean function
340 | taking three floating points as arguments: `l`, `a` and `b`. This function
341 | should return `true` for colors which lie within the region you want and `false`
342 | otherwise. The other members are `Iteration`, which should be within [5..100]
343 | where higher means slower but more exact palette, and `ManySamples` which you
344 | should set to `true` in case your `CheckColor` constraint rejects a large part
345 | of the color space.
346 |
347 | For example, to create a palette of 10 brownish colors, you'd call it like this:
348 |
349 | ```go
350 | func isbrowny(l, a, b float64) bool {
351 | h, c, L := colorful.LabToHcl(l, a, b)
352 | return 10.0 < h && h < 50.0 && 0.1 < c && c < 0.5 && L < 0.5
353 | }
354 | // Since the above function is pretty restrictive, we set ManySamples to true.
355 | brownies := colorful.SoftPaletteEx(10, colorful.SoftPaletteSettings{isbrowny, 50, true})
356 | ```
357 |
358 | The following picture shows the palettes generated by all of these methods
359 | (sourcecode in `doc/palettegens/palettegens.go`), in the order they were presented, i.e.
360 | from top to bottom: `Warm`, `FastWarm`, `Happy`, `FastHappy`, `Soft`,
361 | `SoftEx(isbrowny)`. All of them contain some randomness, so YMMV.
362 |
363 | 
364 |
365 | Again, the code used for generating the above image is available as [doc/palettegens/palettegens.go](https://github.com/lucasb-eyer/go-colorful/blob/master/doc/palettegens/palettegens.go).
366 |
367 | ### Sorting colors
368 |
369 | Sorting colors is not a well-defined operation. For example, {dark blue, dark red, light blue, light red} is already sorted if darker colors should precede lighter colors but would need to be re-sorted as {dark red, light red, dark blue, light blue} if longer-wavelength colors should precede shorter-wavelength colors.
370 |
371 | Go-Colorful's `Sorted` function orders a list of colors so as to minimize the average distance between adjacent colors, including between the last and the first. (`Sorted` does not necessarily find the true minimum, only a reasonably close approximation.) The following picture, drawn by [doc/colorsort/colorsort.go](https://github.com/lucasb-eyer/go-colorful/blob/master/doc/colorsort/colorsort.go), illustrates `Sorted`'s behavior:
372 |
373 | 
374 |
375 | The first row represents the input: a slice of 512 randomly chosen colors. The second row shows the colors sorted in CIE-L\*C\*h° space, ordered first by lightness (L), then by hue angle (h), and finally by chroma (C). Note that distracting pinstripes permeate the colors. Sorting using *any* color space and *any* ordering of the channels yields a similar pinstriped pattern. The third row of the image was sorted using Go-Colorful's `Sorted` function. Although the colors do not appear to be in any particular order, the sequence at least appears smoother than the one sorted by channel.
376 |
377 |
378 | ### Using linear RGB for computations
379 | There are two methods for transforming RGB⟷Linear RGB: a fast and almost precise one,
380 | and a slow and precise one.
381 |
382 | ```go
383 | r, g, b := colorful.Hex("#FF0000").FastLinearRgb()
384 | ```
385 |
386 | TODO: describe some more.
387 |
388 | ### Want to use some other reference point?
389 |
390 | ```go
391 | c := colorful.LabWhiteRef(0.507850, 0.040585,-0.370945, colorful.D50)
392 | l, a, b := c.LabWhiteRef(colorful.D50)
393 | ```
394 |
395 | ### Reading and writing colors from databases
396 |
397 | The type `HexColor` makes it easy to store colors as strings in a database. It
398 | implements the [https://godoc.org/database/sql#Scanner](database/sql.Scanner)
399 | and [database/sql/driver.Value](https://godoc.org/database/sql/driver.Value)
400 | interfaces which provide automatic type conversion.
401 |
402 | Example:
403 |
404 | ```go
405 | var hc HexColor
406 | _, err := db.QueryRow("SELECT '#ff0000';").Scan(&hc)
407 | // hc == HexColor{R: 1, G: 0, B: 0}; err == nil
408 | ```
409 |
410 | FAQ
411 | ===
412 |
413 | ### Q: I get all f!@#ed up values! Your library sucks!
414 | A: You probably provided values in the wrong range. For example, RGB values are
415 | expected to reside between 0 and 1, *not* between 0 and 255. Normalize your colors.
416 |
417 | ### Q: Lab/Luv/HCl seem broken! Your library sucks!
418 | They look like this:
419 |
420 |
421 |
422 | A: You're likely trying to generate and display colors that can't be represented by RGB,
423 | and thus monitors. When you're trying to convert, say, `HCL(190.0, 1.0, 1.0).RGB255()`,
424 | you're asking for RGB values of `(-2105.254 300.680 286.185)`, which clearly don't exist,
425 | and the `RGB255` function just casts these numbers to `uint8`, creating wrap-around and
426 | what looks like a completely broken gradient. What you want to do, is either use more
427 | reasonable values of colors which actually exist in RGB, or just `Clamp()` the resulting
428 | color to its nearest existing one, living with the consequences:
429 | `HCL(190.0, 1.0, 1.0).Clamp().RGB255()`. It will look something like this:
430 |
431 |
432 |
433 | [Here's an issue going in-depth about this](https://github.com/lucasb-eyer/go-colorful/issues/14),
434 | as well as [my answer](https://github.com/lucasb-eyer/go-colorful/issues/14#issuecomment-324205385),
435 | both with code and pretty pictures. Also note that this was somewhat covered above in the
436 | ["Blending colors" section](https://github.com/lucasb-eyer/go-colorful#blending-colors).
437 |
438 | ### Q: In a tight loop, conversion to Lab/Luv/HCl/... are slooooow!
439 | A: Yes, they are.
440 | This library aims for correctness, readability, and modularity; it wasn't written with speed in mind.
441 | A large part of the slowness comes from these conversions going through `LinearRgb` which uses powers.
442 | I implemented a fast approximation to `LinearRgb` called `FastLinearRgb` by using Taylor approximations.
443 | The approximation is roughly 5x faster and precise up to roughly 0.5%,
444 | the major caveat being that if the input values are outside the range 0-1, accuracy drops dramatically.
445 | You can use these in your conversions as follows:
446 |
447 | ```go
448 | col := // Get your color somehow
449 | l, a, b := XyzToLab(LinearRgbToXyz(col.LinearRgb()))
450 | ```
451 |
452 | If you need faster versions of `Distance*` and `Blend*` that make use of this fast approximation,
453 | feel free to implement them and open a pull-request, I'll happily accept.
454 |
455 | The derivation of these functions can be followed in [this Jupyter notebook](doc/LinearRGB Approximations.ipynb).
456 | Here's the main figure showing the approximation quality:
457 |
458 | 
459 |
460 | More speed could be gained by using SIMD instructions in many places.
461 | You can also get more speed for specific conversions by approximating the full conversion function,
462 | but that is outside the scope of this library.
463 | Thanks to [@ZirconiumX](https://github.com/ZirconiumX) for starting this investigation,
464 | see [issue #18](https://github.com/lucasb-eyer/go-colorful/issues/18) for details.
465 |
466 | ### Q: Why would `MakeColor` ever fail!?
467 | A: `MakeColor` fails when the alpha channel is zero. In that case, the
468 | conversion is undefined. See [issue 21](https://github.com/lucasb-eyer/go-colorful/issues/21)
469 | as well as the short caveat note in the ["The `color.Color` interface"](README.md#the-colorcolor-interface)
470 | section above.
471 |
472 | Who?
473 | ====
474 |
475 | This library was developed by Lucas Beyer with contributions from
476 | Bastien Dejean (@baskerville), Phil Kulak (@pkulak), Christian Muehlhaeuser (@muesli), and Scott Pakin (@spakin).
477 |
478 | It is now maintained by makeworld (@makeworld-the-better-one).
479 |
480 |
481 | ## License
482 |
483 | This repo is under the MIT license, see [LICENSE](LICENSE) for details.
484 |
--------------------------------------------------------------------------------
/colorgens.go:
--------------------------------------------------------------------------------
1 | // Various ways to generate single random colors
2 |
3 | package colorful
4 |
5 | // Creates a random dark, "warm" color through a restricted HSV space.
6 | func FastWarmColorWithRand(rand RandInterface) Color {
7 | return Hsv(
8 | rand.Float64()*360.0,
9 | 0.5+rand.Float64()*0.3,
10 | 0.3+rand.Float64()*0.3)
11 | }
12 |
13 | func FastWarmColor() Color {
14 | return FastWarmColorWithRand(getDefaultGlobalRand())
15 | }
16 |
17 | // Creates a random dark, "warm" color through restricted HCL space.
18 | // This is slower than FastWarmColor but will likely give you colors which have
19 | // the same "warmness" if you run it many times.
20 | func WarmColorWithRand(rand RandInterface) (c Color) {
21 | for c = randomWarmWithRand(rand); !c.IsValid(); c = randomWarmWithRand(rand) {
22 | }
23 | return
24 | }
25 |
26 | func WarmColor() (c Color) {
27 | return WarmColorWithRand(getDefaultGlobalRand())
28 | }
29 |
30 | func randomWarmWithRand(rand RandInterface) Color {
31 | return Hcl(
32 | rand.Float64()*360.0,
33 | 0.1+rand.Float64()*0.3,
34 | 0.2+rand.Float64()*0.3)
35 | }
36 |
37 | // Creates a random bright, "pimpy" color through a restricted HSV space.
38 | func FastHappyColorWithRand(rand RandInterface) Color {
39 | return Hsv(
40 | rand.Float64()*360.0,
41 | 0.7+rand.Float64()*0.3,
42 | 0.6+rand.Float64()*0.3)
43 | }
44 |
45 | func FastHappyColor() Color {
46 | return FastHappyColorWithRand(getDefaultGlobalRand())
47 | }
48 |
49 | // Creates a random bright, "pimpy" color through restricted HCL space.
50 | // This is slower than FastHappyColor but will likely give you colors which
51 | // have the same "brightness" if you run it many times.
52 | func HappyColorWithRand(rand RandInterface) (c Color) {
53 | for c = randomPimpWithRand(rand); !c.IsValid(); c = randomPimpWithRand(rand) {
54 | }
55 | return
56 | }
57 |
58 | func HappyColor() (c Color) {
59 | return HappyColorWithRand(getDefaultGlobalRand())
60 | }
61 |
62 | func randomPimpWithRand(rand RandInterface) Color {
63 | return Hcl(
64 | rand.Float64()*360.0,
65 | 0.5+rand.Float64()*0.3,
66 | 0.5+rand.Float64()*0.3)
67 | }
68 |
--------------------------------------------------------------------------------
/colorgens_test.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 | "time"
7 | )
8 |
9 | // This is really difficult to test, if you've got a good idea, pull request!
10 |
11 | // Check if it returns all valid colors.
12 | func TestColorValidity(t *testing.T) {
13 | // with default seed
14 | for i := 0; i < 100; i++ {
15 | if col := WarmColor(); !col.IsValid() {
16 | t.Errorf("Warm color %v is not valid! Seed was: default", col)
17 | }
18 |
19 | if col := FastWarmColor(); !col.IsValid() {
20 | t.Errorf("Fast warm color %v is not valid! Seed was: default", col)
21 | }
22 |
23 | if col := HappyColor(); !col.IsValid() {
24 | t.Errorf("Happy color %v is not valid! Seed was: default", col)
25 | }
26 |
27 | if col := FastHappyColor(); !col.IsValid() {
28 | t.Errorf("Fast happy color %v is not valid! Seed was: default", col)
29 | }
30 | }
31 |
32 | // with custom seed
33 | seed := time.Now().UTC().UnixNano()
34 | rand := rand.New(rand.NewSource(seed))
35 |
36 | for i := 0; i < 100; i++ {
37 | if col := WarmColorWithRand(rand); !col.IsValid() {
38 | t.Errorf("Warm color %v is not valid! Seed was: %v", col, seed)
39 | }
40 |
41 | if col := FastWarmColorWithRand(rand); !col.IsValid() {
42 | t.Errorf("Fast warm color %v is not valid! Seed was: %v", col, seed)
43 | }
44 |
45 | if col := HappyColorWithRand(rand); !col.IsValid() {
46 | t.Errorf("Happy color %v is not valid! Seed was: %v", col, seed)
47 | }
48 |
49 | if col := FastHappyColorWithRand(rand); !col.IsValid() {
50 | t.Errorf("Fast happy color %v is not valid! Seed was: %v", col, seed)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/colors.go:
--------------------------------------------------------------------------------
1 | // The colorful package provides all kinds of functions for working with colors.
2 | package colorful
3 |
4 | import (
5 | "fmt"
6 | "image/color"
7 | "math"
8 | )
9 |
10 | // A color is stored internally using sRGB (standard RGB) values in the range 0-1
11 | type Color struct {
12 | R, G, B float64
13 | }
14 |
15 | // Implement the Go color.Color interface.
16 | func (col Color) RGBA() (r, g, b, a uint32) {
17 | r = uint32(col.R*65535.0 + 0.5)
18 | g = uint32(col.G*65535.0 + 0.5)
19 | b = uint32(col.B*65535.0 + 0.5)
20 | a = 0xFFFF
21 | return
22 | }
23 |
24 | // Constructs a colorful.Color from something implementing color.Color
25 | func MakeColor(col color.Color) (Color, bool) {
26 | r, g, b, a := col.RGBA()
27 | if a == 0 {
28 | return Color{0, 0, 0}, false
29 | }
30 |
31 | // Since color.Color is alpha pre-multiplied, we need to divide the
32 | // RGB values by alpha again in order to get back the original RGB.
33 | r *= 0xffff
34 | r /= a
35 | g *= 0xffff
36 | g /= a
37 | b *= 0xffff
38 | b /= a
39 |
40 | return Color{float64(r) / 65535.0, float64(g) / 65535.0, float64(b) / 65535.0}, true
41 | }
42 |
43 | // Might come in handy sometimes to reduce boilerplate code.
44 | func (col Color) RGB255() (r, g, b uint8) {
45 | r = uint8(col.R*255.0 + 0.5)
46 | g = uint8(col.G*255.0 + 0.5)
47 | b = uint8(col.B*255.0 + 0.5)
48 | return
49 | }
50 |
51 | // Used to simplify HSLuv testing.
52 | func (col Color) values() (float64, float64, float64) {
53 | return col.R, col.G, col.B
54 | }
55 |
56 | // This is the tolerance used when comparing colors using AlmostEqualRgb.
57 | const Delta = 1.0 / 255.0
58 |
59 | // This is the default reference white point.
60 | var D65 = [3]float64{0.95047, 1.00000, 1.08883}
61 |
62 | // And another one.
63 | var D50 = [3]float64{0.96422, 1.00000, 0.82521}
64 |
65 | // Checks whether the color exists in RGB space, i.e. all values are in [0..1]
66 | func (c Color) IsValid() bool {
67 | return 0.0 <= c.R && c.R <= 1.0 &&
68 | 0.0 <= c.G && c.G <= 1.0 &&
69 | 0.0 <= c.B && c.B <= 1.0
70 | }
71 |
72 | // clamp01 clamps from 0 to 1.
73 | func clamp01(v float64) float64 {
74 | return math.Max(0.0, math.Min(v, 1.0))
75 | }
76 |
77 | // Returns Clamps the color into valid range, clamping each value to [0..1]
78 | // If the color is valid already, this is a no-op.
79 | func (c Color) Clamped() Color {
80 | return Color{clamp01(c.R), clamp01(c.G), clamp01(c.B)}
81 | }
82 |
83 | func sq(v float64) float64 {
84 | return v * v
85 | }
86 |
87 | func cub(v float64) float64 {
88 | return v * v * v
89 | }
90 |
91 | // DistanceRgb computes the distance between two colors in RGB space.
92 | // This is not a good measure! Rather do it in Lab space.
93 | func (c1 Color) DistanceRgb(c2 Color) float64 {
94 | return math.Sqrt(sq(c1.R-c2.R) + sq(c1.G-c2.G) + sq(c1.B-c2.B))
95 | }
96 |
97 | // DistanceLinearRgb computes the distance between two colors in linear RGB
98 | // space. This is not useful for measuring how humans perceive color, but
99 | // might be useful for other things, like dithering.
100 | func (c1 Color) DistanceLinearRgb(c2 Color) float64 {
101 | r1, g1, b1 := c1.LinearRgb()
102 | r2, g2, b2 := c2.LinearRgb()
103 | return math.Sqrt(sq(r1-r2) + sq(g1-g2) + sq(b1-b2))
104 | }
105 |
106 | // DistanceLinearRGB is deprecated in favour of DistanceLinearRgb.
107 | // They do the exact same thing.
108 | func (c1 Color) DistanceLinearRGB(c2 Color) float64 {
109 | return c1.DistanceLinearRgb(c2)
110 | }
111 |
112 | // DistanceRiemersma is a color distance algorithm developed by Thiadmer Riemersma.
113 | // It uses RGB coordinates, but he claims it has similar results to CIELUV.
114 | // This makes it both fast and accurate.
115 | //
116 | // Sources:
117 | //
118 | // https://www.compuphase.com/cmetric.htm
119 | // https://github.com/lucasb-eyer/go-colorful/issues/52
120 | func (c1 Color) DistanceRiemersma(c2 Color) float64 {
121 | rAvg := (c1.R + c2.R) / 2.0
122 | // Deltas
123 | dR := c1.R - c2.R
124 | dG := c1.G - c2.G
125 | dB := c1.B - c2.B
126 |
127 | return math.Sqrt((2+rAvg)*dR*dR + 4*dG*dG + (2+(1-rAvg))*dB*dB)
128 | }
129 |
130 | // Check for equality between colors within the tolerance Delta (1/255).
131 | func (c1 Color) AlmostEqualRgb(c2 Color) bool {
132 | return math.Abs(c1.R-c2.R)+
133 | math.Abs(c1.G-c2.G)+
134 | math.Abs(c1.B-c2.B) < 3.0*Delta
135 | }
136 |
137 | // You don't really want to use this, do you? Go for BlendLab, BlendLuv or BlendHcl.
138 | func (c1 Color) BlendRgb(c2 Color, t float64) Color {
139 | return Color{
140 | c1.R + t*(c2.R-c1.R),
141 | c1.G + t*(c2.G-c1.G),
142 | c1.B + t*(c2.B-c1.B),
143 | }
144 | }
145 |
146 | // Utility used by Hxx color-spaces for interpolating between two angles in [0,360].
147 | func interp_angle(a0, a1, t float64) float64 {
148 | // Based on the answer here: http://stackoverflow.com/a/14498790/2366315
149 | // With potential proof that it works here: http://math.stackexchange.com/a/2144499
150 | delta := math.Mod(math.Mod(a1-a0, 360.0)+540, 360.0) - 180.0
151 | return math.Mod(a0+t*delta+360.0, 360.0)
152 | }
153 |
154 | /// HSV ///
155 | ///////////
156 | // From http://en.wikipedia.org/wiki/HSL_and_HSV
157 | // Note that h is in [0..359] and s,v in [0..1]
158 |
159 | // Hsv returns the Hue [0..359], Saturation and Value [0..1] of the color.
160 | func (col Color) Hsv() (h, s, v float64) {
161 | min := math.Min(math.Min(col.R, col.G), col.B)
162 | v = math.Max(math.Max(col.R, col.G), col.B)
163 | C := v - min
164 |
165 | s = 0.0
166 | if v != 0.0 {
167 | s = C / v
168 | }
169 |
170 | h = 0.0 // We use 0 instead of undefined as in wp.
171 | if min != v {
172 | if v == col.R {
173 | h = math.Mod((col.G-col.B)/C, 6.0)
174 | }
175 | if v == col.G {
176 | h = (col.B-col.R)/C + 2.0
177 | }
178 | if v == col.B {
179 | h = (col.R-col.G)/C + 4.0
180 | }
181 | h *= 60.0
182 | if h < 0.0 {
183 | h += 360.0
184 | }
185 | }
186 | return
187 | }
188 |
189 | // Hsv creates a new Color given a Hue in [0..359], a Saturation and a Value in [0..1]
190 | func Hsv(H, S, V float64) Color {
191 | Hp := H / 60.0
192 | C := V * S
193 | X := C * (1.0 - math.Abs(math.Mod(Hp, 2.0)-1.0))
194 |
195 | m := V - C
196 | r, g, b := 0.0, 0.0, 0.0
197 |
198 | switch {
199 | case 0.0 <= Hp && Hp < 1.0:
200 | r = C
201 | g = X
202 | case 1.0 <= Hp && Hp < 2.0:
203 | r = X
204 | g = C
205 | case 2.0 <= Hp && Hp < 3.0:
206 | g = C
207 | b = X
208 | case 3.0 <= Hp && Hp < 4.0:
209 | g = X
210 | b = C
211 | case 4.0 <= Hp && Hp < 5.0:
212 | r = X
213 | b = C
214 | case 5.0 <= Hp && Hp < 6.0:
215 | r = C
216 | b = X
217 | }
218 |
219 | return Color{m + r, m + g, m + b}
220 | }
221 |
222 | // You don't really want to use this, do you? Go for BlendLab, BlendLuv or BlendHcl.
223 | func (c1 Color) BlendHsv(c2 Color, t float64) Color {
224 | h1, s1, v1 := c1.Hsv()
225 | h2, s2, v2 := c2.Hsv()
226 |
227 | // https://github.com/lucasb-eyer/go-colorful/pull/60
228 | if s1 == 0 && s2 != 0 {
229 | h1 = h2
230 | } else if s2 == 0 && s1 != 0 {
231 | h2 = h1
232 | }
233 |
234 | // We know that h are both in [0..360]
235 | return Hsv(interp_angle(h1, h2, t), s1+t*(s2-s1), v1+t*(v2-v1))
236 | }
237 |
238 | /// HSL ///
239 | ///////////
240 |
241 | // Hsl returns the Hue [0..359], Saturation [0..1], and Luminance (lightness) [0..1] of the color.
242 | func (col Color) Hsl() (h, s, l float64) {
243 | min := math.Min(math.Min(col.R, col.G), col.B)
244 | max := math.Max(math.Max(col.R, col.G), col.B)
245 |
246 | l = (max + min) / 2
247 |
248 | if min == max {
249 | s = 0
250 | h = 0
251 | } else {
252 | if l < 0.5 {
253 | s = (max - min) / (max + min)
254 | } else {
255 | s = (max - min) / (2.0 - max - min)
256 | }
257 |
258 | if max == col.R {
259 | h = (col.G - col.B) / (max - min)
260 | } else if max == col.G {
261 | h = 2.0 + (col.B-col.R)/(max-min)
262 | } else {
263 | h = 4.0 + (col.R-col.G)/(max-min)
264 | }
265 |
266 | h *= 60
267 |
268 | if h < 0 {
269 | h += 360
270 | }
271 | }
272 |
273 | return
274 | }
275 |
276 | // Hsl creates a new Color given a Hue in [0..359], a Saturation [0..1], and a Luminance (lightness) in [0..1]
277 | func Hsl(h, s, l float64) Color {
278 | if s == 0 {
279 | return Color{l, l, l}
280 | }
281 |
282 | var r, g, b float64
283 | var t1 float64
284 | var t2 float64
285 | var tr float64
286 | var tg float64
287 | var tb float64
288 |
289 | if l < 0.5 {
290 | t1 = l * (1.0 + s)
291 | } else {
292 | t1 = l + s - l*s
293 | }
294 |
295 | t2 = 2*l - t1
296 | h /= 360
297 | tr = h + 1.0/3.0
298 | tg = h
299 | tb = h - 1.0/3.0
300 |
301 | if tr < 0 {
302 | tr++
303 | }
304 | if tr > 1 {
305 | tr--
306 | }
307 | if tg < 0 {
308 | tg++
309 | }
310 | if tg > 1 {
311 | tg--
312 | }
313 | if tb < 0 {
314 | tb++
315 | }
316 | if tb > 1 {
317 | tb--
318 | }
319 |
320 | // Red
321 | if 6*tr < 1 {
322 | r = t2 + (t1-t2)*6*tr
323 | } else if 2*tr < 1 {
324 | r = t1
325 | } else if 3*tr < 2 {
326 | r = t2 + (t1-t2)*(2.0/3.0-tr)*6
327 | } else {
328 | r = t2
329 | }
330 |
331 | // Green
332 | if 6*tg < 1 {
333 | g = t2 + (t1-t2)*6*tg
334 | } else if 2*tg < 1 {
335 | g = t1
336 | } else if 3*tg < 2 {
337 | g = t2 + (t1-t2)*(2.0/3.0-tg)*6
338 | } else {
339 | g = t2
340 | }
341 |
342 | // Blue
343 | if 6*tb < 1 {
344 | b = t2 + (t1-t2)*6*tb
345 | } else if 2*tb < 1 {
346 | b = t1
347 | } else if 3*tb < 2 {
348 | b = t2 + (t1-t2)*(2.0/3.0-tb)*6
349 | } else {
350 | b = t2
351 | }
352 |
353 | return Color{r, g, b}
354 | }
355 |
356 | /// Hex ///
357 | ///////////
358 |
359 | // Hex returns the hex "html" representation of the color, as in #ff0080.
360 | func (col Color) Hex() string {
361 | // Add 0.5 for rounding
362 | return fmt.Sprintf("#%02x%02x%02x", uint8(col.R*255.0+0.5), uint8(col.G*255.0+0.5), uint8(col.B*255.0+0.5))
363 | }
364 |
365 | // Hex parses a "html" hex color-string, either in the 3 "#f0c" or 6 "#ff1034" digits form.
366 | func Hex(scol string) (Color, error) {
367 | format := "#%02x%02x%02x"
368 | factor := 1.0 / 255.0
369 | if len(scol) == 4 {
370 | format = "#%1x%1x%1x"
371 | factor = 1.0 / 15.0
372 | }
373 |
374 | var r, g, b uint8
375 | n, err := fmt.Sscanf(scol, format, &r, &g, &b)
376 | if err != nil {
377 | return Color{}, err
378 | }
379 | if n != 3 {
380 | return Color{}, fmt.Errorf("color: %v is not a hex-color", scol)
381 | }
382 |
383 | return Color{float64(r) * factor, float64(g) * factor, float64(b) * factor}, nil
384 | }
385 |
386 | /// Linear ///
387 | //////////////
388 | // http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/
389 | // http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html
390 |
391 | func linearize(v float64) float64 {
392 | if v <= 0.04045 {
393 | return v / 12.92
394 | }
395 | return math.Pow((v+0.055)/1.055, 2.4)
396 | }
397 |
398 | // LinearRgb converts the color into the linear RGB space (see http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/).
399 | func (col Color) LinearRgb() (r, g, b float64) {
400 | r = linearize(col.R)
401 | g = linearize(col.G)
402 | b = linearize(col.B)
403 | return
404 | }
405 |
406 | // A much faster and still quite precise linearization using a 6th-order Taylor approximation.
407 | // See the accompanying Jupyter notebook for derivation of the constants.
408 | func linearize_fast(v float64) float64 {
409 | v1 := v - 0.5
410 | v2 := v1 * v1
411 | v3 := v2 * v1
412 | v4 := v2 * v2
413 | // v5 := v3*v2
414 | return -0.248750514614486 + 0.925583310193438*v + 1.16740237321695*v2 + 0.280457026598666*v3 - 0.0757991963780179*v4 //+ 0.0437040411548932*v5
415 | }
416 |
417 | // FastLinearRgb is much faster than and almost as accurate as LinearRgb.
418 | // BUT it is important to NOTE that they only produce good results for valid colors r,g,b in [0,1].
419 | func (col Color) FastLinearRgb() (r, g, b float64) {
420 | r = linearize_fast(col.R)
421 | g = linearize_fast(col.G)
422 | b = linearize_fast(col.B)
423 | return
424 | }
425 |
426 | func delinearize(v float64) float64 {
427 | if v <= 0.0031308 {
428 | return 12.92 * v
429 | }
430 | return 1.055*math.Pow(v, 1.0/2.4) - 0.055
431 | }
432 |
433 | // LinearRgb creates an sRGB color out of the given linear RGB color (see http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/).
434 | func LinearRgb(r, g, b float64) Color {
435 | return Color{delinearize(r), delinearize(g), delinearize(b)}
436 | }
437 |
438 | func delinearize_fast(v float64) float64 {
439 | // This function (fractional root) is much harder to linearize, so we need to split.
440 | if v > 0.2 {
441 | v1 := v - 0.6
442 | v2 := v1 * v1
443 | v3 := v2 * v1
444 | v4 := v2 * v2
445 | v5 := v3 * v2
446 | return 0.442430344268235 + 0.592178981271708*v - 0.287864782562636*v2 + 0.253214392068985*v3 - 0.272557158129811*v4 + 0.325554383321718*v5
447 | } else if v > 0.03 {
448 | v1 := v - 0.115
449 | v2 := v1 * v1
450 | v3 := v2 * v1
451 | v4 := v2 * v2
452 | v5 := v3 * v2
453 | return 0.194915592891669 + 1.55227076330229*v - 3.93691860257828*v2 + 18.0679839248761*v3 - 101.468750302746*v4 + 632.341487393927*v5
454 | } else {
455 | v1 := v - 0.015
456 | v2 := v1 * v1
457 | v3 := v2 * v1
458 | v4 := v2 * v2
459 | v5 := v3 * v2
460 | // You can clearly see from the involved constants that the low-end is highly nonlinear.
461 | return 0.0519565234928877 + 5.09316778537561*v - 99.0338180489702*v2 + 3484.52322764895*v3 - 150028.083412663*v4 + 7168008.42971613*v5
462 | }
463 | }
464 |
465 | // FastLinearRgb is much faster than and almost as accurate as LinearRgb.
466 | // BUT it is important to NOTE that they only produce good results for valid inputs r,g,b in [0,1].
467 | func FastLinearRgb(r, g, b float64) Color {
468 | return Color{delinearize_fast(r), delinearize_fast(g), delinearize_fast(b)}
469 | }
470 |
471 | // XyzToLinearRgb converts from CIE XYZ-space to Linear RGB space.
472 | func XyzToLinearRgb(x, y, z float64) (r, g, b float64) {
473 | r = 3.2409699419045214*x - 1.5373831775700935*y - 0.49861076029300328*z
474 | g = -0.96924363628087983*x + 1.8759675015077207*y + 0.041555057407175613*z
475 | b = 0.055630079696993609*x - 0.20397695888897657*y + 1.0569715142428786*z
476 | return
477 | }
478 |
479 | func LinearRgbToXyz(r, g, b float64) (x, y, z float64) {
480 | x = 0.41239079926595948*r + 0.35758433938387796*g + 0.18048078840183429*b
481 | y = 0.21263900587151036*r + 0.71516867876775593*g + 0.072192315360733715*b
482 | z = 0.019330818715591851*r + 0.11919477979462599*g + 0.95053215224966058*b
483 | return
484 | }
485 |
486 | // BlendLinearRgb blends two colors in the Linear RGB color-space.
487 | // Unlike BlendRgb, this will not produce dark color around the center.
488 | // t == 0 results in c1, t == 1 results in c2
489 | func (c1 Color) BlendLinearRgb(c2 Color, t float64) Color {
490 | r1, g1, b1 := c1.LinearRgb()
491 | r2, g2, b2 := c2.LinearRgb()
492 | return LinearRgb(
493 | r1+t*(r2-r1),
494 | g1+t*(g2-g1),
495 | b1+t*(b2-b1),
496 | )
497 | }
498 |
499 | /// XYZ ///
500 | ///////////
501 | // http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/
502 |
503 | func (col Color) Xyz() (x, y, z float64) {
504 | return LinearRgbToXyz(col.LinearRgb())
505 | }
506 |
507 | func Xyz(x, y, z float64) Color {
508 | return LinearRgb(XyzToLinearRgb(x, y, z))
509 | }
510 |
511 | /// xyY ///
512 | ///////////
513 | // http://www.brucelindbloom.com/Eqn_XYZ_to_xyY.html
514 |
515 | // Well, the name is bad, since it's xyY but Golang needs me to start with a
516 | // capital letter to make the method public.
517 | func XyzToXyy(X, Y, Z float64) (x, y, Yout float64) {
518 | return XyzToXyyWhiteRef(X, Y, Z, D65)
519 | }
520 |
521 | func XyzToXyyWhiteRef(X, Y, Z float64, wref [3]float64) (x, y, Yout float64) {
522 | Yout = Y
523 | N := X + Y + Z
524 | if math.Abs(N) < 1e-14 {
525 | // When we have black, Bruce Lindbloom recommends to use
526 | // the reference white's chromacity for x and y.
527 | x = wref[0] / (wref[0] + wref[1] + wref[2])
528 | y = wref[1] / (wref[0] + wref[1] + wref[2])
529 | } else {
530 | x = X / N
531 | y = Y / N
532 | }
533 | return
534 | }
535 |
536 | func XyyToXyz(x, y, Y float64) (X, Yout, Z float64) {
537 | Yout = Y
538 |
539 | if -1e-14 < y && y < 1e-14 {
540 | X = 0.0
541 | Z = 0.0
542 | } else {
543 | X = Y / y * x
544 | Z = Y / y * (1.0 - x - y)
545 | }
546 |
547 | return
548 | }
549 |
550 | // Converts the given color to CIE xyY space using D65 as reference white.
551 | // (Note that the reference white is only used for black input.)
552 | // x, y and Y are in [0..1]
553 | func (col Color) Xyy() (x, y, Y float64) {
554 | return XyzToXyy(col.Xyz())
555 | }
556 |
557 | // Converts the given color to CIE xyY space, taking into account
558 | // a given reference white. (i.e. the monitor's white)
559 | // (Note that the reference white is only used for black input.)
560 | // x, y and Y are in [0..1]
561 | func (col Color) XyyWhiteRef(wref [3]float64) (x, y, Y float64) {
562 | X, Y2, Z := col.Xyz()
563 | return XyzToXyyWhiteRef(X, Y2, Z, wref)
564 | }
565 |
566 | // Generates a color by using data given in CIE xyY space.
567 | // x, y and Y are in [0..1]
568 | func Xyy(x, y, Y float64) Color {
569 | return Xyz(XyyToXyz(x, y, Y))
570 | }
571 |
572 | /// L*a*b* ///
573 | //////////////
574 | // http://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions
575 | // For L*a*b*, we need to L*a*b*<->XYZ->RGB and the first one is device dependent.
576 |
577 | func lab_f(t float64) float64 {
578 | if t > 6.0/29.0*6.0/29.0*6.0/29.0 {
579 | return math.Cbrt(t)
580 | }
581 | return t/3.0*29.0/6.0*29.0/6.0 + 4.0/29.0
582 | }
583 |
584 | func XyzToLab(x, y, z float64) (l, a, b float64) {
585 | // Use D65 white as reference point by default.
586 | // http://www.fredmiranda.com/forum/topic/1035332
587 | // http://en.wikipedia.org/wiki/Standard_illuminant
588 | return XyzToLabWhiteRef(x, y, z, D65)
589 | }
590 |
591 | func XyzToLabWhiteRef(x, y, z float64, wref [3]float64) (l, a, b float64) {
592 | fy := lab_f(y / wref[1])
593 | l = 1.16*fy - 0.16
594 | a = 5.0 * (lab_f(x/wref[0]) - fy)
595 | b = 2.0 * (fy - lab_f(z/wref[2]))
596 | return
597 | }
598 |
599 | func lab_finv(t float64) float64 {
600 | if t > 6.0/29.0 {
601 | return t * t * t
602 | }
603 | return 3.0 * 6.0 / 29.0 * 6.0 / 29.0 * (t - 4.0/29.0)
604 | }
605 |
606 | func LabToXyz(l, a, b float64) (x, y, z float64) {
607 | // D65 white (see above).
608 | return LabToXyzWhiteRef(l, a, b, D65)
609 | }
610 |
611 | func LabToXyzWhiteRef(l, a, b float64, wref [3]float64) (x, y, z float64) {
612 | l2 := (l + 0.16) / 1.16
613 | x = wref[0] * lab_finv(l2+a/5.0)
614 | y = wref[1] * lab_finv(l2)
615 | z = wref[2] * lab_finv(l2-b/2.0)
616 | return
617 | }
618 |
619 | // Converts the given color to CIE L*a*b* space using D65 as reference white.
620 | func (col Color) Lab() (l, a, b float64) {
621 | return XyzToLab(col.Xyz())
622 | }
623 |
624 | // Converts the given color to CIE L*a*b* space, taking into account
625 | // a given reference white. (i.e. the monitor's white)
626 | func (col Color) LabWhiteRef(wref [3]float64) (l, a, b float64) {
627 | x, y, z := col.Xyz()
628 | return XyzToLabWhiteRef(x, y, z, wref)
629 | }
630 |
631 | // Generates a color by using data given in CIE L*a*b* space using D65 as reference white.
632 | // WARNING: many combinations of `l`, `a`, and `b` values do not have corresponding
633 | // valid RGB values, check the FAQ in the README if you're unsure.
634 | func Lab(l, a, b float64) Color {
635 | return Xyz(LabToXyz(l, a, b))
636 | }
637 |
638 | // Generates a color by using data given in CIE L*a*b* space, taking
639 | // into account a given reference white. (i.e. the monitor's white)
640 | func LabWhiteRef(l, a, b float64, wref [3]float64) Color {
641 | return Xyz(LabToXyzWhiteRef(l, a, b, wref))
642 | }
643 |
644 | // DistanceLab is a good measure of visual similarity between two colors!
645 | // A result of 0 would mean identical colors, while a result of 1 or higher
646 | // means the colors differ a lot.
647 | func (c1 Color) DistanceLab(c2 Color) float64 {
648 | l1, a1, b1 := c1.Lab()
649 | l2, a2, b2 := c2.Lab()
650 | return math.Sqrt(sq(l1-l2) + sq(a1-a2) + sq(b1-b2))
651 | }
652 |
653 | // DistanceCIE76 is the same as DistanceLab.
654 | func (c1 Color) DistanceCIE76(c2 Color) float64 {
655 | return c1.DistanceLab(c2)
656 | }
657 |
658 | // Uses the CIE94 formula to calculate color distance. More accurate than
659 | // DistanceLab, but also more work.
660 | func (cl Color) DistanceCIE94(cr Color) float64 {
661 | l1, a1, b1 := cl.Lab()
662 | l2, a2, b2 := cr.Lab()
663 |
664 | // NOTE: Since all those formulas expect L,a,b values 100x larger than we
665 | // have them in this library, we either need to adjust all constants
666 | // in the formula, or convert the ranges of L,a,b before, and then
667 | // scale the distances down again. The latter is less error-prone.
668 | l1, a1, b1 = l1*100.0, a1*100.0, b1*100.0
669 | l2, a2, b2 = l2*100.0, a2*100.0, b2*100.0
670 |
671 | kl := 1.0 // 2.0 for textiles
672 | kc := 1.0
673 | kh := 1.0
674 | k1 := 0.045 // 0.048 for textiles
675 | k2 := 0.015 // 0.014 for textiles.
676 |
677 | deltaL := l1 - l2
678 | c1 := math.Sqrt(sq(a1) + sq(b1))
679 | c2 := math.Sqrt(sq(a2) + sq(b2))
680 | deltaCab := c1 - c2
681 |
682 | // Not taking Sqrt here for stability, and it's unnecessary.
683 | deltaHab2 := sq(a1-a2) + sq(b1-b2) - sq(deltaCab)
684 | sl := 1.0
685 | sc := 1.0 + k1*c1
686 | sh := 1.0 + k2*c1
687 |
688 | vL2 := sq(deltaL / (kl * sl))
689 | vC2 := sq(deltaCab / (kc * sc))
690 | vH2 := deltaHab2 / sq(kh*sh)
691 |
692 | return math.Sqrt(vL2+vC2+vH2) * 0.01 // See above.
693 | }
694 |
695 | // DistanceCIEDE2000 uses the Delta E 2000 formula to calculate color
696 | // distance. It is more expensive but more accurate than both DistanceLab
697 | // and DistanceCIE94.
698 | func (cl Color) DistanceCIEDE2000(cr Color) float64 {
699 | return cl.DistanceCIEDE2000klch(cr, 1.0, 1.0, 1.0)
700 | }
701 |
702 | // DistanceCIEDE2000klch uses the Delta E 2000 formula with custom values
703 | // for the weighting factors kL, kC, and kH.
704 | func (cl Color) DistanceCIEDE2000klch(cr Color, kl, kc, kh float64) float64 {
705 | l1, a1, b1 := cl.Lab()
706 | l2, a2, b2 := cr.Lab()
707 |
708 | // As with CIE94, we scale up the ranges of L,a,b beforehand and scale
709 | // them down again afterwards.
710 | l1, a1, b1 = l1*100.0, a1*100.0, b1*100.0
711 | l2, a2, b2 = l2*100.0, a2*100.0, b2*100.0
712 |
713 | cab1 := math.Sqrt(sq(a1) + sq(b1))
714 | cab2 := math.Sqrt(sq(a2) + sq(b2))
715 | cabmean := (cab1 + cab2) / 2
716 |
717 | g := 0.5 * (1 - math.Sqrt(math.Pow(cabmean, 7)/(math.Pow(cabmean, 7)+math.Pow(25, 7))))
718 | ap1 := (1 + g) * a1
719 | ap2 := (1 + g) * a2
720 | cp1 := math.Sqrt(sq(ap1) + sq(b1))
721 | cp2 := math.Sqrt(sq(ap2) + sq(b2))
722 |
723 | hp1 := 0.0
724 | if b1 != ap1 || ap1 != 0 {
725 | hp1 = math.Atan2(b1, ap1)
726 | if hp1 < 0 {
727 | hp1 += math.Pi * 2
728 | }
729 | hp1 *= 180 / math.Pi
730 | }
731 | hp2 := 0.0
732 | if b2 != ap2 || ap2 != 0 {
733 | hp2 = math.Atan2(b2, ap2)
734 | if hp2 < 0 {
735 | hp2 += math.Pi * 2
736 | }
737 | hp2 *= 180 / math.Pi
738 | }
739 |
740 | deltaLp := l2 - l1
741 | deltaCp := cp2 - cp1
742 | dhp := 0.0
743 | cpProduct := cp1 * cp2
744 | if cpProduct != 0 {
745 | dhp = hp2 - hp1
746 | if dhp > 180 {
747 | dhp -= 360
748 | } else if dhp < -180 {
749 | dhp += 360
750 | }
751 | }
752 | deltaHp := 2 * math.Sqrt(cpProduct) * math.Sin(dhp/2*math.Pi/180)
753 |
754 | lpmean := (l1 + l2) / 2
755 | cpmean := (cp1 + cp2) / 2
756 | hpmean := hp1 + hp2
757 | if cpProduct != 0 {
758 | hpmean /= 2
759 | if math.Abs(hp1-hp2) > 180 {
760 | if hp1+hp2 < 360 {
761 | hpmean += 180
762 | } else {
763 | hpmean -= 180
764 | }
765 | }
766 | }
767 |
768 | t := 1 - 0.17*math.Cos((hpmean-30)*math.Pi/180) + 0.24*math.Cos(2*hpmean*math.Pi/180) + 0.32*math.Cos((3*hpmean+6)*math.Pi/180) - 0.2*math.Cos((4*hpmean-63)*math.Pi/180)
769 | deltaTheta := 30 * math.Exp(-sq((hpmean-275)/25))
770 | rc := 2 * math.Sqrt(math.Pow(cpmean, 7)/(math.Pow(cpmean, 7)+math.Pow(25, 7)))
771 | sl := 1 + (0.015*sq(lpmean-50))/math.Sqrt(20+sq(lpmean-50))
772 | sc := 1 + 0.045*cpmean
773 | sh := 1 + 0.015*cpmean*t
774 | rt := -math.Sin(2*deltaTheta*math.Pi/180) * rc
775 |
776 | return math.Sqrt(sq(deltaLp/(kl*sl))+sq(deltaCp/(kc*sc))+sq(deltaHp/(kh*sh))+rt*(deltaCp/(kc*sc))*(deltaHp/(kh*sh))) * 0.01
777 | }
778 |
779 | // BlendLab blends two colors in the L*a*b* color-space, which should result in a smoother blend.
780 | // t == 0 results in c1, t == 1 results in c2
781 | func (c1 Color) BlendLab(c2 Color, t float64) Color {
782 | l1, a1, b1 := c1.Lab()
783 | l2, a2, b2 := c2.Lab()
784 | return Lab(l1+t*(l2-l1),
785 | a1+t*(a2-a1),
786 | b1+t*(b2-b1))
787 | }
788 |
789 | /// L*u*v* ///
790 | //////////////
791 | // http://en.wikipedia.org/wiki/CIELUV#XYZ_.E2.86.92_CIELUV_and_CIELUV_.E2.86.92_XYZ_conversions
792 | // For L*u*v*, we need to L*u*v*<->XYZ<->RGB and the first one is device dependent.
793 |
794 | func XyzToLuv(x, y, z float64) (l, a, b float64) {
795 | // Use D65 white as reference point by default.
796 | // http://www.fredmiranda.com/forum/topic/1035332
797 | // http://en.wikipedia.org/wiki/Standard_illuminant
798 | return XyzToLuvWhiteRef(x, y, z, D65)
799 | }
800 |
801 | func XyzToLuvWhiteRef(x, y, z float64, wref [3]float64) (l, u, v float64) {
802 | if y/wref[1] <= 6.0/29.0*6.0/29.0*6.0/29.0 {
803 | l = y / wref[1] * (29.0 / 3.0 * 29.0 / 3.0 * 29.0 / 3.0) / 100.0
804 | } else {
805 | l = 1.16*math.Cbrt(y/wref[1]) - 0.16
806 | }
807 | ubis, vbis := xyz_to_uv(x, y, z)
808 | un, vn := xyz_to_uv(wref[0], wref[1], wref[2])
809 | u = 13.0 * l * (ubis - un)
810 | v = 13.0 * l * (vbis - vn)
811 | return
812 | }
813 |
814 | // For this part, we do as R's graphics.hcl does, not as wikipedia does.
815 | // Or is it the same?
816 | func xyz_to_uv(x, y, z float64) (u, v float64) {
817 | denom := x + 15.0*y + 3.0*z
818 | if denom == 0.0 {
819 | u, v = 0.0, 0.0
820 | } else {
821 | u = 4.0 * x / denom
822 | v = 9.0 * y / denom
823 | }
824 | return
825 | }
826 |
827 | func LuvToXyz(l, u, v float64) (x, y, z float64) {
828 | // D65 white (see above).
829 | return LuvToXyzWhiteRef(l, u, v, D65)
830 | }
831 |
832 | func LuvToXyzWhiteRef(l, u, v float64, wref [3]float64) (x, y, z float64) {
833 | // y = wref[1] * lab_finv((l + 0.16) / 1.16)
834 | if l <= 0.08 {
835 | y = wref[1] * l * 100.0 * 3.0 / 29.0 * 3.0 / 29.0 * 3.0 / 29.0
836 | } else {
837 | y = wref[1] * cub((l+0.16)/1.16)
838 | }
839 | un, vn := xyz_to_uv(wref[0], wref[1], wref[2])
840 | if l != 0.0 {
841 | ubis := u/(13.0*l) + un
842 | vbis := v/(13.0*l) + vn
843 | x = y * 9.0 * ubis / (4.0 * vbis)
844 | z = y * (12.0 - 3.0*ubis - 20.0*vbis) / (4.0 * vbis)
845 | } else {
846 | x, y = 0.0, 0.0
847 | }
848 | return
849 | }
850 |
851 | // Converts the given color to CIE L*u*v* space using D65 as reference white.
852 | // L* is in [0..1] and both u* and v* are in about [-1..1]
853 | func (col Color) Luv() (l, u, v float64) {
854 | return XyzToLuv(col.Xyz())
855 | }
856 |
857 | // Converts the given color to CIE L*u*v* space, taking into account
858 | // a given reference white. (i.e. the monitor's white)
859 | // L* is in [0..1] and both u* and v* are in about [-1..1]
860 | func (col Color) LuvWhiteRef(wref [3]float64) (l, u, v float64) {
861 | x, y, z := col.Xyz()
862 | return XyzToLuvWhiteRef(x, y, z, wref)
863 | }
864 |
865 | // Generates a color by using data given in CIE L*u*v* space using D65 as reference white.
866 | // L* is in [0..1] and both u* and v* are in about [-1..1]
867 | // WARNING: many combinations of `l`, `u`, and `v` values do not have corresponding
868 | // valid RGB values, check the FAQ in the README if you're unsure.
869 | func Luv(l, u, v float64) Color {
870 | return Xyz(LuvToXyz(l, u, v))
871 | }
872 |
873 | // Generates a color by using data given in CIE L*u*v* space, taking
874 | // into account a given reference white. (i.e. the monitor's white)
875 | // L* is in [0..1] and both u* and v* are in about [-1..1]
876 | func LuvWhiteRef(l, u, v float64, wref [3]float64) Color {
877 | return Xyz(LuvToXyzWhiteRef(l, u, v, wref))
878 | }
879 |
880 | // DistanceLuv is a good measure of visual similarity between two colors!
881 | // A result of 0 would mean identical colors, while a result of 1 or higher
882 | // means the colors differ a lot.
883 | func (c1 Color) DistanceLuv(c2 Color) float64 {
884 | l1, u1, v1 := c1.Luv()
885 | l2, u2, v2 := c2.Luv()
886 | return math.Sqrt(sq(l1-l2) + sq(u1-u2) + sq(v1-v2))
887 | }
888 |
889 | // BlendLuv blends two colors in the CIE-L*u*v* color-space, which should result in a smoother blend.
890 | // t == 0 results in c1, t == 1 results in c2
891 | func (c1 Color) BlendLuv(c2 Color, t float64) Color {
892 | l1, u1, v1 := c1.Luv()
893 | l2, u2, v2 := c2.Luv()
894 | return Luv(l1+t*(l2-l1),
895 | u1+t*(u2-u1),
896 | v1+t*(v2-v1))
897 | }
898 |
899 | /// HCL ///
900 | ///////////
901 | // HCL is nothing else than L*a*b* in cylindrical coordinates!
902 | // (this was wrong on English wikipedia, I fixed it, let's hope the fix stays.)
903 | // But it is widely popular since it is a "correct HSV"
904 | // http://www.hunterlab.com/appnotes/an09_96a.pdf
905 |
906 | // Converts the given color to HCL space using D65 as reference white.
907 | // H values are in [0..360], C and L values are in [0..1] although C can overshoot 1.0
908 | func (col Color) Hcl() (h, c, l float64) {
909 | return col.HclWhiteRef(D65)
910 | }
911 |
912 | func LabToHcl(L, a, b float64) (h, c, l float64) {
913 | // Oops, floating point workaround necessary if a ~= b and both are very small (i.e. almost zero).
914 | if math.Abs(b-a) > 1e-4 && math.Abs(a) > 1e-4 {
915 | h = math.Mod(57.29577951308232087721*math.Atan2(b, a)+360.0, 360.0) // Rad2Deg
916 | } else {
917 | h = 0.0
918 | }
919 | c = math.Sqrt(sq(a) + sq(b))
920 | l = L
921 | return
922 | }
923 |
924 | // Converts the given color to HCL space, taking into account
925 | // a given reference white. (i.e. the monitor's white)
926 | // H values are in [0..360], C and L values are in [0..1]
927 | func (col Color) HclWhiteRef(wref [3]float64) (h, c, l float64) {
928 | L, a, b := col.LabWhiteRef(wref)
929 | return LabToHcl(L, a, b)
930 | }
931 |
932 | // Generates a color by using data given in HCL space using D65 as reference white.
933 | // H values are in [0..360], C and L values are in [0..1]
934 | // WARNING: many combinations of `h`, `c`, and `l` values do not have corresponding
935 | // valid RGB values, check the FAQ in the README if you're unsure.
936 | func Hcl(h, c, l float64) Color {
937 | return HclWhiteRef(h, c, l, D65)
938 | }
939 |
940 | func HclToLab(h, c, l float64) (L, a, b float64) {
941 | H := 0.01745329251994329576 * h // Deg2Rad
942 | a = c * math.Cos(H)
943 | b = c * math.Sin(H)
944 | L = l
945 | return
946 | }
947 |
948 | // Generates a color by using data given in HCL space, taking
949 | // into account a given reference white. (i.e. the monitor's white)
950 | // H values are in [0..360], C and L values are in [0..1]
951 | func HclWhiteRef(h, c, l float64, wref [3]float64) Color {
952 | L, a, b := HclToLab(h, c, l)
953 | return LabWhiteRef(L, a, b, wref)
954 | }
955 |
956 | // BlendHcl blends two colors in the CIE-L*C*h° color-space, which should result in a smoother blend.
957 | // t == 0 results in c1, t == 1 results in c2
958 | func (col1 Color) BlendHcl(col2 Color, t float64) Color {
959 | h1, c1, l1 := col1.Hcl()
960 | h2, c2, l2 := col2.Hcl()
961 |
962 | // https://github.com/lucasb-eyer/go-colorful/pull/60
963 | if c1 <= 0.00015 && c2 >= 0.00015 {
964 | h1 = h2
965 | } else if c2 <= 0.00015 && c1 >= 0.00015 {
966 | h2 = h1
967 | }
968 |
969 | // We know that h are both in [0..360]
970 | return Hcl(interp_angle(h1, h2, t), c1+t*(c2-c1), l1+t*(l2-l1)).Clamped()
971 | }
972 |
973 | // LuvLch
974 |
975 | // Converts the given color to LuvLCh space using D65 as reference white.
976 | // h values are in [0..360], C and L values are in [0..1] although C can overshoot 1.0
977 | func (col Color) LuvLCh() (l, c, h float64) {
978 | return col.LuvLChWhiteRef(D65)
979 | }
980 |
981 | func LuvToLuvLCh(L, u, v float64) (l, c, h float64) {
982 | // Oops, floating point workaround necessary if u ~= v and both are very small (i.e. almost zero).
983 | if math.Abs(v-u) > 1e-4 && math.Abs(u) > 1e-4 {
984 | h = math.Mod(57.29577951308232087721*math.Atan2(v, u)+360.0, 360.0) // Rad2Deg
985 | } else {
986 | h = 0.0
987 | }
988 | l = L
989 | c = math.Sqrt(sq(u) + sq(v))
990 | return
991 | }
992 |
993 | // Converts the given color to LuvLCh space, taking into account
994 | // a given reference white. (i.e. the monitor's white)
995 | // h values are in [0..360], c and l values are in [0..1]
996 | func (col Color) LuvLChWhiteRef(wref [3]float64) (l, c, h float64) {
997 | return LuvToLuvLCh(col.LuvWhiteRef(wref))
998 | }
999 |
1000 | // Generates a color by using data given in LuvLCh space using D65 as reference white.
1001 | // h values are in [0..360], C and L values are in [0..1]
1002 | // WARNING: many combinations of `l`, `c`, and `h` values do not have corresponding
1003 | // valid RGB values, check the FAQ in the README if you're unsure.
1004 | func LuvLCh(l, c, h float64) Color {
1005 | return LuvLChWhiteRef(l, c, h, D65)
1006 | }
1007 |
1008 | func LuvLChToLuv(l, c, h float64) (L, u, v float64) {
1009 | H := 0.01745329251994329576 * h // Deg2Rad
1010 | u = c * math.Cos(H)
1011 | v = c * math.Sin(H)
1012 | L = l
1013 | return
1014 | }
1015 |
1016 | // Generates a color by using data given in LuvLCh space, taking
1017 | // into account a given reference white. (i.e. the monitor's white)
1018 | // h values are in [0..360], C and L values are in [0..1]
1019 | func LuvLChWhiteRef(l, c, h float64, wref [3]float64) Color {
1020 | L, u, v := LuvLChToLuv(l, c, h)
1021 | return LuvWhiteRef(L, u, v, wref)
1022 | }
1023 |
1024 | // BlendLuvLCh blends two colors in the cylindrical CIELUV color space.
1025 | // t == 0 results in c1, t == 1 results in c2
1026 | func (col1 Color) BlendLuvLCh(col2 Color, t float64) Color {
1027 | l1, c1, h1 := col1.LuvLCh()
1028 | l2, c2, h2 := col2.LuvLCh()
1029 |
1030 | // We know that h are both in [0..360]
1031 | return LuvLCh(l1+t*(l2-l1), c1+t*(c2-c1), interp_angle(h1, h2, t))
1032 | }
1033 |
1034 | /// OkLab ///
1035 | ///////////
1036 |
1037 | func (col Color) OkLab() (l, a, b float64) {
1038 | return XyzToOkLab(col.Xyz())
1039 | }
1040 |
1041 | func OkLab(l, a, b float64) Color {
1042 | return Xyz(OkLabToXyz(l, a, b))
1043 | }
1044 |
1045 | func XyzToOkLab(x, y, z float64) (l, a, b float64) {
1046 | l_ := math.Cbrt(0.8189330101*x + 0.3618667424*y - 0.1288597137*z)
1047 | m_ := math.Cbrt(0.0329845436*x + 0.9293118715*y + 0.0361456387*z)
1048 | s_ := math.Cbrt(0.0482003018*x + 0.2643662691*y + 0.6338517070*z)
1049 | l = 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_
1050 | a = 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_
1051 | b = 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_
1052 | return
1053 | }
1054 |
1055 | func OkLabToXyz(l, a, b float64) (x, y, z float64) {
1056 | l_ := 0.9999999984505196*l + 0.39633779217376774*a + 0.2158037580607588*b
1057 | m_ := 1.0000000088817607*l - 0.10556134232365633*a - 0.0638541747717059*b
1058 | s_ := 1.0000000546724108*l - 0.08948418209496574*a - 1.2914855378640917*b
1059 |
1060 | ll := math.Pow(l_, 3)
1061 | m := math.Pow(m_, 3)
1062 | s := math.Pow(s_, 3)
1063 |
1064 | x = 1.2268798733741557*ll - 0.5578149965554813*m + 0.28139105017721594*s
1065 | y = -0.04057576262431372*ll + 1.1122868293970594*m - 0.07171106666151696*s
1066 | z = -0.07637294974672142*ll - 0.4214933239627916*m + 1.5869240244272422*s
1067 |
1068 | return
1069 | }
1070 |
1071 | // BlendOkLab blends two colors in the OkLab color-space, which should result in a better blend (even compared to BlendLab).
1072 | func (c1 Color) BlendOkLab(c2 Color, t float64) Color {
1073 | l1, a1, b1 := c1.OkLab()
1074 | l2, a2, b2 := c2.OkLab()
1075 | return OkLab(l1+t*(l2-l1),
1076 | a1+t*(a2-a1),
1077 | b1+t*(b2-b1))
1078 | }
1079 |
1080 | /// OkLch ///
1081 | ///////////
1082 |
1083 | func (col Color) OkLch() (l, c, h float64) {
1084 | return OkLabToOkLch(col.OkLab())
1085 | }
1086 |
1087 | func OkLch(l, c, h float64) Color {
1088 | return Xyz(OkLchToXyz(l, c, h))
1089 | }
1090 |
1091 | func XyzToOkLch(x, y, z float64) (float64, float64, float64) {
1092 | l, c, h := OkLabToOkLch(XyzToOkLab(x, y, z))
1093 | return l, c, h
1094 | }
1095 |
1096 | func OkLchToXyz(l, c, h float64) (float64, float64, float64) {
1097 | x, y, z := OkLabToXyz(OkLchToOkLab(l, c, h))
1098 | return x, y, z
1099 | }
1100 |
1101 | func OkLabToOkLch(l, a, b float64) (float64, float64, float64) {
1102 | c := math.Sqrt((a * a) + (b * b))
1103 | h := math.Atan2(b, a)
1104 | if h < 0 {
1105 | h += 2 * math.Pi
1106 | }
1107 |
1108 | return l, c, h * 180 / math.Pi
1109 | }
1110 |
1111 | func OkLchToOkLab(l, c, h float64) (float64, float64, float64) {
1112 | h *= math.Pi / 180
1113 | a := c * math.Cos(h)
1114 | b := c * math.Sin(h)
1115 | return l, a, b
1116 | }
1117 |
1118 | // BlendOkLch blends two colors in the OkLch color-space, which should result in a better blend (even compared to BlendHcl).
1119 | func (col1 Color) BlendOkLch(col2 Color, t float64) Color {
1120 | l1, c1, h1 := col1.OkLch()
1121 | l2, c2, h2 := col2.OkLch()
1122 |
1123 | // https://github.com/lucasb-eyer/go-colorful/pull/60
1124 | if c1 <= 0.00015 && c2 >= 0.00015 {
1125 | h1 = h2
1126 | } else if c2 <= 0.00015 && c1 >= 0.00015 {
1127 | h2 = h1
1128 | }
1129 |
1130 | // We know that h are both in [0..360]
1131 | return OkLch(l1+t*(l2-l1), c1+t*(c2-c1), interp_angle(h1, h2, t)).Clamped()
1132 | }
1133 |
--------------------------------------------------------------------------------
/colors_test.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import (
4 | "image/color"
5 | "math"
6 | "math/rand"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | var bench_result float64 // Dummy for benchmarks to avoid optimization
12 |
13 | // Checks whether the relative error is below eps
14 | func almosteq_eps(v1, v2, eps float64) bool {
15 | if math.Abs(v1) > delta {
16 | return math.Abs((v1-v2)/v1) < eps
17 | }
18 | return true
19 | }
20 |
21 | // Checks whether the relative error is below the 8bit RGB delta, which should be good enough.
22 | const delta = 1.0 / 256.0
23 |
24 | func almosteq(v1, v2 float64) bool {
25 | return almosteq_eps(v1, v2, delta)
26 | }
27 |
28 | // Note: the XYZ, L*a*b*, etc. are using D65 white and D50 white if postfixed by "50".
29 | // See http://www.brucelindbloom.com/index.html?ColorCalcHelp.html
30 | // For d50 white, no "adaptation" and the sRGB model are used in colorful
31 | // HCL values form http://www.easyrgb.com/index.php?X=CALC and missing ones hand-computed from lab ones
32 | var vals = []struct {
33 | c Color
34 | hsl [3]float64
35 | hsv [3]float64
36 | hex string
37 | xyz [3]float64
38 | xyy [3]float64
39 | lab [3]float64
40 | lab50 [3]float64
41 | luv [3]float64
42 | luv50 [3]float64
43 | hcl [3]float64
44 | hcl50 [3]float64
45 | rgba [4]uint32
46 | rgb255 [3]uint8
47 | }{
48 | {Color{1.0, 1.0, 1.0}, [3]float64{0.0, 0.0, 1.00}, [3]float64{0.0, 0.0, 1.0}, "#ffffff", [3]float64{0.950470, 1.000000, 1.088830}, [3]float64{0.312727, 0.329023, 1.000000}, [3]float64{1.000000, 0.000000, 0.000000}, [3]float64{1.000000, -0.023881, -0.193622}, [3]float64{1.00000, 0.00000, 0.00000}, [3]float64{1.00000, -0.14716, -0.25658}, [3]float64{0.0000, 0.000000, 1.000000}, [3]float64{262.9688, 0.195089, 1.000000}, [4]uint32{65535, 65535, 65535, 65535}, [3]uint8{255, 255, 255}},
49 | {Color{0.5, 1.0, 1.0}, [3]float64{180.0, 1.0, 0.75}, [3]float64{180.0, 0.5, 1.0}, "#80ffff", [3]float64{0.626296, 0.832848, 1.073634}, [3]float64{0.247276, 0.328828, 0.832848}, [3]float64{0.931390, -0.353319, -0.108946}, [3]float64{0.931390, -0.374100, -0.301663}, [3]float64{0.93139, -0.53909, -0.11630}, [3]float64{0.93139, -0.67615, -0.35528}, [3]float64{197.1371, 0.369735, 0.931390}, [3]float64{218.8817, 0.480574, 0.931390}, [4]uint32{32768, 65535, 65535, 65535}, [3]uint8{128, 255, 255}},
50 | {Color{1.0, 0.5, 1.0}, [3]float64{300.0, 1.0, 0.75}, [3]float64{300.0, 0.5, 1.0}, "#ff80ff", [3]float64{0.669430, 0.437920, 0.995150}, [3]float64{0.318397, 0.208285, 0.437920}, [3]float64{0.720892, 0.651673, -0.422133}, [3]float64{0.720892, 0.630425, -0.610035}, [3]float64{0.72089, 0.60047, -0.77626}, [3]float64{0.72089, 0.49438, -0.96123}, [3]float64{327.0661, 0.776450, 0.720892}, [3]float64{315.9417, 0.877257, 0.720892}, [4]uint32{65535, 32768, 65535, 65535}, [3]uint8{255, 128, 255}},
51 | {Color{1.0, 1.0, 0.5}, [3]float64{60.0, 1.0, 0.75}, [3]float64{60.0, 0.5, 1.0}, "#ffff80", [3]float64{0.808654, 0.943273, 0.341930}, [3]float64{0.386203, 0.450496, 0.943273}, [3]float64{0.977637, -0.165795, 0.602017}, [3]float64{0.977637, -0.188424, 0.470410}, [3]float64{0.97764, 0.05759, 0.79816}, [3]float64{0.97764, -0.08628, 0.54731}, [3]float64{105.3975, 0.624430, 0.977637}, [3]float64{111.8287, 0.506743, 0.977637}, [4]uint32{65535, 65535, 32768, 65535}, [3]uint8{255, 255, 128}},
52 | {Color{0.5, 0.5, 1.0}, [3]float64{240.0, 1.0, 0.75}, [3]float64{240.0, 0.5, 1.0}, "#8080ff", [3]float64{0.345256, 0.270768, 0.979954}, [3]float64{0.216329, 0.169656, 0.270768}, [3]float64{0.590453, 0.332846, -0.637099}, [3]float64{0.590453, 0.315806, -0.824040}, [3]float64{0.59045, -0.07568, -1.04877}, [3]float64{0.59045, -0.16257, -1.20027}, [3]float64{297.5843, 0.718805, 0.590453}, [3]float64{290.9689, 0.882482, 0.590453}, [4]uint32{32768, 32768, 65535, 65535}, [3]uint8{128, 128, 255}},
53 | {Color{1.0, 0.5, 0.5}, [3]float64{0.0, 1.0, 0.75}, [3]float64{0.0, 0.5, 1.0}, "#ff8080", [3]float64{0.527613, 0.381193, 0.248250}, [3]float64{0.455996, 0.329451, 0.381193}, [3]float64{0.681085, 0.483884, 0.228328}, [3]float64{0.681085, 0.464258, 0.110043}, [3]float64{0.68108, 0.92148, 0.19879}, [3]float64{0.68106, 0.82106, 0.02393}, [3]float64{25.2610, 0.535049, 0.681085}, [3]float64{13.3347, 0.477121, 0.681085}, [4]uint32{65535, 32768, 32768, 65535}, [3]uint8{255, 128, 128}},
54 | {Color{0.5, 1.0, 0.5}, [3]float64{120.0, 1.0, 0.75}, [3]float64{120.0, 0.5, 1.0}, "#80ff80", [3]float64{0.484480, 0.776121, 0.326734}, [3]float64{0.305216, 0.488946, 0.776121}, [3]float64{0.906026, -0.600870, 0.498993}, [3]float64{0.906026, -0.619946, 0.369365}, [3]float64{0.90603, -0.58869, 0.76102}, [3]float64{0.90603, -0.72202, 0.52855}, [3]float64{140.2920, 0.781050, 0.906026}, [3]float64{149.2134, 0.721640, 0.906026}, [4]uint32{32768, 65535, 32768, 65535}, [3]uint8{128, 255, 128}},
55 | {Color{0.5, 0.5, 0.5}, [3]float64{0.0, 0.0, 0.50}, [3]float64{0.0, 0.0, 0.5}, "#808080", [3]float64{0.203440, 0.214041, 0.233054}, [3]float64{0.312727, 0.329023, 0.214041}, [3]float64{0.533890, 0.000000, 0.000000}, [3]float64{0.533890, -0.014285, -0.115821}, [3]float64{0.53389, 0.00000, 0.00000}, [3]float64{0.53389, -0.07857, -0.13699}, [3]float64{0.0000, 0.000000, 0.533890}, [3]float64{262.9688, 0.116699, 0.533890}, [4]uint32{32768, 32768, 32768, 65535}, [3]uint8{128, 128, 128}},
56 | {Color{0.0, 1.0, 1.0}, [3]float64{180.0, 1.0, 0.50}, [3]float64{180.0, 1.0, 1.0}, "#00ffff", [3]float64{0.538014, 0.787327, 1.069496}, [3]float64{0.224656, 0.328760, 0.787327}, [3]float64{0.911132, -0.480875, -0.141312}, [3]float64{0.911132, -0.500630, -0.333781}, [3]float64{0.91113, -0.70477, -0.15204}, [3]float64{0.91113, -0.83886, -0.38582}, [3]float64{196.3762, 0.501209, 0.911132}, [3]float64{213.6923, 0.601698, 0.911132}, [4]uint32{0, 65535, 65535, 65535}, [3]uint8{0, 255, 255}},
57 | {Color{1.0, 0.0, 1.0}, [3]float64{300.0, 1.0, 0.50}, [3]float64{300.0, 1.0, 1.0}, "#ff00ff", [3]float64{0.592894, 0.284848, 0.969638}, [3]float64{0.320938, 0.154190, 0.284848}, [3]float64{0.603242, 0.982343, -0.608249}, [3]float64{0.603242, 0.961939, -0.794531}, [3]float64{0.60324, 0.84071, -1.08683}, [3]float64{0.60324, 0.75194, -1.24161}, [3]float64{328.2350, 1.155407, 0.603242}, [3]float64{320.4444, 1.247640, 0.603242}, [4]uint32{65535, 0, 65535, 65535}, [3]uint8{255, 0, 255}},
58 | {Color{1.0, 1.0, 0.0}, [3]float64{60.0, 1.0, 0.50}, [3]float64{60.0, 1.0, 1.0}, "#ffff00", [3]float64{0.770033, 0.927825, 0.138526}, [3]float64{0.419320, 0.505246, 0.927825}, [3]float64{0.971393, -0.215537, 0.944780}, [3]float64{0.971393, -0.237800, 0.847398}, [3]float64{0.97139, 0.07706, 1.06787}, [3]float64{0.97139, -0.06590, 0.81862}, [3]float64{102.8512, 0.969054, 0.971393}, [3]float64{105.6754, 0.880131, 0.971393}, [4]uint32{65535, 65535, 0, 65535}, [3]uint8{255, 255, 0}},
59 | {Color{0.0, 0.0, 1.0}, [3]float64{240.0, 1.0, 0.50}, [3]float64{240.0, 1.0, 1.0}, "#0000ff", [3]float64{0.180437, 0.072175, 0.950304}, [3]float64{0.150000, 0.060000, 0.072175}, [3]float64{0.322970, 0.791875, -1.078602}, [3]float64{0.322970, 0.778150, -1.263638}, [3]float64{0.32297, -0.09405, -1.30342}, [3]float64{0.32297, -0.14158, -1.38629}, [3]float64{306.2849, 1.338076, 0.322970}, [3]float64{301.6248, 1.484014, 0.322970}, [4]uint32{0, 0, 65535, 65535}, [3]uint8{0, 0, 255}},
60 | {Color{0.0, 1.0, 0.0}, [3]float64{120.0, 1.0, 0.50}, [3]float64{120.0, 1.0, 1.0}, "#00ff00", [3]float64{0.357576, 0.715152, 0.119192}, [3]float64{0.300000, 0.600000, 0.715152}, [3]float64{0.877347, -0.861827, 0.831793}, [3]float64{0.877347, -0.879067, 0.739170}, [3]float64{0.87735, -0.83078, 1.07398}, [3]float64{0.87735, -0.95989, 0.84887}, [3]float64{136.0160, 1.197759, 0.877347}, [3]float64{139.9409, 1.148534, 0.877347}, [4]uint32{0, 65535, 0, 65535}, [3]uint8{0, 255, 0}},
61 | {Color{1.0, 0.0, 0.0}, [3]float64{0.0, 1.0, 0.50}, [3]float64{0.0, 1.0, 1.0}, "#ff0000", [3]float64{0.412456, 0.212673, 0.019334}, [3]float64{0.640000, 0.330000, 0.212673}, [3]float64{0.532408, 0.800925, 0.672032}, [3]float64{0.532408, 0.782845, 0.621518}, [3]float64{0.53241, 1.75015, 0.37756}, [3]float64{0.53241, 1.67180, 0.24096}, [3]float64{39.9990, 1.045518, 0.532408}, [3]float64{38.4469, 0.999566, 0.532408}, [4]uint32{65535, 0, 0, 65535}, [3]uint8{255, 0, 0}},
62 | {Color{0.0, 0.0, 0.0}, [3]float64{0.0, 0.0, 0.00}, [3]float64{0.0, 0.0, 0.0}, "#000000", [3]float64{0.000000, 0.000000, 0.000000}, [3]float64{0.312727, 0.329023, 0.000000}, [3]float64{0.000000, 0.000000, 0.000000}, [3]float64{0.000000, 0.000000, 0.000000}, [3]float64{0.00000, 0.00000, 0.00000}, [3]float64{0.00000, 0.00000, 0.00000}, [3]float64{0.0000, 0.000000, 0.000000}, [3]float64{0.0000, 0.000000, 0.000000}, [4]uint32{0, 0, 0, 65535}, [3]uint8{0, 0, 0}},
63 | }
64 |
65 | // For testing short-hex values, since the above contains colors which don't
66 | // have corresponding short hexes.
67 | var shorthexvals = []struct {
68 | c Color
69 | hex string
70 | }{
71 | {Color{1.0, 1.0, 1.0}, "#fff"},
72 | {Color{0.6, 1.0, 1.0}, "#9ff"},
73 | {Color{1.0, 0.6, 1.0}, "#f9f"},
74 | {Color{1.0, 1.0, 0.6}, "#ff9"},
75 | {Color{0.6, 0.6, 1.0}, "#99f"},
76 | {Color{1.0, 0.6, 0.6}, "#f99"},
77 | {Color{0.6, 1.0, 0.6}, "#9f9"},
78 | {Color{0.6, 0.6, 0.6}, "#999"},
79 | {Color{0.0, 1.0, 1.0}, "#0ff"},
80 | {Color{1.0, 0.0, 1.0}, "#f0f"},
81 | {Color{1.0, 1.0, 0.0}, "#ff0"},
82 | {Color{0.0, 0.0, 1.0}, "#00f"},
83 | {Color{0.0, 1.0, 0.0}, "#0f0"},
84 | {Color{1.0, 0.0, 0.0}, "#f00"},
85 | {Color{0.0, 0.0, 0.0}, "#000"},
86 | }
87 |
88 | /// RGBA ///
89 | ////////////
90 |
91 | func TestRGBAConversion(t *testing.T) {
92 | for i, tt := range vals {
93 | r, g, b, a := tt.c.RGBA()
94 | if r != tt.rgba[0] || g != tt.rgba[1] || b != tt.rgba[2] || a != tt.rgba[3] {
95 | t.Errorf("%v. %v.RGBA() => (%v), want %v (delta %v)", i, tt.c, []uint32{r, g, b, a}, tt.rgba, delta)
96 | }
97 | }
98 | }
99 |
100 | /// RGB255 ///
101 | ////////////
102 |
103 | func TestRGB255Conversion(t *testing.T) {
104 | for i, tt := range vals {
105 | r, g, b := tt.c.RGB255()
106 | if r != tt.rgb255[0] || g != tt.rgb255[1] || b != tt.rgb255[2] {
107 | t.Errorf("%v. %v.RGB255() => (%v), want %v (delta %v)", i, tt.c, []uint8{r, g, b}, tt.rgb255, delta)
108 | }
109 | }
110 | }
111 |
112 | /// HSV ///
113 | ///////////
114 |
115 | func TestHsvCreation(t *testing.T) {
116 | for i, tt := range vals {
117 | c := Hsv(tt.hsv[0], tt.hsv[1], tt.hsv[2])
118 | if !c.AlmostEqualRgb(tt.c) {
119 | t.Errorf("%v. Hsv(%v) => (%v), want %v (delta %v)", i, tt.hsv, c, tt.c, delta)
120 | }
121 | }
122 | }
123 |
124 | func TestHsvConversion(t *testing.T) {
125 | for i, tt := range vals {
126 | h, s, v := tt.c.Hsv()
127 | if !almosteq(h, tt.hsv[0]) || !almosteq(s, tt.hsv[1]) || !almosteq(v, tt.hsv[2]) {
128 | t.Errorf("%v. %v.Hsv() => (%v), want %v (delta %v)", i, tt.c, []float64{h, s, v}, tt.hsv, delta)
129 | }
130 | }
131 | }
132 |
133 | /// HSL ///
134 | ///////////
135 |
136 | func TestHslCreation(t *testing.T) {
137 | for i, tt := range vals {
138 | c := Hsl(tt.hsl[0], tt.hsl[1], tt.hsl[2])
139 | if !c.AlmostEqualRgb(tt.c) {
140 | t.Errorf("%v. Hsl(%v) => (%v), want %v (delta %v)", i, tt.hsl, c, tt.c, delta)
141 | }
142 | }
143 | }
144 |
145 | func TestHslConversion(t *testing.T) {
146 | for i, tt := range vals {
147 | h, s, l := tt.c.Hsl()
148 | if !almosteq(h, tt.hsl[0]) || !almosteq(s, tt.hsl[1]) || !almosteq(l, tt.hsl[2]) {
149 | t.Errorf("%v. %v.Hsl() => (%v), want %v (delta %v)", i, tt.c, []float64{h, s, l}, tt.hsl, delta)
150 | }
151 | }
152 | }
153 |
154 | /// Hex ///
155 | ///////////
156 |
157 | func TestHexCreation(t *testing.T) {
158 | for i, tt := range vals {
159 | c, err := Hex(tt.hex)
160 | if err != nil || !c.AlmostEqualRgb(tt.c) {
161 | t.Errorf("%v. Hex(%v) => (%v), want %v (delta %v)", i, tt.hex, c, tt.c, delta)
162 | }
163 | }
164 | }
165 |
166 | func TestHEXCreation(t *testing.T) {
167 | for i, tt := range vals {
168 | c, err := Hex(strings.ToUpper(tt.hex))
169 | if err != nil || !c.AlmostEqualRgb(tt.c) {
170 | t.Errorf("%v. HEX(%v) => (%v), want %v (delta %v)", i, strings.ToUpper(tt.hex), c, tt.c, delta)
171 | }
172 | }
173 | }
174 |
175 | func TestShortHexCreation(t *testing.T) {
176 | for i, tt := range shorthexvals {
177 | c, err := Hex(tt.hex)
178 | if err != nil || !c.AlmostEqualRgb(tt.c) {
179 | t.Errorf("%v. Hex(%v) => (%v), want %v (delta %v)", i, tt.hex, c, tt.c, delta)
180 | }
181 | }
182 | }
183 |
184 | func TestShortHEXCreation(t *testing.T) {
185 | for i, tt := range shorthexvals {
186 | c, err := Hex(strings.ToUpper(tt.hex))
187 | if err != nil || !c.AlmostEqualRgb(tt.c) {
188 | t.Errorf("%v. Hex(%v) => (%v), want %v (delta %v)", i, strings.ToUpper(tt.hex), c, tt.c, delta)
189 | }
190 | }
191 | }
192 |
193 | func TestHexConversion(t *testing.T) {
194 | for i, tt := range vals {
195 | hex := tt.c.Hex()
196 | if hex != tt.hex {
197 | t.Errorf("%v. %v.Hex() => (%v), want %v (delta %v)", i, tt.c, hex, tt.hex, delta)
198 | }
199 | }
200 | }
201 |
202 | /// Linear ///
203 | //////////////
204 |
205 | // LinearRgb itself is implicitly tested by XYZ conversions below (they use it).
206 | // So what we do here is just test that the FastLinearRgb approximation is "good enough"
207 | func TestFastLinearRgb(t *testing.T) {
208 | const eps = 6.0 / 255.0 // We want that "within 6 RGB values total" is "good enough".
209 |
210 | for r := 0.0; r < 256.0; r++ {
211 | for g := 0.0; g < 256.0; g++ {
212 | for b := 0.0; b < 256.0; b++ {
213 | c := Color{r / 255.0, g / 255.0, b / 255.0}
214 | r_want, g_want, b_want := c.LinearRgb()
215 | r_appr, g_appr, b_appr := c.FastLinearRgb()
216 | dr, dg, db := math.Abs(r_want-r_appr), math.Abs(g_want-g_appr), math.Abs(b_want-b_appr)
217 | if dr+dg+db > eps {
218 | t.Errorf("FastLinearRgb not precise enough for %v: differences are (%v, %v, %v), allowed total difference is %v", c, dr, dg, db, eps)
219 | return
220 | }
221 |
222 | c_want := LinearRgb(r/255.0, g/255.0, b/255.0)
223 | c_appr := FastLinearRgb(r/255.0, g/255.0, b/255.0)
224 | dr, dg, db = math.Abs(c_want.R-c_appr.R), math.Abs(c_want.G-c_appr.G), math.Abs(c_want.B-c_appr.B)
225 | if dr+dg+db > eps {
226 | t.Errorf("FastLinearRgb not precise enough for (%v, %v, %v): differences are (%v, %v, %v), allowed total difference is %v", r, g, b, dr, dg, db, eps)
227 | return
228 | }
229 | }
230 | }
231 | }
232 | }
233 |
234 | // Also include some benchmarks to make sure the `Fast` versions are actually significantly faster!
235 | // (Sounds silly, but the original ones weren't!)
236 |
237 | func BenchmarkColorToLinear(bench *testing.B) {
238 | var r, g, b float64
239 | for n := 0; n < bench.N; n++ {
240 | r, g, b = Color{rand.Float64(), rand.Float64(), rand.Float64()}.LinearRgb()
241 | }
242 | bench_result = r + g + b
243 | }
244 |
245 | func BenchmarkFastColorToLinear(bench *testing.B) {
246 | var r, g, b float64
247 | for n := 0; n < bench.N; n++ {
248 | r, g, b = Color{rand.Float64(), rand.Float64(), rand.Float64()}.FastLinearRgb()
249 | }
250 | bench_result = r + g + b
251 | }
252 |
253 | func BenchmarkLinearToColor(bench *testing.B) {
254 | var c Color
255 | for n := 0; n < bench.N; n++ {
256 | c = LinearRgb(rand.Float64(), rand.Float64(), rand.Float64())
257 | }
258 | bench_result = c.R + c.G + c.B
259 | }
260 |
261 | func BenchmarkFastLinearToColor(bench *testing.B) {
262 | var c Color
263 | for n := 0; n < bench.N; n++ {
264 | c = FastLinearRgb(rand.Float64(), rand.Float64(), rand.Float64())
265 | }
266 | bench_result = c.R + c.G + c.B
267 | }
268 |
269 | // / XYZ ///
270 | // /////////
271 | func TestXyzCreation(t *testing.T) {
272 | for i, tt := range vals {
273 | c := Xyz(tt.xyz[0], tt.xyz[1], tt.xyz[2])
274 | if !c.AlmostEqualRgb(tt.c) {
275 | t.Errorf("%v. Xyz(%v) => (%v), want %v (delta %v)", i, tt.xyz, c, tt.c, delta)
276 | }
277 | }
278 | }
279 |
280 | func TestXyzConversion(t *testing.T) {
281 | for i, tt := range vals {
282 | x, y, z := tt.c.Xyz()
283 | if !almosteq(x, tt.xyz[0]) || !almosteq(y, tt.xyz[1]) || !almosteq(z, tt.xyz[2]) {
284 | t.Errorf("%v. %v.Xyz() => (%v), want %v (delta %v)", i, tt.c, [3]float64{x, y, z}, tt.xyz, delta)
285 | }
286 | }
287 | }
288 |
289 | // / xyY ///
290 | // /////////
291 | func TestXyyCreation(t *testing.T) {
292 | for i, tt := range vals {
293 | c := Xyy(tt.xyy[0], tt.xyy[1], tt.xyy[2])
294 | if !c.AlmostEqualRgb(tt.c) {
295 | t.Errorf("%v. Xyy(%v) => (%v), want %v (delta %v)", i, tt.xyy, c, tt.c, delta)
296 | }
297 | }
298 | }
299 |
300 | func TestXyyConversion(t *testing.T) {
301 | for i, tt := range vals {
302 | x, y, Y := tt.c.Xyy()
303 | if !almosteq(x, tt.xyy[0]) || !almosteq(y, tt.xyy[1]) || !almosteq(Y, tt.xyy[2]) {
304 | t.Errorf("%v. %v.Xyy() => (%v), want %v (delta %v)", i, tt.c, [3]float64{x, y, Y}, tt.xyy, delta)
305 | }
306 | }
307 | }
308 |
309 | // / L*a*b* ///
310 | // ////////////
311 | func TestLabCreation(t *testing.T) {
312 | for i, tt := range vals {
313 | c := Lab(tt.lab[0], tt.lab[1], tt.lab[2])
314 | if !c.AlmostEqualRgb(tt.c) {
315 | t.Errorf("%v. Lab(%v) => (%v), want %v (delta %v)", i, tt.lab, c, tt.c, delta)
316 | }
317 | }
318 | }
319 |
320 | func TestLabConversion(t *testing.T) {
321 | for i, tt := range vals {
322 | l, a, b := tt.c.Lab()
323 | if !almosteq(l, tt.lab[0]) || !almosteq(a, tt.lab[1]) || !almosteq(b, tt.lab[2]) {
324 | t.Errorf("%v. %v.Lab() => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, a, b}, tt.lab, delta)
325 | }
326 | }
327 | }
328 |
329 | func TestLabWhiteRefCreation(t *testing.T) {
330 | for i, tt := range vals {
331 | c := LabWhiteRef(tt.lab50[0], tt.lab50[1], tt.lab50[2], D50)
332 | if !c.AlmostEqualRgb(tt.c) {
333 | t.Errorf("%v. LabWhiteRef(%v, D50) => (%v), want %v (delta %v)", i, tt.lab50, c, tt.c, delta)
334 | }
335 | }
336 | }
337 |
338 | func TestLabWhiteRefConversion(t *testing.T) {
339 | for i, tt := range vals {
340 | l, a, b := tt.c.LabWhiteRef(D50)
341 | if !almosteq(l, tt.lab50[0]) || !almosteq(a, tt.lab50[1]) || !almosteq(b, tt.lab50[2]) {
342 | t.Errorf("%v. %v.LabWhiteRef(D50) => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, a, b}, tt.lab50, delta)
343 | }
344 | }
345 | }
346 |
347 | // / L*u*v* ///
348 | // ////////////
349 | func TestLuvCreation(t *testing.T) {
350 | for i, tt := range vals {
351 | c := Luv(tt.luv[0], tt.luv[1], tt.luv[2])
352 | if !c.AlmostEqualRgb(tt.c) {
353 | t.Errorf("%v. Luv(%v) => (%v), want %v (delta %v)", i, tt.luv, c, tt.c, delta)
354 | }
355 | }
356 | }
357 |
358 | func TestLuvConversion(t *testing.T) {
359 | for i, tt := range vals {
360 | l, u, v := tt.c.Luv()
361 | if !almosteq(l, tt.luv[0]) || !almosteq(u, tt.luv[1]) || !almosteq(v, tt.luv[2]) {
362 | t.Errorf("%v. %v.Luv() => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, u, v}, tt.luv, delta)
363 | }
364 | }
365 | }
366 |
367 | func TestLuvWhiteRefCreation(t *testing.T) {
368 | for i, tt := range vals {
369 | c := LuvWhiteRef(tt.luv50[0], tt.luv50[1], tt.luv50[2], D50)
370 | if !c.AlmostEqualRgb(tt.c) {
371 | t.Errorf("%v. LuvWhiteRef(%v, D50) => (%v), want %v (delta %v)", i, tt.luv50, c, tt.c, delta)
372 | }
373 | }
374 | }
375 |
376 | func TestLuvWhiteRefConversion(t *testing.T) {
377 | for i, tt := range vals {
378 | l, u, v := tt.c.LuvWhiteRef(D50)
379 | if !almosteq(l, tt.luv50[0]) || !almosteq(u, tt.luv50[1]) || !almosteq(v, tt.luv50[2]) {
380 | t.Errorf("%v. %v.LuvWhiteRef(D50) => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, u, v}, tt.luv50, delta)
381 | }
382 | }
383 | }
384 |
385 | // / HCL ///
386 | // /////////
387 | // CIE-L*a*b* in polar coordinates.
388 | func TestHclCreation(t *testing.T) {
389 | for i, tt := range vals {
390 | c := Hcl(tt.hcl[0], tt.hcl[1], tt.hcl[2])
391 | if !c.AlmostEqualRgb(tt.c) {
392 | t.Errorf("%v. Hcl(%v) => (%v), want %v (delta %v)", i, tt.hcl, c, tt.c, delta)
393 | }
394 | }
395 | }
396 |
397 | func TestHclConversion(t *testing.T) {
398 | for i, tt := range vals {
399 | h, c, l := tt.c.Hcl()
400 | if !almosteq(h, tt.hcl[0]) || !almosteq(c, tt.hcl[1]) || !almosteq(l, tt.hcl[2]) {
401 | t.Errorf("%v. %v.Hcl() => (%v), want %v (delta %v)", i, tt.c, [3]float64{h, c, l}, tt.hcl, delta)
402 | }
403 | }
404 | }
405 |
406 | func TestHclWhiteRefCreation(t *testing.T) {
407 | for i, tt := range vals {
408 | c := HclWhiteRef(tt.hcl50[0], tt.hcl50[1], tt.hcl50[2], D50)
409 | if !c.AlmostEqualRgb(tt.c) {
410 | t.Errorf("%v. HclWhiteRef(%v, D50) => (%v), want %v (delta %v)", i, tt.hcl50, c, tt.c, delta)
411 | }
412 | }
413 | }
414 |
415 | func TestHclWhiteRefConversion(t *testing.T) {
416 | for i, tt := range vals {
417 | h, c, l := tt.c.HclWhiteRef(D50)
418 | if !almosteq(h, tt.hcl50[0]) || !almosteq(c, tt.hcl50[1]) || !almosteq(l, tt.hcl50[2]) {
419 | t.Errorf("%v. %v.HclWhiteRef(D50) => (%v), want %v (delta %v)", i, tt.c, [3]float64{h, c, l}, tt.hcl50, delta)
420 | }
421 | }
422 | }
423 |
424 | // / Oklab ///
425 | // ///////////
426 |
427 | func TestRgbToOkLab(t *testing.T) {
428 | for i, tCase := range []struct {
429 | R, G, B float64
430 | l, a, b float64
431 | }{
432 | {1, 1, 1, 1.000, 0.000, 0.000}, // white
433 | {1, 0, 0, 0.627955, 0.224863, 0.125846}, // red
434 | {0, 1, 0, 0.86644, -0.233888, 0.179498}, // lime
435 | {0, 0, 1, 0.452014, -0.032457, -0.311528}, // blue
436 | {0, 1, 1, 0.905399, -0.149444, -0.039398}, // cyan
437 | {1, 0, 1, 0.701674, 0.274566, -0.169156}, // magenta
438 | {1, 1, 0, 0.967983, -0.071369, 0.198570}, // yellow
439 | {0, 0, 0, 0.000000, 0.000000, 0.000000}, // black
440 | } {
441 | l, a, b := XyzToOkLab(LinearRgb(tCase.R, tCase.G, tCase.B).Xyz())
442 | if !almosteq(l, tCase.l) {
443 | t.Errorf("%v. RgbToOklab => (%v), want %v (l)", i, l, tCase.l)
444 | }
445 | if !almosteq(a, tCase.a) {
446 | t.Errorf("%v. RgbToOklab => (%v), want %v (a)", i, a, tCase.a)
447 | }
448 | if !almosteq(b, tCase.b) {
449 | t.Errorf("%v. RgbToOklab => (%v), want %v (b)", i, b, tCase.b)
450 | }
451 | }
452 | }
453 |
454 | // https://bottosson.github.io/posts/oklab/#table-of-example-xyz-and-oklab-pairs
455 | var xyzOklabPairs = []struct {
456 | x, y, z float64
457 | l, a, b float64
458 | }{
459 | {0.950, 1.000, 1.089, 1.000, 0.000, 0.000},
460 | {1.000, 0.000, 0.000, 0.450, 1.236, -0.019},
461 | {0.000, 1.000, 0.000, 0.922, -0.671, 0.263},
462 | {0.000, 0.000, 1.000, 0.153, -1.415, -0.449},
463 | }
464 |
465 | func TestXyzToOkLab(t *testing.T) {
466 | for i, tCase := range xyzOklabPairs {
467 | l, a, b := XyzToOkLab(tCase.x, tCase.y, tCase.z)
468 | if !almosteq(l, tCase.l) {
469 | t.Errorf("%v. XyzToOklab => (%v), want %v (l)", i, l, tCase.l)
470 | }
471 | if !almosteq(a, tCase.a) {
472 | t.Errorf("%v. XyzToOklab => (%v), want %v (a)", i, a, tCase.a)
473 | }
474 | if !almosteq(b, tCase.b) {
475 | t.Errorf("%v. XyzToOklab => (%v), want %v (b)", i, b, tCase.b)
476 | }
477 | }
478 | }
479 |
480 | func TestOklabToXyz(t *testing.T) {
481 | for i, tCase := range xyzOklabPairs {
482 | x, y, z := OkLabToXyz(tCase.l, tCase.a, tCase.b)
483 | if !almosteq(x, tCase.x) {
484 | t.Errorf("%v. OklabToXyz => (%v), want %v (x)", i, x, tCase.x)
485 | }
486 | if !almosteq(y, tCase.y) {
487 | t.Errorf("%v. OklabToXyz => (%v), want %v (y)", i, y, tCase.y)
488 | }
489 | if !almosteq(z, tCase.z) {
490 | t.Errorf("%v. OklabToXyz => (%v), want %v (z)", i, z, tCase.z)
491 | }
492 | }
493 | }
494 |
495 | var OkPairs = []struct {
496 | lab [3]float64
497 | lch [3]float64
498 | }{
499 | {
500 | [3]float64{55.0, 0.17, -0.14}, // oklab
501 | [3]float64{55.0, 0.22, 320.528}, // oklch
502 | },
503 | {
504 | [3]float64{90.0, 0.32, 0.00}, // oklab
505 | [3]float64{90.0, 0.32, 0.0}, // oklch
506 | },
507 | {
508 | [3]float64{10.0, 0.00, -0.40}, // oklab
509 | [3]float64{10.0, 0.40, 270.0}, // oklch
510 | },
511 | }
512 |
513 | func TestOkLabToOkLch(t *testing.T) {
514 | for i, tc := range OkPairs {
515 | l, c, h := OkLabToOkLch(tc.lab[0], tc.lab[1], tc.lab[2])
516 | if !almosteq(l, tc.lch[0]) {
517 | t.Errorf("%d. l returned %v, expected %v", i, l, tc.lch[0])
518 | }
519 |
520 | if !almosteq(c, tc.lch[1]) {
521 | t.Errorf("%d. c returned %v, expected %v", i, c, tc.lch[1])
522 | }
523 |
524 | if !almosteq(h, tc.lch[2]) {
525 | t.Errorf("%d. h returned %v, expected %v", i, h, tc.lch[2])
526 | }
527 | }
528 | }
529 |
530 | func TestOkLchToOkLab(t *testing.T) {
531 | for i, tc := range OkPairs {
532 | l, a, b := OkLchToOkLab(tc.lch[0], tc.lch[1], tc.lch[2])
533 | if !almosteq(l, tc.lab[0]) {
534 | t.Errorf("%d. l returned %v, expected %v", i, l, tc.lab[0])
535 | }
536 |
537 | if !almosteq(a, tc.lab[1]) {
538 | t.Errorf("%d. a returned %v, expected %v", i, a, tc.lab[1])
539 | }
540 |
541 | if !almosteq(b, tc.lab[2]) {
542 | t.Errorf("%d. b returned %v, expected %v", i, b, tc.lab[2])
543 | }
544 | }
545 | }
546 |
547 | /// Test distances ///
548 | //////////////////////
549 |
550 | // Ground-truth from http://www.brucelindbloom.com/index.html?ColorDifferenceCalcHelp.html
551 | var dists = []struct {
552 | c1 Color
553 | c2 Color
554 | d76 float64 // That's also dLab
555 | d94 float64
556 | d00 float64
557 | }{
558 | {Color{1.0, 1.0, 1.0}, Color{1.0, 1.0, 1.0}, 0.0, 0.0, 0.0},
559 | {Color{0.0, 0.0, 0.0}, Color{0.0, 0.0, 0.0}, 0.0, 0.0, 0.0},
560 |
561 | // Just pairs of values of the table way above.
562 | {Lab(1.000000, 0.000000, 0.000000), Lab(0.931390, -0.353319, -0.108946), 0.37604638, 0.37604638, 0.23528129},
563 | {Lab(0.720892, 0.651673, -0.422133), Lab(0.977637, -0.165795, 0.602017), 1.33531088, 0.65466377, 0.75175896},
564 | {Lab(0.590453, 0.332846, -0.637099), Lab(0.681085, 0.483884, 0.228328), 0.88317072, 0.42541075, 0.37688153},
565 | {Lab(0.906026, -0.600870, 0.498993), Lab(0.533890, 0.000000, 0.000000), 0.86517280, 0.41038323, 0.39960503},
566 | {Lab(0.911132, -0.480875, -0.141312), Lab(0.603242, 0.982343, -0.608249), 1.56647162, 0.87431457, 0.57983482},
567 | {Lab(0.971393, -0.215537, 0.944780), Lab(0.322970, 0.791875, -1.078602), 2.35146891, 1.11858192, 1.03426977},
568 | {Lab(0.877347, -0.861827, 0.831793), Lab(0.532408, 0.800925, 0.672032), 1.70565338, 0.68800270, 0.86608245},
569 | }
570 |
571 | func TestLabDistance(t *testing.T) {
572 | for i, tt := range dists {
573 | d := tt.c1.DistanceCIE76(tt.c2)
574 | if !almosteq(d, tt.d76) {
575 | t.Errorf("%v. %v.DistanceCIE76(%v) => (%v), want %v (delta %v)", i, tt.c1, tt.c2, d, tt.d76, delta)
576 | }
577 | }
578 | }
579 |
580 | func TestCIE94Distance(t *testing.T) {
581 | for i, tt := range dists {
582 | d := tt.c1.DistanceCIE94(tt.c2)
583 | if !almosteq(d, tt.d94) {
584 | t.Errorf("%v. %v.DistanceCIE94(%v) => (%v), want %v (delta %v)", i, tt.c1, tt.c2, d, tt.d94, delta)
585 | }
586 | }
587 | }
588 |
589 | func TestCIEDE2000Distance(t *testing.T) {
590 | for i, tt := range dists {
591 | d := tt.c1.DistanceCIEDE2000(tt.c2)
592 | if !almosteq(d, tt.d00) {
593 | t.Errorf("%v. %v.DistanceCIEDE2000(%v) => (%v), want %v (delta %v)", i, tt.c1, tt.c2, d, tt.d00, delta)
594 | }
595 | }
596 | }
597 |
598 | /// Test utilities ///
599 | //////////////////////
600 |
601 | func TestClamp(t *testing.T) {
602 | c_orig := Color{1.1, -0.1, 0.5}
603 | c_want := Color{1.0, 0.0, 0.5}
604 | if c_orig.Clamped() != c_want {
605 | t.Errorf("%v.Clamped() => %v, want %v", c_orig, c_orig.Clamped(), c_want)
606 | }
607 | }
608 |
609 | func TestMakeColor(t *testing.T) {
610 | c_orig_nrgba := color.NRGBA{123, 45, 67, 255}
611 | c_ours, ok := MakeColor(c_orig_nrgba)
612 | r, g, b := c_ours.RGB255()
613 | if r != 123 || g != 45 || b != 67 || !ok {
614 | t.Errorf("NRGBA->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_nrgba, r, g, b, ok)
615 | }
616 |
617 | c_orig_nrgba64 := color.NRGBA64{123 << 8, 45 << 8, 67 << 8, 0xffff}
618 | c_ours, ok = MakeColor(c_orig_nrgba64)
619 | r, g, b = c_ours.RGB255()
620 | if r != 123 || g != 45 || b != 67 || !ok {
621 | t.Errorf("NRGBA64->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_nrgba64, r, g, b, ok)
622 | }
623 |
624 | c_orig_gray := color.Gray{123}
625 | c_ours, ok = MakeColor(c_orig_gray)
626 | r, g, b = c_ours.RGB255()
627 | if r != 123 || g != 123 || b != 123 || !ok {
628 | t.Errorf("Gray->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_gray, r, g, b, ok)
629 | }
630 |
631 | c_orig_gray16 := color.Gray16{123 << 8}
632 | c_ours, ok = MakeColor(c_orig_gray16)
633 | r, g, b = c_ours.RGB255()
634 | if r != 123 || g != 123 || b != 123 || !ok {
635 | t.Errorf("Gray16->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_gray16, r, g, b, ok)
636 | }
637 |
638 | c_orig_rgba := color.RGBA{255, 255, 255, 0}
639 | c_ours, ok = MakeColor(c_orig_rgba)
640 | r, g, b = c_ours.RGB255()
641 | if r != 0 || g != 0 || b != 0 || ok {
642 | t.Errorf("RGBA->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_rgba, r, g, b, ok)
643 | }
644 | }
645 |
646 | /// Issues raised on github ///
647 | ///////////////////////////////
648 |
649 | // https://github.com/lucasb-eyer/go-colorful/issues/11
650 | func TestIssue11(t *testing.T) {
651 | c1hex := "#1a1a46"
652 | c2hex := "#666666"
653 |
654 | c1, _ := Hex(c1hex)
655 | c2, _ := Hex(c2hex)
656 |
657 | blend := c1.BlendHsv(c2, 0).Hex()
658 | if blend != c1hex {
659 | t.Errorf("Issue11: %v --Hsv-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
660 | }
661 | blend = c1.BlendHsv(c2, 1).Hex()
662 | if blend != c2hex {
663 | t.Errorf("Issue11: %v --Hsv-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
664 | }
665 |
666 | blend = c1.BlendLuv(c2, 0).Hex()
667 | if blend != c1hex {
668 | t.Errorf("Issue11: %v --Luv-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
669 | }
670 | blend = c1.BlendLuv(c2, 1).Hex()
671 | if blend != c2hex {
672 | t.Errorf("Issue11: %v --Luv-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
673 | }
674 |
675 | blend = c1.BlendRgb(c2, 0).Hex()
676 | if blend != c1hex {
677 | t.Errorf("Issue11: %v --Rgb-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
678 | }
679 | blend = c1.BlendRgb(c2, 1).Hex()
680 | if blend != c2hex {
681 | t.Errorf("Issue11: %v --Rgb-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
682 | }
683 |
684 | blend = c1.BlendLinearRgb(c2, 0).Hex()
685 | if blend != c1hex {
686 | t.Errorf("Issue11: %v --LinearRgb-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
687 | }
688 | blend = c1.BlendLinearRgb(c2, 1).Hex()
689 | if blend != c2hex {
690 | t.Errorf("Issue11: %v --LinearRgb-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
691 | }
692 |
693 | blend = c1.BlendLab(c2, 0).Hex()
694 | if blend != c1hex {
695 | t.Errorf("Issue11: %v --Lab-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
696 | }
697 | blend = c1.BlendLab(c2, 1).Hex()
698 | if blend != c2hex {
699 | t.Errorf("Issue11: %v --Lab-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
700 | }
701 |
702 | blend = c1.BlendHcl(c2, 0).Hex()
703 | if blend != c1hex {
704 | t.Errorf("Issue11: %v --Hcl-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
705 | }
706 | blend = c1.BlendHcl(c2, 1).Hex()
707 | if blend != c2hex {
708 | t.Errorf("Issue11: %v --Hcl-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
709 | }
710 |
711 | blend = c1.BlendLuvLCh(c2, 0).Hex()
712 | if blend != c1hex {
713 | t.Errorf("Issue11: %v --LuvLCh-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
714 | }
715 | blend = c1.BlendLuvLCh(c2, 1).Hex()
716 | if blend != c2hex {
717 | t.Errorf("Issue11: %v --LuvLCh-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
718 | }
719 |
720 | blend = c1.BlendOkLab(c2, 0).Hex()
721 | if blend != c1hex {
722 | t.Errorf("Issue11: %v --OkLab-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
723 | }
724 | blend = c1.BlendOkLab(c2, 1).Hex()
725 | if blend != c2hex {
726 | t.Errorf("Issue11: %v --OkLab-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
727 | }
728 |
729 | blend = c1.BlendOkLch(c2, 0).Hex()
730 | if blend != c1hex {
731 | t.Errorf("Issue11: %v --OkLch-> %v = %v, want %v", c1hex, c2hex, blend, c1hex)
732 | }
733 | blend = c1.BlendOkLch(c2, 1).Hex()
734 | if blend != c2hex {
735 | t.Errorf("Issue11: %v --OkLch-> %v = %v, want %v", c1hex, c2hex, blend, c2hex)
736 | }
737 | }
738 |
739 | // For testing angular interpolation internal function
740 | // NOTE: They are being tested in both directions.
741 | var anglevals = []struct {
742 | a0 float64
743 | a1 float64
744 | t float64
745 | at float64
746 | }{
747 | {0.0, 1.0, 0.0, 0.0},
748 | {0.0, 1.0, 0.25, 0.25},
749 | {0.0, 1.0, 0.5, 0.5},
750 | {0.0, 1.0, 1.0, 1.0},
751 | {0.0, 90.0, 0.0, 0.0},
752 | {0.0, 90.0, 0.25, 22.5},
753 | {0.0, 90.0, 0.5, 45.0},
754 | {0.0, 90.0, 1.0, 90.0},
755 | {0.0, 178.0, 0.0, 0.0}, // Exact 0-180 is ambiguous.
756 | {0.0, 178.0, 0.25, 44.5},
757 | {0.0, 178.0, 0.5, 89.0},
758 | {0.0, 178.0, 1.0, 178.0},
759 | {0.0, 182.0, 0.0, 0.0}, // Exact 0-180 is ambiguous.
760 | {0.0, 182.0, 0.25, 315.5},
761 | {0.0, 182.0, 0.5, 271.0},
762 | {0.0, 182.0, 1.0, 182.0},
763 | {0.0, 270.0, 0.0, 0.0},
764 | {0.0, 270.0, 0.25, 337.5},
765 | {0.0, 270.0, 0.5, 315.0},
766 | {0.0, 270.0, 1.0, 270.0},
767 | {0.0, 359.0, 0.0, 0.0},
768 | {0.0, 359.0, 0.25, 359.75},
769 | {0.0, 359.0, 0.5, 359.5},
770 | {0.0, 359.0, 1.0, 359.0},
771 | }
772 |
773 | func TestInterpolation(t *testing.T) {
774 | // Forward
775 | for i, tt := range anglevals {
776 | res := interp_angle(tt.a0, tt.a1, tt.t)
777 | if !almosteq_eps(res, tt.at, 1e-15) {
778 | t.Errorf("%v. interp_angle(%v, %v, %v) => (%v), want %v", i, tt.a0, tt.a1, tt.t, res, tt.at)
779 | }
780 | }
781 | // Backward
782 | for i, tt := range anglevals {
783 | res := interp_angle(tt.a1, tt.a0, 1.0-tt.t)
784 | if !almosteq_eps(res, tt.at, 1e-15) {
785 | t.Errorf("%v. interp_angle(%v, %v, %v) => (%v), want %v", i, tt.a1, tt.a0, 1.0-tt.t, res, tt.at)
786 | }
787 | }
788 | }
789 |
--------------------------------------------------------------------------------
/doc/approx-quality.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/approx-quality.png
--------------------------------------------------------------------------------
/doc/colorblend/clamped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorblend/clamped.png
--------------------------------------------------------------------------------
/doc/colorblend/clamped.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorblend/clamped.xcf
--------------------------------------------------------------------------------
/doc/colorblend/colorblend.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "fmt"
4 | import "github.com/lucasb-eyer/go-colorful"
5 | import "image"
6 | import "image/draw"
7 | import "image/png"
8 | import "os"
9 |
10 | func main() {
11 | blocks := 10
12 | blockw := 40
13 | img := image.NewRGBA(image.Rect(0, 0, blocks*blockw, 200))
14 |
15 | c1, _ := colorful.Hex("#fdffcc")
16 | c2, _ := colorful.Hex("#242a42")
17 |
18 | // Use these colors to get invalid RGB in the gradient.
19 | //c1, _ := colorful.Hex("#EEEF61")
20 | //c2, _ := colorful.Hex("#1E3140")
21 |
22 | for i := 0; i < blocks; i++ {
23 | draw.Draw(img, image.Rect(i*blockw, 0, (i+1)*blockw, 40), &image.Uniform{c1.BlendHsv(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
24 | draw.Draw(img, image.Rect(i*blockw, 40, (i+1)*blockw, 80), &image.Uniform{c1.BlendLuv(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
25 | draw.Draw(img, image.Rect(i*blockw, 80, (i+1)*blockw, 120), &image.Uniform{c1.BlendRgb(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
26 | draw.Draw(img, image.Rect(i*blockw, 120, (i+1)*blockw, 160), &image.Uniform{c1.BlendLab(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
27 | draw.Draw(img, image.Rect(i*blockw, 160, (i+1)*blockw, 200), &image.Uniform{c1.BlendHcl(c2, float64(i)/float64(blocks-1))}, image.Point{}, draw.Src)
28 |
29 | // This can be used to "fix" invalid colors in the gradient.
30 | //draw.Draw(img, image.Rect(i*blockw,160,(i+1)*blockw,200), &image.Uniform{c1.BlendHcl(c2, float64(i)/float64(blocks-1)).Clamped()}, image.Point{}, draw.Src)
31 | }
32 |
33 | toimg, err := os.Create("colorblend.png")
34 | if err != nil {
35 | fmt.Printf("Error: %v", err)
36 | return
37 | }
38 | defer toimg.Close()
39 |
40 | png.Encode(toimg, img)
41 | }
42 |
--------------------------------------------------------------------------------
/doc/colorblend/colorblend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorblend/colorblend.png
--------------------------------------------------------------------------------
/doc/colorblend/colorblend.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorblend/colorblend.xcf
--------------------------------------------------------------------------------
/doc/colorblend/invalid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorblend/invalid.png
--------------------------------------------------------------------------------
/doc/colorblend/invalid.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorblend/invalid.xcf
--------------------------------------------------------------------------------
/doc/colordist/colordist.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "fmt"
4 | import "github.com/lucasb-eyer/go-colorful"
5 |
6 | func main() {
7 | c1a := colorful.Color{150.0 / 255.0, 10.0 / 255.0, 150.0 / 255.0}
8 | c1b := colorful.Color{53.0 / 255.0, 10.0 / 255.0, 150.0 / 255.0}
9 | c2a := colorful.Color{10.0 / 255.0, 150.0 / 255.0, 50.0 / 255.0}
10 | c2b := colorful.Color{99.9 / 255.0, 150.0 / 255.0, 10.0 / 255.0}
11 |
12 | fmt.Printf("DistanceRgb: c1: %v\tand c2: %v\n", c1a.DistanceRgb(c1b), c2a.DistanceRgb(c2b))
13 | fmt.Printf("DistanceLab: c1: %v\tand c2: %v\n", c1a.DistanceLab(c1b), c2a.DistanceLab(c2b))
14 | fmt.Printf("DistanceLuv: c1: %v\tand c2: %v\n", c1a.DistanceLuv(c1b), c2a.DistanceLuv(c2b))
15 | fmt.Printf("DistanceCIE76: c1: %v\tand c2: %v\n", c1a.DistanceCIE76(c1b), c2a.DistanceCIE76(c2b))
16 | fmt.Printf("DistanceCIE94: c1: %v\tand c2: %v\n", c1a.DistanceCIE94(c1b), c2a.DistanceCIE94(c2b))
17 | fmt.Printf("DistanceCIEDE2000: c1: %v\tand c2: %v\n", c1a.DistanceCIEDE2000(c1b), c2a.DistanceCIEDE2000(c2b))
18 | }
19 |
--------------------------------------------------------------------------------
/doc/colordist/colordist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colordist/colordist.png
--------------------------------------------------------------------------------
/doc/colordist/colordist.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colordist/colordist.xcf
--------------------------------------------------------------------------------
/doc/colorgens/colorgens.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/draw"
7 | "image/png"
8 | "math/rand"
9 | "os"
10 | "time"
11 |
12 | "github.com/lucasb-eyer/go-colorful"
13 | )
14 |
15 | func main() {
16 | blocks := 10
17 | blockw := 40
18 | space := 5
19 |
20 | seed := time.Now().UTC().UnixNano()
21 |
22 | rand := rand.New(rand.NewSource(seed))
23 | img := image.NewRGBA(image.Rect(0, 0, blocks*blockw+space*(blocks-1), 4*(blockw+space)))
24 |
25 | for i := 0; i < blocks; i++ {
26 | warm := colorful.WarmColorWithRand(rand)
27 | fwarm := colorful.FastWarmColorWithRand(rand)
28 | happy := colorful.HappyColorWithRand(rand)
29 | fhappy := colorful.FastHappyColorWithRand(rand)
30 | draw.Draw(img, image.Rect(i*(blockw+space), 0, i*(blockw+space)+blockw, blockw), &image.Uniform{warm}, image.Point{}, draw.Src)
31 | draw.Draw(img, image.Rect(i*(blockw+space), blockw+space, i*(blockw+space)+blockw, 2*blockw+space), &image.Uniform{fwarm}, image.Point{}, draw.Src)
32 | draw.Draw(img, image.Rect(i*(blockw+space), 2*blockw+3*space, i*(blockw+space)+blockw, 3*blockw+3*space), &image.Uniform{happy}, image.Point{}, draw.Src)
33 | draw.Draw(img, image.Rect(i*(blockw+space), 3*blockw+4*space, i*(blockw+space)+blockw, 4*blockw+4*space), &image.Uniform{fhappy}, image.Point{}, draw.Src)
34 | }
35 |
36 | toimg, err := os.Create("colorgens.png")
37 | if err != nil {
38 | fmt.Printf("Error: %v", err)
39 | return
40 | }
41 | defer toimg.Close()
42 |
43 | png.Encode(toimg, img)
44 | }
45 |
--------------------------------------------------------------------------------
/doc/colorgens/colorgens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorgens/colorgens.png
--------------------------------------------------------------------------------
/doc/colorsort/colorsort.go:
--------------------------------------------------------------------------------
1 | // This program generates an example of go-colorful's Sorted function. It
2 | // produces an image with three stripes of color. The first is unsorted.
3 | // The second is sorted in the CIE-L\*C\*h° space, ordered primarily by
4 | // lightness, then by hue angle, and finally by chroma. The third is
5 | // sorted using colorful.Sorted.
6 |
7 | package main
8 |
9 | import (
10 | "image"
11 | "image/png"
12 | "math/rand"
13 | "os"
14 | "sort"
15 |
16 | "github.com/lucasb-eyer/go-colorful"
17 | )
18 |
19 | // randomColors produces a slice of random colors.
20 | func randomColors(n int, rand colorful.RandInterface) []colorful.Color {
21 | cs := make([]colorful.Color, n)
22 | for i := range cs {
23 | cs[i] = colorful.Color{
24 | R: rand.Float64(),
25 | G: rand.Float64(),
26 | B: rand.Float64(),
27 | }
28 | }
29 | return cs
30 | }
31 |
32 | // drawStripes creates an image with three sets of stripes.
33 | func drawStripes(cs1, cs2, cs3 []colorful.Color, ht, sep int) *image.RGBA {
34 | img := image.NewRGBA(image.Rect(0, 0, len(cs1), 3*ht+2*sep))
35 | for c := range cs1 {
36 | for r := 0; r < ht; r++ {
37 | img.Set(c, r, cs1[c].Clamped())
38 | img.Set(c, r+ht+sep, cs2[c].Clamped())
39 | img.Set(c, r+(ht+sep)*2, cs3[c].Clamped())
40 | }
41 | }
42 | return img
43 | }
44 |
45 | // writeImage writes an image to disk.
46 | func writeImage(fn string, img image.Image) {
47 | w, err := os.Create(fn)
48 | if err != nil {
49 | panic(err)
50 | }
51 | defer w.Close()
52 | err = png.Encode(w, img)
53 | if err != nil {
54 | panic(err)
55 | }
56 | }
57 |
58 | func main() {
59 | n := 512
60 | const SEED = 8675309
61 | rand := rand.New(rand.NewSource(SEED))
62 | cs1 := randomColors(n, rand)
63 | cs2 := make([]colorful.Color, n)
64 | copy(cs2, cs1)
65 | sort.Slice(cs2, func(i, j int) bool {
66 | l1, c1, h1 := cs2[i].LuvLCh()
67 | l2, c2, h2 := cs2[j].LuvLCh()
68 | if l1 != l2 {
69 | return l1 < l2
70 | }
71 | if h1 != h2 {
72 | return h1 < h2
73 | }
74 |
75 | if c1 != c2 {
76 | return c1 < c2
77 | }
78 | return false
79 | })
80 | cs3 := colorful.Sorted(cs1)
81 | img := drawStripes(cs1, cs2, cs3, 64, 16)
82 | writeImage("colorsort.png", img)
83 | }
84 |
--------------------------------------------------------------------------------
/doc/colorsort/colorsort.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/colorsort/colorsort.png
--------------------------------------------------------------------------------
/doc/gradientgen/gradientgen.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "image"
5 | "image/draw"
6 | "image/png"
7 | "os"
8 | "strconv"
9 |
10 | "github.com/lucasb-eyer/go-colorful"
11 | )
12 |
13 | // This table contains the "keypoints" of the colorgradient you want to generate.
14 | // The position of each keypoint has to live in the range [0,1]
15 | type GradientTable []struct {
16 | Col colorful.Color
17 | Pos float64
18 | }
19 |
20 | // This is the meat of the gradient computation. It returns a HCL-blend between
21 | // the two colors around `t`.
22 | // Note: It relies heavily on the fact that the gradient keypoints are sorted.
23 | func (gt GradientTable) GetInterpolatedColorFor(t float64) colorful.Color {
24 | for i := 0; i < len(gt)-1; i++ {
25 | c1 := gt[i]
26 | c2 := gt[i+1]
27 | if c1.Pos <= t && t <= c2.Pos {
28 | // We are in between c1 and c2. Go blend them!
29 | t := (t - c1.Pos) / (c2.Pos - c1.Pos)
30 | return c1.Col.BlendHcl(c2.Col, t).Clamped()
31 | }
32 | }
33 |
34 | // Nothing found? Means we're at (or past) the last gradient keypoint.
35 | return gt[len(gt)-1].Col
36 | }
37 |
38 | // This is a very nice thing Golang forces you to do!
39 | // It is necessary so that we can write out the literal of the colortable below.
40 | func MustParseHex(s string) colorful.Color {
41 | c, err := colorful.Hex(s)
42 | if err != nil {
43 | panic("MustParseHex: " + err.Error())
44 | }
45 | return c
46 | }
47 |
48 | func main() {
49 | // The "keypoints" of the gradient.
50 | keypoints := GradientTable{
51 | {MustParseHex("#9e0142"), 0.0},
52 | {MustParseHex("#d53e4f"), 0.1},
53 | {MustParseHex("#f46d43"), 0.2},
54 | {MustParseHex("#fdae61"), 0.3},
55 | {MustParseHex("#fee090"), 0.4},
56 | {MustParseHex("#ffffbf"), 0.5},
57 | {MustParseHex("#e6f598"), 0.6},
58 | {MustParseHex("#abdda4"), 0.7},
59 | {MustParseHex("#66c2a5"), 0.8},
60 | {MustParseHex("#3288bd"), 0.9},
61 | {MustParseHex("#5e4fa2"), 1.0},
62 | }
63 |
64 | h := 1024
65 | w := 40
66 |
67 | if len(os.Args) == 3 {
68 | // Meh, I'm being lazy...
69 | w, _ = strconv.Atoi(os.Args[1])
70 | h, _ = strconv.Atoi(os.Args[2])
71 | }
72 |
73 | img := image.NewRGBA(image.Rect(0, 0, w, h))
74 |
75 | for y := h - 1; y >= 0; y-- {
76 | c := keypoints.GetInterpolatedColorFor(float64(y) / float64(h))
77 | draw.Draw(img, image.Rect(0, y, w, y+1), &image.Uniform{c}, image.Point{}, draw.Src)
78 | }
79 |
80 | outpng, err := os.Create("gradientgen.png")
81 | if err != nil {
82 | panic("Error storing png: " + err.Error())
83 | }
84 | defer outpng.Close()
85 |
86 | png.Encode(outpng, img)
87 | }
88 |
--------------------------------------------------------------------------------
/doc/gradientgen/gradientgen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/gradientgen/gradientgen.png
--------------------------------------------------------------------------------
/doc/palettegens/palettegens.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "fmt"
4 | import "github.com/lucasb-eyer/go-colorful"
5 | import "image"
6 | import "image/draw"
7 | import "image/png"
8 | import "math/rand"
9 | import "os"
10 | import "time"
11 |
12 | func main() {
13 | colors := 10
14 | blockw := 40
15 | space := 5
16 |
17 | rand.Seed(time.Now().UTC().UnixNano())
18 | img := image.NewRGBA(image.Rect(0, 0, colors*blockw+space*(colors-1), 6*blockw+8*space))
19 |
20 | warm, err := colorful.WarmPalette(colors)
21 | if err != nil {
22 | fmt.Printf("Error generating warm palette: %v", err)
23 | return
24 | }
25 | fwarm := colorful.FastWarmPalette(colors)
26 | happy, err := colorful.HappyPalette(colors)
27 | if err != nil {
28 | fmt.Printf("Error generating happy palette: %v", err)
29 | return
30 | }
31 | fhappy := colorful.FastHappyPalette(colors)
32 | soft, err := colorful.SoftPalette(colors)
33 | if err != nil {
34 | fmt.Printf("Error generating soft palette: %v", err)
35 | return
36 | }
37 | brownies, err := colorful.SoftPaletteEx(colors, colorful.SoftPaletteSettings{isbrowny, 50, true})
38 | if err != nil {
39 | fmt.Printf("Error generating brownies: %v", err)
40 | return
41 | }
42 | for i := 0; i < colors; i++ {
43 | draw.Draw(img, image.Rect(i*(blockw+space), 0, i*(blockw+space)+blockw, blockw), &image.Uniform{warm[i]}, image.Point{}, draw.Src)
44 | draw.Draw(img, image.Rect(i*(blockw+space), 1*blockw+1*space, i*(blockw+space)+blockw, 2*blockw+1*space), &image.Uniform{fwarm[i]}, image.Point{}, draw.Src)
45 | draw.Draw(img, image.Rect(i*(blockw+space), 2*blockw+3*space, i*(blockw+space)+blockw, 3*blockw+3*space), &image.Uniform{happy[i]}, image.Point{}, draw.Src)
46 | draw.Draw(img, image.Rect(i*(blockw+space), 3*blockw+4*space, i*(blockw+space)+blockw, 4*blockw+4*space), &image.Uniform{fhappy[i]}, image.Point{}, draw.Src)
47 | draw.Draw(img, image.Rect(i*(blockw+space), 4*blockw+6*space, i*(blockw+space)+blockw, 5*blockw+6*space), &image.Uniform{soft[i]}, image.Point{}, draw.Src)
48 | draw.Draw(img, image.Rect(i*(blockw+space), 5*blockw+8*space, i*(blockw+space)+blockw, 6*blockw+8*space), &image.Uniform{brownies[i]}, image.Point{}, draw.Src)
49 | }
50 |
51 | toimg, err := os.Create("palettegens.png")
52 | if err != nil {
53 | fmt.Printf("Error: %v", err)
54 | return
55 | }
56 | defer toimg.Close()
57 |
58 | png.Encode(toimg, img)
59 | }
60 |
61 | func isbrowny(l, a, b float64) bool {
62 | h, c, L := colorful.LabToHcl(l, a, b)
63 | return 10.0 < h && h < 50.0 && 0.1 < c && c < 0.5 && L < 0.5
64 | }
65 |
--------------------------------------------------------------------------------
/doc/palettegens/palettegens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasb-eyer/go-colorful/9e385f7b653ac6956c75d32bd60efc7a0c25348e/doc/palettegens/palettegens.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/lucasb-eyer/go-colorful
2 |
3 | go 1.12
4 |
--------------------------------------------------------------------------------
/happy_palettegen.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | // Uses the HSV color space to generate colors with similar S,V but distributed
4 | // evenly along their Hue. This is fast but not always pretty.
5 | // If you've got time to spare, use Lab (the non-fast below).
6 | func FastHappyPaletteWithRand(colorsCount int, rand RandInterface) (colors []Color) {
7 | colors = make([]Color, colorsCount)
8 |
9 | for i := 0; i < colorsCount; i++ {
10 | colors[i] = Hsv(float64(i)*(360.0/float64(colorsCount)), 0.8+rand.Float64()*0.2, 0.65+rand.Float64()*0.2)
11 | }
12 | return
13 | }
14 |
15 | func FastHappyPalette(colorsCount int) (colors []Color) {
16 | return FastHappyPaletteWithRand(colorsCount, getDefaultGlobalRand())
17 | }
18 |
19 | func HappyPaletteWithRand(colorsCount int, rand RandInterface) ([]Color, error) {
20 | pimpy := func(l, a, b float64) bool {
21 | _, c, _ := LabToHcl(l, a, b)
22 | return 0.3 <= c && 0.4 <= l && l <= 0.8
23 | }
24 | return SoftPaletteExWithRand(colorsCount, SoftPaletteSettings{pimpy, 50, true}, rand)
25 | }
26 |
27 | func HappyPalette(colorsCount int) ([]Color, error) {
28 | return HappyPaletteWithRand(colorsCount, getDefaultGlobalRand())
29 | }
30 |
--------------------------------------------------------------------------------
/hexcolor.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import (
4 | "database/sql/driver"
5 | "encoding/json"
6 | "fmt"
7 | "reflect"
8 | )
9 |
10 | // A HexColor is a Color stored as a hex string "#rrggbb". It implements the
11 | // database/sql.Scanner, database/sql/driver.Value,
12 | // encoding/json.Unmarshaler and encoding/json.Marshaler interfaces.
13 | type HexColor Color
14 |
15 | type errUnsupportedType struct {
16 | got interface{}
17 | want reflect.Type
18 | }
19 |
20 | func (hc *HexColor) Scan(value interface{}) error {
21 | s, ok := value.(string)
22 | if !ok {
23 | return errUnsupportedType{got: reflect.TypeOf(value), want: reflect.TypeOf("")}
24 | }
25 | c, err := Hex(s)
26 | if err != nil {
27 | return err
28 | }
29 | *hc = HexColor(c)
30 | return nil
31 | }
32 |
33 | func (hc *HexColor) Value() (driver.Value, error) {
34 | return Color(*hc).Hex(), nil
35 | }
36 |
37 | func (e errUnsupportedType) Error() string {
38 | return fmt.Sprintf("unsupported type: got %v, want a %s", e.got, e.want)
39 | }
40 |
41 | func (hc *HexColor) UnmarshalJSON(data []byte) error {
42 | var hexCode string
43 | if err := json.Unmarshal(data, &hexCode); err != nil {
44 | return err
45 | }
46 |
47 | var col, err = Hex(hexCode)
48 | if err != nil {
49 | return err
50 | }
51 | *hc = HexColor(col)
52 | return nil
53 | }
54 |
55 | func (hc HexColor) MarshalJSON() ([]byte, error) {
56 | return json.Marshal(Color(hc).Hex())
57 | }
58 |
59 | // Decode - deserialize function for https://github.com/kelseyhightower/envconfig
60 | func (hc *HexColor) Decode(hexCode string) error {
61 | var col, err = Hex(hexCode)
62 | if err != nil {
63 | return err
64 | }
65 | *hc = HexColor(col)
66 | return nil
67 | }
68 |
69 | func (hc HexColor) MarshalYAML() (interface{}, error) {
70 | return Color(hc).Hex(), nil
71 | }
72 |
73 | func (hc *HexColor) UnmarshalYAML(unmarshal func(interface{}) error) error {
74 | var hexCode string
75 | if err := unmarshal(&hexCode); err != nil {
76 | return err
77 | }
78 |
79 | var col, err = Hex(hexCode)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | *hc = HexColor(col)
85 |
86 | return nil
87 | }
88 |
--------------------------------------------------------------------------------
/hexcolor_test.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | func TestHexColor(t *testing.T) {
10 | for _, tc := range []struct {
11 | hc HexColor
12 | s string
13 | }{
14 | {HexColor{R: 0, G: 0, B: 0}, "#000000"},
15 | {HexColor{R: 1, G: 0, B: 0}, "#ff0000"},
16 | {HexColor{R: 0, G: 1, B: 0}, "#00ff00"},
17 | {HexColor{R: 0, G: 0, B: 1}, "#0000ff"},
18 | {HexColor{R: 1, G: 1, B: 1}, "#ffffff"},
19 | } {
20 | var gotHC HexColor
21 | if err := gotHC.Scan(tc.s); err != nil {
22 | t.Errorf("_.Scan(%q) == %v, want ", tc.s, err)
23 | }
24 | if !reflect.DeepEqual(gotHC, tc.hc) {
25 | t.Errorf("_.Scan(%q) wrote %v, want %v", tc.s, gotHC, tc.hc)
26 | }
27 | if gotValue, err := tc.hc.Value(); err != nil || !reflect.DeepEqual(gotValue, tc.s) {
28 | t.Errorf("%v.Value() == %v, %v, want %v, ", tc.hc, gotValue, err, tc.s)
29 | }
30 | }
31 | }
32 |
33 | type CompositeType struct {
34 | Name string `json:"name,omitempty"`
35 | Color HexColor `json:"color,omitempty"`
36 | }
37 |
38 | func TestHexColorCompositeJson(t *testing.T) {
39 | var obj = CompositeType{Name: "John", Color: HexColor{R: 1, G: 0, B: 1}}
40 | var jsonData, err = json.Marshal(obj)
41 | if err != nil {
42 | t.Errorf("json.Marshall(obj) wrote %v", err)
43 | }
44 | var obj2 CompositeType
45 | err = json.Unmarshal(jsonData, &obj2)
46 |
47 | if err != nil {
48 | t.Errorf("json.Unmarshall(%s) wrote %v", jsonData, err)
49 | }
50 |
51 | if !reflect.DeepEqual(obj2, obj) {
52 | t.Errorf("json.Unmarshal(json.Marsrhall(obj)) != obj")
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/hsluv.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import "math"
4 |
5 | // Source: https://github.com/hsluv/hsluv-go
6 | // Under MIT License
7 | // Modified so that Saturation and Luminance are in [0..1] instead of [0..100].
8 |
9 | // HSLuv uses a rounded version of the D65. This has no impact on the final RGB
10 | // values, but to keep high levels of accuracy for internal operations and when
11 | // comparing to the test values, this modified white reference is used internally.
12 | //
13 | // See this GitHub thread for details on these values:
14 | //
15 | // https://github.com/hsluv/hsluv/issues/79
16 | var hSLuvD65 = [3]float64{0.95045592705167, 1.0, 1.089057750759878}
17 |
18 | func LuvLChToHSLuv(l, c, h float64) (float64, float64, float64) {
19 | // [-1..1] but the code expects it to be [-100..100]
20 | c *= 100.0
21 | l *= 100.0
22 |
23 | var s, max float64
24 | if l > 99.9999999 || l < 0.00000001 {
25 | s = 0.0
26 | } else {
27 | max = maxChromaForLH(l, h)
28 | s = c / max * 100.0
29 | }
30 | return h, clamp01(s / 100.0), clamp01(l / 100.0)
31 | }
32 |
33 | func HSLuvToLuvLCh(h, s, l float64) (float64, float64, float64) {
34 | l *= 100.0
35 | s *= 100.0
36 |
37 | var c, max float64
38 | if l > 99.9999999 || l < 0.00000001 {
39 | c = 0.0
40 | } else {
41 | max = maxChromaForLH(l, h)
42 | c = max / 100.0 * s
43 | }
44 |
45 | // c is [-100..100], but for LCh it's supposed to be almost [-1..1]
46 | return clamp01(l / 100.0), c / 100.0, h
47 | }
48 |
49 | func LuvLChToHPLuv(l, c, h float64) (float64, float64, float64) {
50 | // [-1..1] but the code expects it to be [-100..100]
51 | c *= 100.0
52 | l *= 100.0
53 |
54 | var s, max float64
55 | if l > 99.9999999 || l < 0.00000001 {
56 | s = 0.0
57 | } else {
58 | max = maxSafeChromaForL(l)
59 | s = c / max * 100.0
60 | }
61 | return h, s / 100.0, l / 100.0
62 | }
63 |
64 | func HPLuvToLuvLCh(h, s, l float64) (float64, float64, float64) {
65 | // [-1..1] but the code expects it to be [-100..100]
66 | l *= 100.0
67 | s *= 100.0
68 |
69 | var c, max float64
70 | if l > 99.9999999 || l < 0.00000001 {
71 | c = 0.0
72 | } else {
73 | max = maxSafeChromaForL(l)
74 | c = max / 100.0 * s
75 | }
76 | return l / 100.0, c / 100.0, h
77 | }
78 |
79 | // HSLuv creates a new Color from values in the HSLuv color space.
80 | // Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1].
81 | //
82 | // The returned color values are clamped (using .Clamped), so this will never output
83 | // an invalid color.
84 | func HSLuv(h, s, l float64) Color {
85 | // HSLuv -> LuvLCh -> CIELUV -> CIEXYZ -> Linear RGB -> sRGB
86 | l, u, v := LuvLChToLuv(HSLuvToLuvLCh(h, s, l))
87 | return LinearRgb(XyzToLinearRgb(LuvToXyzWhiteRef(l, u, v, hSLuvD65))).Clamped()
88 | }
89 |
90 | // HPLuv creates a new Color from values in the HPLuv color space.
91 | // Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1].
92 | //
93 | // The returned color values are clamped (using .Clamped), so this will never output
94 | // an invalid color.
95 | func HPLuv(h, s, l float64) Color {
96 | // HPLuv -> LuvLCh -> CIELUV -> CIEXYZ -> Linear RGB -> sRGB
97 | l, u, v := LuvLChToLuv(HPLuvToLuvLCh(h, s, l))
98 | return LinearRgb(XyzToLinearRgb(LuvToXyzWhiteRef(l, u, v, hSLuvD65))).Clamped()
99 | }
100 |
101 | // HSLuv returns the Hue, Saturation and Luminance of the color in the HSLuv
102 | // color space. Hue in [0..360], a Saturation [0..1], and a Luminance
103 | // (lightness) in [0..1].
104 | func (col Color) HSLuv() (h, s, l float64) {
105 | // sRGB -> Linear RGB -> CIEXYZ -> CIELUV -> LuvLCh -> HSLuv
106 | return LuvLChToHSLuv(col.LuvLChWhiteRef(hSLuvD65))
107 | }
108 |
109 | // HPLuv returns the Hue, Saturation and Luminance of the color in the HSLuv
110 | // color space. Hue in [0..360], a Saturation [0..1], and a Luminance
111 | // (lightness) in [0..1].
112 | //
113 | // Note that HPLuv can only represent pastel colors, and so the Saturation
114 | // value could be much larger than 1 for colors it can't represent.
115 | func (col Color) HPLuv() (h, s, l float64) {
116 | return LuvLChToHPLuv(col.LuvLChWhiteRef(hSLuvD65))
117 | }
118 |
119 | // DistanceHSLuv calculates Euclidean distance in the HSLuv colorspace. No idea
120 | // how useful this is.
121 | //
122 | // The Hue value is divided by 100 before the calculation, so that H, S, and L
123 | // have the same relative ranges.
124 | func (c1 Color) DistanceHSLuv(c2 Color) float64 {
125 | h1, s1, l1 := c1.HSLuv()
126 | h2, s2, l2 := c2.HSLuv()
127 | return math.Sqrt(sq((h1-h2)/100.0) + sq(s1-s2) + sq(l1-l2))
128 | }
129 |
130 | // DistanceHPLuv calculates Euclidean distance in the HPLuv colorspace. No idea
131 | // how useful this is.
132 | //
133 | // The Hue value is divided by 100 before the calculation, so that H, S, and L
134 | // have the same relative ranges.
135 | func (c1 Color) DistanceHPLuv(c2 Color) float64 {
136 | h1, s1, l1 := c1.HPLuv()
137 | h2, s2, l2 := c2.HPLuv()
138 | return math.Sqrt(sq((h1-h2)/100.0) + sq(s1-s2) + sq(l1-l2))
139 | }
140 |
141 | var m = [3][3]float64{
142 | {3.2409699419045214, -1.5373831775700935, -0.49861076029300328},
143 | {-0.96924363628087983, 1.8759675015077207, 0.041555057407175613},
144 | {0.055630079696993609, -0.20397695888897657, 1.0569715142428786},
145 | }
146 |
147 | const kappa = 903.2962962962963
148 | const epsilon = 0.0088564516790356308
149 |
150 | func maxChromaForLH(l, h float64) float64 {
151 | hRad := h / 360.0 * math.Pi * 2.0
152 | minLength := math.MaxFloat64
153 | for _, line := range getBounds(l) {
154 | length := lengthOfRayUntilIntersect(hRad, line[0], line[1])
155 | if length > 0.0 && length < minLength {
156 | minLength = length
157 | }
158 | }
159 | return minLength
160 | }
161 |
162 | func getBounds(l float64) [6][2]float64 {
163 | var sub2 float64
164 | var ret [6][2]float64
165 | sub1 := math.Pow(l+16.0, 3.0) / 1560896.0
166 | if sub1 > epsilon {
167 | sub2 = sub1
168 | } else {
169 | sub2 = l / kappa
170 | }
171 | for i := range m {
172 | for k := 0; k < 2; k++ {
173 | top1 := (284517.0*m[i][0] - 94839.0*m[i][2]) * sub2
174 | top2 := (838422.0*m[i][2]+769860.0*m[i][1]+731718.0*m[i][0])*l*sub2 - 769860.0*float64(k)*l
175 | bottom := (632260.0*m[i][2]-126452.0*m[i][1])*sub2 + 126452.0*float64(k)
176 | ret[i*2+k][0] = top1 / bottom
177 | ret[i*2+k][1] = top2 / bottom
178 | }
179 | }
180 | return ret
181 | }
182 |
183 | func lengthOfRayUntilIntersect(theta, x, y float64) (length float64) {
184 | length = y / (math.Sin(theta) - x*math.Cos(theta))
185 | return
186 | }
187 |
188 | func maxSafeChromaForL(l float64) float64 {
189 | minLength := math.MaxFloat64
190 | for _, line := range getBounds(l) {
191 | m1 := line[0]
192 | b1 := line[1]
193 | x := intersectLineLine(m1, b1, -1.0/m1, 0.0)
194 | dist := distanceFromPole(x, b1+x*m1)
195 | if dist < minLength {
196 | minLength = dist
197 | }
198 | }
199 | return minLength
200 | }
201 |
202 | func intersectLineLine(x1, y1, x2, y2 float64) float64 {
203 | return (y1 - y2) / (x2 - x1)
204 | }
205 |
206 | func distanceFromPole(x, y float64) float64 {
207 | return math.Sqrt(math.Pow(x, 2.0) + math.Pow(y, 2.0))
208 | }
209 |
--------------------------------------------------------------------------------
/hsluv_test.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | // Source: https://github.com/hsluv/hsluv-go
4 | // Under MIT License
5 | // Modified so that Saturation and Luminance are in [0..1] instead of [0..100],
6 | // and so that it works with this library in general.
7 |
8 | import (
9 | "encoding/json"
10 | "fmt"
11 | "math"
12 | "os"
13 | "testing"
14 | )
15 |
16 | type mapping map[string]values
17 |
18 | type values struct {
19 | Rgb [3]float64
20 | Xyz [3]float64
21 | Luv [3]float64
22 | Lch [3]float64
23 | Hsluv [3]float64
24 | Hpluv [3]float64
25 | }
26 |
27 | func pack(a, b, c float64) [3]float64 {
28 | return [3]float64{a, b, c}
29 | }
30 |
31 | func unpack(tuple [3]float64) (float64, float64, float64) {
32 | return tuple[0], tuple[1], tuple[2]
33 | }
34 |
35 | func fromHex(s string) Color {
36 | c, _ := Hex(s)
37 | return c
38 | }
39 |
40 | // const delta = 0.00000001
41 | const hsluvTestDelta = 0.0000000001 // Two more zeros than the original delta, because values are divided by 100
42 |
43 | func compareTuple(t *testing.T, result, expected [3]float64, method string, hex string) {
44 | var err bool
45 | var errs [3]bool
46 | for i := 0; i < 3; i++ {
47 | if math.Abs(result[i]-expected[i]) > hsluvTestDelta {
48 | err = true
49 | errs[i] = true
50 | }
51 | }
52 | if err {
53 | resultOutput := "["
54 | for i := 0; i < 3; i++ {
55 | resultOutput += fmt.Sprintf("%f", result[i])
56 | if errs[i] {
57 | resultOutput += " *"
58 | }
59 | if i < 2 {
60 | resultOutput += ", "
61 | }
62 | }
63 | resultOutput += "]"
64 | t.Errorf("result: %s expected: %v, testing %s with test case %s", resultOutput, expected, method, hex)
65 | }
66 | }
67 |
68 | func compareHex(t *testing.T, result, expected string, method string, hex string) {
69 | if result != expected {
70 | t.Errorf("result: %v expected: %v, testing %s with test case %s", result, expected, method, hex)
71 | }
72 | }
73 |
74 | func TestHSLuv(t *testing.T) {
75 | snapshotFile, err := os.Open("hsluv-snapshot-rev4.json")
76 | if err != nil {
77 | t.Fatal(err)
78 | }
79 | defer snapshotFile.Close()
80 |
81 | jsonParser := json.NewDecoder(snapshotFile)
82 | snapshot := make(mapping)
83 | if err = jsonParser.Decode(&snapshot); err != nil {
84 | t.Fatal(err)
85 | }
86 |
87 | for hex, colorValues := range snapshot {
88 | // tests for public methods
89 | if testing.Verbose() {
90 | t.Logf("Testing public methods for test case %s", hex)
91 | }
92 |
93 | // Adjust color values to be in the ranges this library uses
94 | colorValues.Hsluv[1] /= 100.0
95 | colorValues.Hsluv[2] /= 100.0
96 | colorValues.Hpluv[1] /= 100.0
97 | colorValues.Hpluv[2] /= 100.0
98 |
99 | compareHex(t, HSLuv(unpack(colorValues.Hsluv)).Hex(), hex, "HsluvToHex", hex)
100 | compareTuple(t, pack(HSLuv(unpack(colorValues.Hsluv)).values()), colorValues.Rgb, "HsluvToRGB", hex)
101 | compareTuple(t, pack(fromHex(hex).HSLuv()), colorValues.Hsluv, "HsluvFromHex", hex)
102 | compareTuple(t, pack(Color{colorValues.Rgb[0], colorValues.Rgb[1], colorValues.Rgb[2]}.HSLuv()), colorValues.Hsluv, "HsluvFromRGB", hex)
103 | compareHex(t, HPLuv(unpack(colorValues.Hpluv)).Hex(), hex, "HpluvToHex", hex)
104 | compareTuple(t, pack(HPLuv(unpack(colorValues.Hpluv)).values()), colorValues.Rgb, "HpluvToRGB", hex)
105 | compareTuple(t, pack(fromHex(hex).HPLuv()), colorValues.Hpluv, "HpluvFromHex", hex)
106 | compareTuple(t, pack(Color{colorValues.Rgb[0], colorValues.Rgb[1], colorValues.Rgb[2]}.HPLuv()), colorValues.Hpluv, "HpluvFromRGB", hex)
107 |
108 | if !testing.Short() {
109 | // internal tests
110 | if testing.Verbose() {
111 | t.Logf("Testing internal methods for test case %s", hex)
112 | }
113 |
114 | // Adjust color values to be in the ranges this library uses
115 | colorValues.Lch[0] /= 100.0
116 | colorValues.Lch[1] /= 100.0
117 | colorValues.Luv[0] /= 100.0
118 | colorValues.Luv[1] /= 100.0
119 | colorValues.Luv[2] /= 100.0
120 |
121 | compareTuple(t, pack(LuvLChWhiteRef(
122 | colorValues.Lch[0], colorValues.Lch[1], colorValues.Lch[2], hSLuvD65,
123 | ).values()), colorValues.Rgb, "convLchRgb", hex)
124 | compareTuple(t, pack(Color{
125 | colorValues.Rgb[0], colorValues.Rgb[1], colorValues.Rgb[2],
126 | }.LuvLChWhiteRef(hSLuvD65)), colorValues.Lch, "convRgbLch", hex)
127 | compareTuple(t, pack(XyzToLuvWhiteRef(
128 | colorValues.Xyz[0], colorValues.Xyz[1], colorValues.Xyz[2], hSLuvD65,
129 | )), colorValues.Luv, "convXyzLuv", hex)
130 | compareTuple(t, pack(LuvToXyzWhiteRef(
131 | colorValues.Luv[0], colorValues.Luv[1], colorValues.Luv[2], hSLuvD65,
132 | )), colorValues.Xyz, "convLuvXyz", hex)
133 | compareTuple(t, pack(LuvToLuvLCh(unpack(colorValues.Luv))), colorValues.Lch, "convLuvLch", hex)
134 | compareTuple(t, pack(LuvLChToLuv(unpack(colorValues.Lch))), colorValues.Luv, "convLchLuv", hex)
135 | compareTuple(t, pack(HSLuvToLuvLCh(unpack(colorValues.Hsluv))), colorValues.Lch, "convHsluvLch", hex)
136 | compareTuple(t, pack(LuvLChToHSLuv(unpack(colorValues.Lch))), colorValues.Hsluv, "convLchHsluv", hex)
137 | compareTuple(t, pack(HPLuvToLuvLCh(unpack(colorValues.Hpluv))), colorValues.Lch, "convHpluvLch", hex)
138 | compareTuple(t, pack(LuvLChToHPLuv(unpack(colorValues.Lch))), colorValues.Hpluv, "convLchHpluv", hex)
139 | compareTuple(t, pack(LinearRgb(XyzToLinearRgb(unpack(colorValues.Xyz))).values()), colorValues.Rgb, "convXyzRgb", hex)
140 | compareTuple(t, pack(Color{colorValues.Rgb[0], colorValues.Rgb[1], colorValues.Rgb[2]}.Xyz()), colorValues.Xyz, "convRgbXyz", hex)
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/rand.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import "math/rand"
4 |
5 | type RandInterface interface {
6 | Float64() float64
7 | Intn(n int) int
8 | }
9 |
10 | type defaultGlobalRand struct{}
11 |
12 | func (df defaultGlobalRand) Float64() float64 {
13 | return rand.Float64()
14 | }
15 |
16 | func (df defaultGlobalRand) Intn(n int) int {
17 | return rand.Intn(n)
18 | }
19 |
20 | func getDefaultGlobalRand() RandInterface {
21 | return defaultGlobalRand{}
22 | }
23 |
--------------------------------------------------------------------------------
/soft_palettegen.go:
--------------------------------------------------------------------------------
1 | // Largely inspired by the descriptions in http://lab.medialab.sciences-po.fr/iwanthue/
2 | // but written from scratch.
3 |
4 | package colorful
5 |
6 | import (
7 | "fmt"
8 | "math"
9 | )
10 |
11 | // The algorithm works in L*a*b* color space and converts to RGB in the end.
12 | // L* in [0..1], a* and b* in [-1..1]
13 | type lab_t struct {
14 | L, A, B float64
15 | }
16 |
17 | type SoftPaletteSettings struct {
18 | // A function which can be used to restrict the allowed color-space.
19 | CheckColor func(l, a, b float64) bool
20 |
21 | // The higher, the better quality but the slower. Usually two figures.
22 | Iterations int
23 |
24 | // Use up to 160000 or 8000 samples of the L*a*b* space (and thus calls to CheckColor).
25 | // Set this to true only if your CheckColor shapes the Lab space weirdly.
26 | ManySamples bool
27 | }
28 |
29 | // Yeah, windows-stype Foo, FooEx, screw you golang...
30 | // Uses K-means to cluster the color-space and return the means of the clusters
31 | // as a new palette of distinctive colors. Falls back to K-medoid if the mean
32 | // happens to fall outside of the color-space, which can only happen if you
33 | // specify a CheckColor function.
34 | func SoftPaletteExWithRand(colorsCount int, settings SoftPaletteSettings, rand RandInterface) ([]Color, error) {
35 |
36 | // Checks whether it's a valid RGB and also fulfills the potentially provided constraint.
37 | check := func(col lab_t) bool {
38 | c := Lab(col.L, col.A, col.B)
39 | return c.IsValid() && (settings.CheckColor == nil || settings.CheckColor(col.L, col.A, col.B))
40 | }
41 |
42 | // Sample the color space. These will be the points k-means is run on.
43 | dl := 0.05
44 | dab := 0.1
45 | if settings.ManySamples {
46 | dl = 0.01
47 | dab = 0.05
48 | }
49 |
50 | samples := make([]lab_t, 0, int(1.0/dl*2.0/dab*2.0/dab))
51 | for l := 0.0; l <= 1.0; l += dl {
52 | for a := -1.0; a <= 1.0; a += dab {
53 | for b := -1.0; b <= 1.0; b += dab {
54 | if check(lab_t{l, a, b}) {
55 | samples = append(samples, lab_t{l, a, b})
56 | }
57 | }
58 | }
59 | }
60 |
61 | // That would cause some infinite loops down there...
62 | if len(samples) < colorsCount {
63 | return nil, fmt.Errorf("palettegen: more colors requested (%v) than samples available (%v). Your requested color count may be wrong, you might want to use many samples or your constraint function makes the valid color space too small", colorsCount, len(samples))
64 | } else if len(samples) == colorsCount {
65 | return labs2cols(samples), nil // Oops?
66 | }
67 |
68 | // We take the initial means out of the samples, so they are in fact medoids.
69 | // This helps us avoid infinite loops or arbitrary cutoffs with too restrictive constraints.
70 | means := make([]lab_t, colorsCount)
71 | for i := 0; i < colorsCount; i++ {
72 | for means[i] = samples[rand.Intn(len(samples))]; in(means, i, means[i]); means[i] = samples[rand.Intn(len(samples))] {
73 | }
74 | }
75 |
76 | clusters := make([]int, len(samples))
77 | samples_used := make([]bool, len(samples))
78 |
79 | // The actual k-means/medoid iterations
80 | for i := 0; i < settings.Iterations; i++ {
81 | // Reassigning the samples to clusters, i.e. to their closest mean.
82 | // By the way, also check if any sample is used as a medoid and if so, mark that.
83 | for isample, sample := range samples {
84 | samples_used[isample] = false
85 | mindist := math.Inf(+1)
86 | for imean, mean := range means {
87 | dist := lab_dist(sample, mean)
88 | if dist < mindist {
89 | mindist = dist
90 | clusters[isample] = imean
91 | }
92 |
93 | // Mark samples which are used as a medoid.
94 | if lab_eq(sample, mean) {
95 | samples_used[isample] = true
96 | }
97 | }
98 | }
99 |
100 | // Compute new means according to the samples.
101 | for imean := range means {
102 | // The new mean is the average of all samples belonging to it.
103 | nsamples := 0
104 | newmean := lab_t{0.0, 0.0, 0.0}
105 | for isample, sample := range samples {
106 | if clusters[isample] == imean {
107 | nsamples++
108 | newmean.L += sample.L
109 | newmean.A += sample.A
110 | newmean.B += sample.B
111 | }
112 | }
113 | if nsamples > 0 {
114 | newmean.L /= float64(nsamples)
115 | newmean.A /= float64(nsamples)
116 | newmean.B /= float64(nsamples)
117 | } else {
118 | // That mean doesn't have any samples? Get a new mean from the sample list!
119 | var inewmean int
120 | for inewmean = rand.Intn(len(samples_used)); samples_used[inewmean]; inewmean = rand.Intn(len(samples_used)) {
121 | }
122 | newmean = samples[inewmean]
123 | samples_used[inewmean] = true
124 | }
125 |
126 | // But now we still need to check whether the new mean is an allowed color.
127 | if nsamples > 0 && check(newmean) {
128 | // It does, life's good (TM)
129 | means[imean] = newmean
130 | } else {
131 | // New mean isn't an allowed color or doesn't have any samples!
132 | // Switch to medoid mode and pick the closest (unused) sample.
133 | // This should always find something thanks to len(samples) >= colorsCount
134 | mindist := math.Inf(+1)
135 | for isample, sample := range samples {
136 | if !samples_used[isample] {
137 | dist := lab_dist(sample, newmean)
138 | if dist < mindist {
139 | mindist = dist
140 | newmean = sample
141 | }
142 | }
143 | }
144 | }
145 | }
146 | }
147 | return labs2cols(means), nil
148 | }
149 |
150 | func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, error) {
151 | return SoftPaletteExWithRand(colorsCount, settings, getDefaultGlobalRand())
152 | }
153 |
154 | // A wrapper which uses common parameters.
155 | func SoftPaletteWithRand(colorsCount int, rand RandInterface) ([]Color, error) {
156 | return SoftPaletteExWithRand(colorsCount, SoftPaletteSettings{nil, 50, false}, rand)
157 | }
158 |
159 | func SoftPalette(colorsCount int) ([]Color, error) {
160 | return SoftPaletteWithRand(colorsCount, getDefaultGlobalRand())
161 | }
162 |
163 | func in(haystack []lab_t, upto int, needle lab_t) bool {
164 | for i := 0; i < upto && i < len(haystack); i++ {
165 | if haystack[i] == needle {
166 | return true
167 | }
168 | }
169 | return false
170 | }
171 |
172 | const LAB_DELTA = 1e-6
173 |
174 | func lab_eq(lab1, lab2 lab_t) bool {
175 | return math.Abs(lab1.L-lab2.L) < LAB_DELTA &&
176 | math.Abs(lab1.A-lab2.A) < LAB_DELTA &&
177 | math.Abs(lab1.B-lab2.B) < LAB_DELTA
178 | }
179 |
180 | // That's faster than using colorful's DistanceLab since we would have to
181 | // convert back and forth for that. Here is no conversion.
182 | func lab_dist(lab1, lab2 lab_t) float64 {
183 | return math.Sqrt(sq(lab1.L-lab2.L) + sq(lab1.A-lab2.A) + sq(lab1.B-lab2.B))
184 | }
185 |
186 | func labs2cols(labs []lab_t) (cols []Color) {
187 | cols = make([]Color, len(labs))
188 | for k, v := range labs {
189 | cols[k] = Lab(v.L, v.A, v.B)
190 | }
191 | return cols
192 | }
193 |
--------------------------------------------------------------------------------
/soft_palettegen_test.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | // This is really difficult to test, if you've got a good idea, pull request!
9 |
10 | // Check if it returns all valid and enough colors.
11 | func TestColorCount(t *testing.T) {
12 | fmt.Printf("Testing up to %v palettes may take a while...\n", 100)
13 | for i := 0; i < 100; i++ {
14 | //pal, err := SoftPaletteEx(i, SoftPaletteGenSettings{nil, 50, true})
15 | pal, err := SoftPalette(i)
16 | if err != nil {
17 | t.Errorf("Error: %v", err)
18 | }
19 |
20 | // Check the color count of the palette
21 | if len(pal) != i {
22 | t.Errorf("Requested %v colors but got %v", i, len(pal))
23 | }
24 |
25 | // Also check whether all colors exist in RGB space.
26 | for icol, col := range pal {
27 | if !col.IsValid() {
28 | t.Errorf("Color %v in palette of %v is invalid: %v", icol, len(pal), col)
29 | }
30 | }
31 | }
32 | fmt.Println("Done with that, but more tests to run.")
33 | }
34 |
35 | // Check if it errors-out on an impossible constraint
36 | func TestImpossibleConstraint(t *testing.T) {
37 | never := func(l, a, b float64) bool { return false }
38 |
39 | pal, err := SoftPaletteEx(10, SoftPaletteSettings{never, 50, true})
40 | if err == nil || pal != nil {
41 | t.Error("Should error-out on impossible constraint!")
42 | }
43 | }
44 |
45 | // Check whether the constraint is respected
46 | func TestConstraint(t *testing.T) {
47 | octant := func(l, a, b float64) bool { return l <= 0.5 && a <= 0.0 && b <= 0.0 }
48 |
49 | pal, err := SoftPaletteEx(100, SoftPaletteSettings{octant, 50, true})
50 | if err != nil {
51 | t.Errorf("Error: %v", err)
52 | }
53 |
54 | // Check ALL the colors!
55 | for icol, col := range pal {
56 | if !col.IsValid() {
57 | t.Errorf("Color %v in constrained palette is invalid: %v", icol, col)
58 | }
59 |
60 | l, a, b := col.Lab()
61 | if l > 0.5 || a > 0.0 || b > 0.0 {
62 | t.Errorf("Color %v in constrained palette violates the constraint: %v (lab: %v)", icol, col, [3]float64{l, a, b})
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/sort.go:
--------------------------------------------------------------------------------
1 | // This file provides functions for sorting colors.
2 |
3 | package colorful
4 |
5 | import (
6 | "math"
7 | "sort"
8 | )
9 |
10 | // An element represents a single element of a set. It is used to
11 | // implement a disjoint-set forest.
12 | type element struct {
13 | parent *element // Parent element
14 | rank int // Rank (approximate depth) of the subtree with this element as root
15 | }
16 |
17 | // newElement creates a singleton set and returns its sole element.
18 | func newElement() *element {
19 | s := &element{}
20 | s.parent = s
21 | return s
22 | }
23 |
24 | // find returns an arbitrary element of a set when invoked on any element of
25 | // the set, The important feature is that it returns the same value when
26 | // invoked on any element of the set. Consequently, it can be used to test if
27 | // two elements belong to the same set.
28 | func (e *element) find() *element {
29 | for e.parent != e {
30 | e.parent = e.parent.parent
31 | e = e.parent
32 | }
33 | return e
34 | }
35 |
36 | // union establishes the union of two sets when given an element from each set.
37 | // Afterwards, the original sets no longer exist as separate entities.
38 | func union(e1, e2 *element) {
39 | // Ensure the two elements aren't already part of the same union.
40 | e1Root := e1.find()
41 | e2Root := e2.find()
42 | if e1Root == e2Root {
43 | return
44 | }
45 |
46 | // Create a union by making the shorter tree point to the root of the
47 | // larger tree.
48 | switch {
49 | case e1Root.rank < e2Root.rank:
50 | e1Root.parent = e2Root
51 | case e1Root.rank > e2Root.rank:
52 | e2Root.parent = e1Root
53 | default:
54 | e2Root.parent = e1Root
55 | e1Root.rank++
56 | }
57 | }
58 |
59 | // An edgeIdxs describes an edge in a graph or tree. The vertices in the edge
60 | // are indexes into a list of Color values.
61 | type edgeIdxs [2]int
62 |
63 | // An edgeDistance is a map from an edge (pair of indices) to a distance
64 | // between the two vertices.
65 | type edgeDistance map[edgeIdxs]float64
66 |
67 | // allToAllDistancesCIEDE2000 computes the CIEDE2000 distance between each pair of
68 | // colors. It returns a map from a pair of indices (u, v) with u < v to a
69 | // distance.
70 | func allToAllDistancesCIEDE2000(cs []Color) edgeDistance {
71 | nc := len(cs)
72 | m := make(edgeDistance, nc*nc)
73 | for u := 0; u < nc-1; u++ {
74 | for v := u + 1; v < nc; v++ {
75 | m[edgeIdxs{u, v}] = cs[u].DistanceCIEDE2000(cs[v])
76 | }
77 | }
78 | return m
79 | }
80 |
81 | // sortEdges sorts all edges in a distance map by increasing vertex distance.
82 | func sortEdges(m edgeDistance) []edgeIdxs {
83 | es := make([]edgeIdxs, 0, len(m))
84 | for uv := range m {
85 | es = append(es, uv)
86 | }
87 | sort.Slice(es, func(i, j int) bool {
88 | return m[es[i]] < m[es[j]]
89 | })
90 | return es
91 | }
92 |
93 | // minSpanTree computes a minimum spanning tree from a vertex count and a
94 | // distance-sorted edge list. It returns the subset of edges that belong to
95 | // the tree, including both (u, v) and (v, u) for each edge.
96 | func minSpanTree(nc int, es []edgeIdxs) map[edgeIdxs]struct{} {
97 | // Start with each vertex in its own set.
98 | elts := make([]*element, nc)
99 | for i := range elts {
100 | elts[i] = newElement()
101 | }
102 |
103 | // Run Kruskal's algorithm to construct a minimal spanning tree.
104 | mst := make(map[edgeIdxs]struct{}, nc)
105 | for _, uv := range es {
106 | u, v := uv[0], uv[1]
107 | if elts[u].find() == elts[v].find() {
108 | continue // Same set: edge would introduce a cycle.
109 | }
110 | mst[uv] = struct{}{}
111 | mst[edgeIdxs{v, u}] = struct{}{}
112 | union(elts[u], elts[v])
113 | }
114 | return mst
115 | }
116 |
117 | // traverseMST walks a minimum spanning tree in prefix order.
118 | func traverseMST(mst map[edgeIdxs]struct{}, root int) []int {
119 | // Compute a list of neighbors for each vertex.
120 | neighs := make(map[int][]int, len(mst))
121 | for uv := range mst {
122 | u, v := uv[0], uv[1]
123 | neighs[u] = append(neighs[u], v)
124 | }
125 | for u, vs := range neighs {
126 | sort.Ints(vs)
127 | copy(neighs[u], vs)
128 | }
129 |
130 | // Walk the tree from a given vertex.
131 | order := make([]int, 0, len(neighs))
132 | visited := make(map[int]bool, len(neighs))
133 | var walkFrom func(int)
134 | walkFrom = func(r int) {
135 | // Visit the starting vertex.
136 | order = append(order, r)
137 | visited[r] = true
138 |
139 | // Recursively visit each child in turn.
140 | for _, c := range neighs[r] {
141 | if !visited[c] {
142 | walkFrom(c)
143 | }
144 | }
145 | }
146 | walkFrom(root)
147 | return order
148 | }
149 |
150 | // Sorted sorts a list of Color values. Sorting is not a well-defined operation
151 | // for colors so the intention here primarily is to order colors so that the
152 | // transition from one to the next is fairly smooth.
153 | func Sorted(cs []Color) []Color {
154 | // Do nothing in trivial cases.
155 | newCs := make([]Color, len(cs))
156 | if len(cs) < 2 {
157 | copy(newCs, cs)
158 | return newCs
159 | }
160 |
161 | // Compute the distance from each color to every other color.
162 | dists := allToAllDistancesCIEDE2000(cs)
163 |
164 | // Produce a list of edges in increasing order of the distance between
165 | // their vertices.
166 | edges := sortEdges(dists)
167 |
168 | // Construct a minimum spanning tree from the list of edges.
169 | mst := minSpanTree(len(cs), edges)
170 |
171 | // Find the darkest color in the list.
172 | var black Color
173 | var dIdx int // Index of darkest color
174 | light := math.MaxFloat64 // Lightness of darkest color (distance from black)
175 | for i, c := range cs {
176 | d := black.DistanceCIEDE2000(c)
177 | if d < light {
178 | dIdx = i
179 | light = d
180 | }
181 | }
182 |
183 | // Traverse the tree starting from the darkest color.
184 | idxs := traverseMST(mst, dIdx)
185 |
186 | // Convert the index list to a list of colors, overwriting the input.
187 | for i, idx := range idxs {
188 | newCs[i] = cs[idx]
189 | }
190 | return newCs
191 | }
192 |
--------------------------------------------------------------------------------
/sort_test.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | import "testing"
4 |
5 | // TestSortSimple tests the sorting of a small set of colors.
6 | func TestSortSimple(t *testing.T) {
7 | // Sort a list of reds and blues.
8 | in := make([]Color, 0, 6)
9 | for i := 0; i < 3; i++ {
10 | in = append(in, Color{1.0 - float64(i+1)*0.25, 0.0, 0.0}) // Reds
11 | in = append(in, Color{0.0, 0.0, 1.0 - float64(i+1)*0.25}) // Blues
12 | }
13 | out := Sorted(in)
14 |
15 | // Ensure the output matches what we expected.
16 | exp := []Color{
17 | {R: 0.25, G: 0.0, B: 0},
18 | {R: 0.50, G: 0.0, B: 0},
19 | {R: 0.75, G: 0.0, B: 0},
20 | {R: 0.0, G: 0.0, B: 0.25},
21 | {R: 0.0, G: 0.0, B: 0.50},
22 | {R: 0.0, G: 0.0, B: 0.75},
23 | }
24 | for i, e := range exp {
25 | if out[i] != e {
26 | t.Fatalf("Expected %v but saw %v", e, out[i])
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/warm_palettegen.go:
--------------------------------------------------------------------------------
1 | package colorful
2 |
3 | // Uses the HSV color space to generate colors with similar S,V but distributed
4 | // evenly along their Hue. This is fast but not always pretty.
5 | // If you've got time to spare, use Lab (the non-fast below).
6 | func FastWarmPaletteWithRand(colorsCount int, rand RandInterface) (colors []Color) {
7 | colors = make([]Color, colorsCount)
8 |
9 | for i := 0; i < colorsCount; i++ {
10 | colors[i] = Hsv(float64(i)*(360.0/float64(colorsCount)), 0.55+rand.Float64()*0.2, 0.35+rand.Float64()*0.2)
11 | }
12 | return
13 | }
14 |
15 | func FastWarmPalette(colorsCount int) (colors []Color) {
16 | return FastWarmPaletteWithRand(colorsCount, getDefaultGlobalRand())
17 | }
18 |
19 | func WarmPaletteWithRand(colorsCount int, rand RandInterface) ([]Color, error) {
20 | warmy := func(l, a, b float64) bool {
21 | _, c, _ := LabToHcl(l, a, b)
22 | return 0.1 <= c && c <= 0.4 && 0.2 <= l && l <= 0.5
23 | }
24 | return SoftPaletteExWithRand(colorsCount, SoftPaletteSettings{warmy, 50, true}, rand)
25 | }
26 |
27 | func WarmPalette(colorsCount int) ([]Color, error) {
28 | return WarmPaletteWithRand(colorsCount, getDefaultGlobalRand())
29 | }
30 |
--------------------------------------------------------------------------------