├── .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 | [![Go Reference](https://pkg.go.dev/badge/github.com/lucasb-eyer/go-colorful.svg)](https://pkg.go.dev/github.com/lucasb-eyer/go-colorful) 5 | [![go reportcard](https://goreportcard.com/badge/github.com/lucasb-eyer/go-colorful)](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 | ![Color distance comparison](doc/colordist/colordist.png) 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 | ![Blending colors in different spaces.](doc/colorblend/colorblend.png) 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 | ![Invalid RGB colors may crop up when blending in CIE spaces.](doc/colorblend/invalid.png) 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 | ![Fixing invalid RGB colors by clamping them to the valid range.](doc/colorblend/clamped.png) 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 | !["Spectral" colorbrewer gradient in HCL space.](doc/gradientgen/gradientgen.png) 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 | ![Warm, fast warm, happy and fast happy random colors, respectively.](doc/colorgens/colorgens.png) 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 | ![All example palettes](doc/palettegens/palettegens.png) 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 | ![Sorting colors](doc/colorsort/colorsort.png) 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 | ![approximation quality](doc/approx-quality.png) 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 | --------------------------------------------------------------------------------