├── red-blue.png ├── white-black.png ├── white-blue.png ├── greyred-blue.png ├── go.mod ├── go.sum ├── .gitignore ├── README.md ├── LICENSE ├── .github └── workflows │ └── go.yml ├── examples └── lerp │ └── lerp.go ├── color_test.go └── color.go /red-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soypat/colorspace/HEAD/red-blue.png -------------------------------------------------------------------------------- /white-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soypat/colorspace/HEAD/white-black.png -------------------------------------------------------------------------------- /white-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soypat/colorspace/HEAD/white-blue.png -------------------------------------------------------------------------------- /greyred-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soypat/colorspace/HEAD/greyred-blue.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/soypat/colorspace 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/chewxy/math32 v1.11.1 7 | github.com/soypat/geometry v0.0.0-20250718121518-cc41fcad0ec7 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= 2 | github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= 3 | github.com/soypat/geometry v0.0.0-20250618223846-196a4c63e8ef h1:7SMdeUipfzssy+Yl06GOVMKpmT1u2ZL/r0nVfGAEMBE= 4 | github.com/soypat/geometry v0.0.0-20250618223846-196a4c63e8ef/go.mod h1:ks6uGIIvsbkaGB7RBLG8pc2yHYBIxZ1FSRSNPtXPAwY= 5 | github.com/soypat/geometry v0.0.0-20250718121518-cc41fcad0ec7 h1:+fKPpSStUmn9WAtq4LDzj3QsbWzdxeUi6EH4rShvH7E= 6 | github.com/soypat/geometry v0.0.0-20250718121518-cc41fcad0ec7/go.mod h1:ks6uGIIvsbkaGB7RBLG8pc2yHYBIxZ1FSRSNPtXPAwY= 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Go workspaces. 2 | go.work 3 | go.work.sum 4 | # Test binary, built with `go test -c` 5 | *.test 6 | # Dependency directories after running `go mod vendor` 7 | vendor/ 8 | 9 | # Binaries for programs and plugins 10 | *.elf 11 | *.uf2 12 | *.exe 13 | *.exe~ 14 | *.dll 15 | *.so 16 | *.dylib 17 | *.hex 18 | 19 | # `__debug_bin` Debug binary generated in VSCode when using the built-in debugger. 20 | **__debug_bin* 21 | *bin 22 | 23 | # IDE 24 | .vscode/ 25 | 26 | # For local development and testing create `local` directories. 27 | local 28 | 29 | # Output of the go coverage tool, specifically when used with LiteIDE 30 | *.out 31 | *.cov 32 | 33 | # Log files and common data formats. 34 | *.log 35 | *.Log 36 | *.LOG 37 | *.csv 38 | *.tsv 39 | *.dat 40 | *.json 41 | *.xlsx 42 | *.xls 43 | *.zip 44 | *.gz 45 | *.tar 46 | 47 | # If running a python script. 48 | */__pycache__/* 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # colorspace 2 | [![go.dev reference](https://pkg.go.dev/badge/github.com/soypat/colorspace)](https://pkg.go.dev/github.com/soypat/colorspace) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/soypat/colorspace)](https://goreportcard.com/report/github.com/soypat/colorspace) 4 | [![codecov](https://codecov.io/gh/soypat/colorspace/branch/main/graph/badge.svg)](https://codecov.io/gh/soypat/colorspace) 5 | [![Go](https://github.com/soypat/colorspace/actions/workflows/go.yml/badge.svg)](https://github.com/soypat/colorspace/actions/workflows/go.yml) 6 | [![sourcegraph](https://sourcegraph.com/github.com/soypat/colorspace/-/badge.svg)](https://sourcegraph.com/github.com/soypat/colorspace?badge) 7 | 8 | colorspace implements different color space logic to allow for conversion from colorspace to colorspace and interpolation within each colorspace. 9 | 10 | How to install package with newer versions of Go (+1.16): 11 | ```sh 12 | go mod download github.com/soypat/colorspace@latest 13 | ``` 14 | 15 | ## Linear interpolation example 16 | Shown in each image are 5 different color gradients generated with linear interpolation in each available colorspace. See [`examples/lerp`](./examples/lerp/lerp.go): 17 | 1. Topmost: **sRGB**. This is the naive linear interpolation 18 | 2. **Linear sRGB**. 19 | 3. **CIE XYZ** 20 | 4. **OKLAB** 21 | 5. **OKLCH**. Designed to yield the most perceptively uniform gradient. 22 | 23 | 24 | ![greyred to blue lerp](./greyred-blue.png) 25 | ![red to blue lerp](./red-blue.png) 26 | ![white to blue lerp](./white-blue.png) 27 | ![white to black lerp](./white-black.png) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Patricio Whittingslow 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.21 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | # - name: govulncheck # Disabled due to lots of false positives due to mismatching tooling versions. If enabled set all tooling Go versions to same version! 28 | # uses: golang/govulncheck-action@v1 29 | # with: 30 | # go-version-input: 1.21 31 | # go-package: ./... 32 | 33 | - name: Test 34 | run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 35 | 36 | - name: Codecov upload coverage 37 | shell: bash 38 | env: 39 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 40 | RUN_ID: ${{ github.run_id }} 41 | run: | 42 | # Replace `linux` below with the appropriate OS 43 | # Options are `alpine`, `linux`, `macos`, `windows` 44 | # You will need to setup the environment variables below in github 45 | # and the project in codecov.io: https://app.codecov.io/gh/${{ github.repository }} 46 | go test -v -coverprofile=coverage.txt -covermode=atomic ./... 47 | curl -Os https://uploader.codecov.io/latest/linux/codecov 48 | chmod +x codecov 49 | ./codecov --verbose upload-process --fail-on-error -t $CODECOV_TOKEN -n 'service'-$RUN_ID -F service -f coverage.txt 50 | -------------------------------------------------------------------------------- /examples/lerp/lerp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/png" 8 | "os" 9 | 10 | "github.com/soypat/colorspace" 11 | ) 12 | 13 | var splitcolor = color.RGBA{R: 255, A: 255} 14 | 15 | func main() { 16 | const width, height = 700, 50 17 | const dx = 1. / width 18 | imgHeight := (height + 1) * len(lerps) // 1 pixel for differentiating lerps. 19 | for _, crange := range ranges { 20 | img := image.NewRGBA64(image.Rect(0, 0, width, imgHeight)) 21 | for ilerp, lerp := range lerps { 22 | flerp := lerp.F 23 | c1, c2 := crange.c1, crange.c2 24 | for ix := 0; ix < width; ix++ { 25 | x := float32(ix) * dx 26 | c := flerp(c1, c2, x) 27 | yoff := ilerp * (height + 1) 28 | for iy := yoff; iy < yoff+height; iy++ { 29 | img.Set(ix, iy, c) 30 | } 31 | img.Set(ix, yoff+height, splitcolor) 32 | } 33 | } 34 | name := crange.name + ".png" 35 | fp, _ := os.Create(name) 36 | png.Encode(fp, img) 37 | fp.Close() 38 | fmt.Println("finished generating", name) 39 | } 40 | } 41 | 42 | type colorrange struct { 43 | name string 44 | c1, c2 color.RGBA 45 | } 46 | 47 | var ranges = []colorrange{ 48 | {name: "white-black", c1: color.RGBA{R: 255, G: 255, B: 255}, c2: color.RGBA{}}, 49 | {name: "white-blue", c1: color.RGBA{R: 255, G: 255, B: 255}, c2: color.RGBA{B: 255}}, 50 | {name: "red-blue", c1: color.RGBA{R: 255}, c2: color.RGBA{B: 255}}, 51 | {name: "greyred-blue", c1: color.RGBA{R: 160, G: 127, B: 127}, c2: color.RGBA{B: 255}}, 52 | } 53 | 54 | type Lerp struct { 55 | Name string 56 | F func(c1, c2 color.Color, v float32) color.Color 57 | } 58 | 59 | var lerps = []Lerp{ 60 | { 61 | Name: "sRGB", 62 | F: colorspace.LerpSRGB, 63 | }, 64 | { 65 | Name: "lin-sRGB", 66 | F: colorspace.LerpLSRGB, 67 | }, 68 | { 69 | Name: "CIE-XYZ", 70 | F: colorspace.LerpCIEXYZ, 71 | }, 72 | { 73 | Name: "OKLAB", 74 | F: colorspace.LerpOKLAB, 75 | }, 76 | { 77 | Name: "OKLCH", 78 | F: colorspace.LerpOKLCH, 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /color_test.go: -------------------------------------------------------------------------------- 1 | package colorspace 2 | 3 | import ( 4 | "image/color" 5 | "math/rand" 6 | "testing" 7 | ) 8 | 9 | func TestBasic(t *testing.T) { 10 | red := SRGB{R: 1, G: 0, B: 0} 11 | redlsrgb := red.LSRGB() 12 | wantlsrgb := LSRGB{R: 1, G: 0, B: 0} 13 | if redlsrgb != wantlsrgb { 14 | t.Errorf("lsrgb for red mismatch, want %v, got %v", wantlsrgb, redlsrgb) 15 | } 16 | redxyz := redlsrgb.CIEXYZ() 17 | wantredxyz := CIEXYZ{X: 0.41239079926595934, Y: 0.21263900587151027, Z: 0.01933081871559182} 18 | if redxyz != wantredxyz { 19 | t.Errorf("xyz for red not match: want %v, got %v", wantredxyz, redxyz) 20 | } 21 | redoklab := redxyz.OKLAB() 22 | expectoklab := OKLAB{ 23 | L: 0.6279553639214311, 24 | A: 0.2248630684262744, 25 | B: 0.125846277330585, 26 | } 27 | if expectoklab.DeltaE(redoklab) > 0.0001 { 28 | t.Errorf("mismatch oklab for red: got %v, want %v", redoklab, expectoklab) 29 | } 30 | } 31 | 32 | func TestColor(t *testing.T) { 33 | rng := rand.New(rand.NewSource(1)) 34 | palette := jet 35 | for i := 0; i < 10; i++ { 36 | idx1 := rng.Intn(len(palette)) 37 | idx2 := rng.Intn(len(palette)) 38 | v := rng.Float32() 39 | c := LerpOKLAB(palette[idx1], palette[idx2], v) 40 | _ = c 41 | } 42 | } 43 | 44 | var jet = color.Palette{ 45 | color.RGBA64{R: 0x8180, G: 0xe0e, B: 0x707, A: 0xffff}, 46 | color.RGBA64{R: 0xa3a3, G: 0x1413, B: 0x808, A: 0xffff}, 47 | color.RGBA64{R: 0xc1c1, G: 0x1f1f, B: 0xd0c, A: 0xffff}, 48 | color.RGBA64{R: 0xc6c6, G: 0x2221, B: 0xe0e, A: 0xffff}, 49 | color.RGBA64{R: 0xe1e1, G: 0x3636, B: 0x1616, A: 0xffff}, 50 | color.RGBA64{R: 0xebeb, G: 0x403f, B: 0x1918, A: 0xffff}, 51 | color.RGBA64{R: 0xffff, G: 0x6464, B: 0x2726, A: 0xffff}, 52 | color.RGBA64{R: 0xffff, G: 0x8282, B: 0x3434, A: 0xffff}, 53 | color.RGBA64{R: 0xffff, G: 0x9f9e, B: 0x4141, A: 0xffff}, 54 | color.RGBA64{R: 0xffff, G: 0xa2a1, B: 0x4242, A: 0xffff}, 55 | color.RGBA64{R: 0xffff, G: 0xc1c1, B: 0x4e4d, A: 0xffff}, 56 | color.RGBA64{R: 0xdedd, G: 0xe0df, B: 0x5150, A: 0xffff}, 57 | color.RGBA64{R: 0xbfbf, G: 0xf1f0, B: 0x5352, A: 0xffff}, 58 | color.RGBA64{R: 0x9d9c, G: 0xf9f9, B: 0x5857, A: 0xffff}, 59 | color.RGBA64{R: 0x7d7c, G: 0xf9f9, B: 0x6160, A: 0xffff}, 60 | color.RGBA64{R: 0x5e5d, G: 0xf9f9, B: 0x6d6d, A: 0xffff}, 61 | color.RGBA64{R: 0x3b3b, G: 0xf9f9, B: 0x7979, A: 0xffff}, 62 | color.RGBA64{R: 0x1a1a, G: 0xf9f9, B: 0x8180, A: 0xffff}, 63 | color.RGBA64{R: 0x0, G: 0xdcdb, B: 0xc2c1, A: 0xffff}, 64 | color.RGBA64{R: 0x0, G: 0xbfbf, B: 0xe5e4, A: 0xffff}, 65 | color.RGBA64{R: 0x0, G: 0x9f9e, B: 0xf9f9, A: 0xffff}, 66 | color.RGBA64{R: 0x201f, G: 0x8787, B: 0xf5f4, A: 0xffff}, 67 | color.RGBA64{R: 0x2726, G: 0x7f7e, B: 0xefef, A: 0xffff}, 68 | color.RGBA64{R: 0x3636, G: 0x5e5d, B: 0xcaca, A: 0xffff}, 69 | color.RGBA64{R: 0x3838, G: 0x5554, B: 0xbebd, A: 0xffff}, 70 | color.RGBA64{R: 0x3a3a, G: 0x3d3d, B: 0x9393, A: 0xffff}, 71 | color.RGBA64{R: 0x3838, G: 0x3131, B: 0x7d7c, A: 0xffff}, 72 | color.RGBA64{R: 0x3636, G: 0x1f1f, B: 0x5656, A: 0xffff}, 73 | color.RGBA64{R: 0x3333, G: 0x1313, B: 0x3938, A: 0xffff}, 74 | } 75 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package colorspace 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/chewxy/math32" 7 | "github.com/soypat/geometry/ms1" 8 | "github.com/soypat/geometry/ms3" 9 | ) 10 | 11 | const ( 12 | undefinedHue = 0.0 13 | epsUnit = 1e-5 14 | d50x, d50z = 0.3457 / 0.3585, (1.0 - 0.3457 - 0.3585) / 0.3585 15 | d65x, d65z = 0.3127 / 0.3290, (1.0 - 0.3127 - 0.3290) / 0.3290 16 | ) 17 | 18 | // IlluminantD65 returns the standard illuminant which represents noon daylight (D65). 19 | // Values are normalized to the y value provided. 20 | func IlluminantD65(ynormal float32) CIEXYZ { 21 | return CIEXYZ{X: ynormal * d65x, Y: ynormal, Z: d65z * ynormal} 22 | } 23 | 24 | // IlluminantD50 returns the standard illuminant representing horizon light (D50). 25 | // Values are normalized to the y value provided. 26 | func IlluminantD50(ynormal float32) CIEXYZ { 27 | return CIEXYZ{X: ynormal * d50x, Y: ynormal, Z: d50z * ynormal} 28 | } 29 | 30 | // Illuminant returns a standard illuminant given the CIE chromaticity coordinates of a perfectly 31 | // reflecting (or transmitting) diffuser. 32 | func Illuminant(ynormal, xchroma, ychroma float32) CIEXYZ { 33 | xmul := xchroma / ychroma 34 | zmul := (1 - xchroma - ychroma) / ychroma 35 | return CIEXYZ{ 36 | X: ynormal * xmul, 37 | Y: ynormal, 38 | Z: ynormal * zmul, 39 | } 40 | } 41 | 42 | var ( 43 | // standard white points, defined by 4-figure CIE x,y chromaticities 44 | d50 = IlluminantD50(1).vec() 45 | 46 | // Transposed due to being defined in column major format. 47 | linSRGBToXYZ = ms3.NewMat3([]float32{ 48 | 506752. / 1228815, 87881. / 245763, 12673. / 70218, 49 | 87098. / 409605, 175762. / 245763, 12673. / 175545, 50 | 7918. / 409605, 87881. / 737289, 1001167. / 1053270, 51 | }) 52 | xyzToLinSRGB = ms3.NewMat3([]float32{12831. / 3959, -329. / 214, -1974. / 3959, 53 | -851781. / 878810, 1648619. / 878810, 36519. / 878810, 54 | 705. / 12673, -2585. / 12673, 705. / 667, 55 | }) 56 | d65Tod50 = ms3.NewMat3([]float32{1.0479297925449969, 0.022946870601609652, -0.05019226628920524, 57 | 0.02962780877005599, 0.9904344267538799, -0.017073799063418826, 58 | -0.009243040646204504, 0.015055191490298152, 0.7518742814281371}) 59 | d50Tod65 = ms3.NewMat3([]float32{0.955473421488075, -0.02309845494876471, 0.06325924320057072, 60 | -0.0283697093338637, 1.0099953980813041, 0.021041441191917323, 61 | 0.012314014864481998, -0.020507649298898964, 1.330365926242124}) 62 | xyzToLMS = ms3.NewMat3([]float32{0.8190224379967030, 0.3619062600528904, -0.1288737815209879, 63 | 0.0329836539323885, 0.9292868615863434, 0.0361446663506424, 64 | 0.0481771893596242, 0.2642395317527308, 0.6335478284694309}) 65 | lmsToOKLAB = ms3.NewMat3([]float32{0.2104542683093140, 0.7936177747023054, -0.0040720430116193, 66 | 1.9779985324311684, -2.4285922420485799, 0.4505937096174110, 67 | 0.0259040424655478, 0.7827717124575296, -0.8086757549230774}) 68 | lmsToXYZ = ms3.NewMat3([]float32{1.2268798758459243, -0.5578149944602171, 0.2813910456659647, 69 | -0.0405757452148008, 1.1122868032803170, -0.0717110580655164, 70 | -0.0763729366746601, -0.4214933324022432, 1.5869240198367816}) 71 | oklabToLMS = ms3.NewMat3([]float32{1.0000000000000000, 0.3963377773761749, 0.2158037573099136, 72 | 1.0000000000000000, -0.1055613458156586, -0.0638541728258133, 73 | 1.0000000000000000, -0.0894841775298119, -1.2914855480194092}) 74 | ) 75 | 76 | // OKLAB is a uniform color space for device independent coloring designed to improve preceptual uniformity, 77 | // hue and lightness prediction, color blending and usability regarding numerical stability. 78 | type OKLAB struct { 79 | // Preceptual lightness. See [LAB] 80 | L float32 81 | // A and B for opposite channels of the four unique hues. unbounded but in practice ranging from -0.5 to 0.5. 82 | // CSS assigns ±100% to ±0.4 for both. 83 | A float32 84 | B float32 85 | } 86 | 87 | // OKLCH is cylindrical representation of [OKLAB] color space. 88 | type OKLCH struct { 89 | L float32 // Perceptual luminosity. Same as for [OKLAB]. 90 | C float32 // Chroma. Defines intensity of hue. 91 | H float32 // Hue in degrees. 92 | } 93 | 94 | // CIELCH is the cylindiracl hue color space representation of CIELAB. 95 | type CIELCH struct { 96 | L float32 // Perceptual luminosity. Same as for [CIELAB]. 97 | C float32 // chroma. 98 | H float32 // hue. 99 | } 100 | 101 | // CIELAB or also known as LAB, is a color model defined by the international commission on illumination (CIE) in 1976. 102 | // It is designed so that a given numerical change always corresponds to a similar preceived change in color. 103 | // Since a* and b* axes are unbounded a correct CIELAB color may not be representable in sRGB gamut. 104 | type CIELAB struct { 105 | // L* (L-star) Perceptual Lightness calcuilated using the cube root of relative luminance with an offset near black. 106 | // Defines black at 0 and white at 1. 107 | L float32 108 | // a* axis (unbounded) Varies greenish appearance. 109 | A float32 110 | // b* axis (unbounded) varies red/green/yellow to blue. 111 | B float32 112 | } 113 | 114 | // SRGB usually denoted as sRGB (standard Red-Green-Blue) is a color space for use on printers, monitors and the world wide web. 115 | // This is to say most monitors/printers receive color data as a triplets for each pixel representing redness, greeness and blueness. 116 | type SRGB struct { 117 | R float32 // Red. 118 | G float32 // Green. 119 | B float32 // Blue. 120 | } 121 | 122 | // LSRGB is linear-light (un-companded) color space. 123 | type LSRGB struct { 124 | R float32 // Red. 125 | G float32 // Green. 126 | B float32 // Blue. 127 | } 128 | 129 | // CIEXYZ refers to the 1931 CIE color space defined such that a mixture between two colors 130 | // in some proportion lies on the line between those two colors in this space. One disadvantage 131 | // of this model is that it is not perceptually uniform. The disadvantage is remedied in subsequent color models 132 | // such as CIELUV and [CIELAB]. 133 | // The XYZ values are also called tristiumulants. 134 | type CIEXYZ struct { 135 | X, Y, Z float32 136 | } 137 | 138 | // HSV is the Hue–Saturation–Value cylindrical-coordinate color space. 139 | // It is often used in user interfaces and color pickers because it aligns 140 | // more closely with human perception of color attributes than RGB. 141 | // 142 | // Components: 143 | // - H (Hue): The angle of the color on the color wheel, measured in degrees. 144 | // Range is [0, 360). Red = 0°, Green = 120°, Blue = 240°. 145 | // Undefined for achromatic colors (S = 0). 146 | // - S (Saturation): The intensity or purity of the hue, in [0, 1]. 147 | // 0 means grayscale (no color), 1 means fully saturated. 148 | // - V (Value): The brightness of the color, in [0, 1]. 149 | // 0 corresponds to black, 1 corresponds to the brightest form 150 | // of the color given its hue and saturation. 151 | // 152 | // Example usage: 153 | // 154 | // HSV{H: 0, S: 1, V: 1} // pure red 155 | // HSV{H: 120, S: 1, V: 1} // pure green 156 | // HSV{H: 240, S: 1, V: 1} // pure blue 157 | type HSV struct { 158 | H float32 159 | S float32 160 | V float32 161 | } 162 | 163 | // HSL is the Hue–Saturation–Lightness cylindrical-coordinate color space. 164 | // It is similar to HSV but defines the third axis as lightness rather than value. 165 | // HSL is widely used in digital art and CSS because its parameters 166 | // allow for intuitive control over tints, shades, and tones. 167 | type HSL struct { 168 | H float32 // Hue. Is the radial component. 169 | S float32 // Saturation. Also known as chroma. Corresponds to intensity of color. 170 | L float32 // Lightness. 171 | } 172 | 173 | // LerpSRGB interpolates directly in gamma-encoded sRGB. 174 | // Fast and simple, but not perceptually uniform. 175 | // Best for quick blends where accuracy is not critical. 176 | func LerpSRGB(c1, c2 color.Color, v float32) color.Color { 177 | o1 := ColorToSRGB(c1) 178 | o2 := ColorToSRGB(c2) 179 | return o1.Lerp(o2, v) 180 | } 181 | 182 | // LerpLSRGB interpolates in linear-light sRGB (after removing gamma). 183 | // More physically accurate than plain sRGB (like mixing light). 184 | // Best for image compositing and blending intensities. 185 | func LerpLSRGB(c1, c2 color.Color, v float32) color.Color { 186 | o1 := ColorToSRGB(c1).LSRGB() 187 | o2 := ColorToSRGB(c2).LSRGB() 188 | return o1.Lerp(o2, v).ClipToGamut().SRGB() 189 | } 190 | 191 | // LerpCIEXYZ interpolates in device-independent CIE XYZ space. 192 | // Useful for cross-device workflows and conversions, not perceptually uniform. 193 | func LerpCIEXYZ(c1, c2 color.Color, v float32) color.Color { 194 | o1 := ColorToSRGB(c1).LSRGB().CIEXYZ() 195 | o2 := ColorToSRGB(c2).LSRGB().CIEXYZ() 196 | return o1.Lerp(o2, v).LSRGB().ClipToGamut().SRGB() 197 | } 198 | 199 | // LerpOKLAB interpolates in OKLab, a perceptually uniform space. 200 | // Produces smooth, visually even blends. 201 | // Best for perceptual color mixing and gradients. 202 | func LerpOKLAB(c1, c2 color.Color, v float32) color.Color { 203 | o1 := ColorToSRGB(c1).LSRGB().CIEXYZ().OKLAB() 204 | o2 := ColorToSRGB(c2).LSRGB().CIEXYZ().OKLAB() 205 | lch := o1.Lerp(o2, v).OKLCH() 206 | mapped := lch.GamutMappedLSRGB() 207 | return mapped.OKLAB().CIEXYZ().LSRGB().ClipToGamut().SRGB() 208 | } 209 | 210 | // LerpOKLCH interpolates in OKLCH (lightness, chroma, hue). 211 | // Preserves hue direction and interpolates hue angles correctly. 212 | // Best for perceptual gradients where hue continuity matters. 213 | func LerpOKLCH(c1, c2 color.Color, v float32) color.Color { 214 | o1 := ColorToSRGB(c1).LSRGB().CIEXYZ().OKLAB().OKLCH() 215 | o2 := ColorToSRGB(c2).LSRGB().CIEXYZ().OKLAB().OKLCH() 216 | mapped := o1.Lerp(o2, v).GamutMappedLSRGB() 217 | result := mapped.OKLAB().CIEXYZ().LSRGB().ClipToGamut().SRGB() 218 | return result 219 | } 220 | 221 | // ColorToSRGB converts the color to [SRGB] discarding the opacity/alpha (A) field. 222 | func ColorToSRGB(c color.Color) SRGB { 223 | r, g, b, _ := c.RGBA() 224 | return SRGB{ 225 | R: float32(r) / 0xffff, 226 | G: float32(g) / 0xffff, 227 | B: float32(b) / 0xffff, 228 | } 229 | } 230 | 231 | // transferFunc is the gamma function. 232 | func transferFunc(v float32) float32 { 233 | sign := math32.Copysign(1, v) 234 | abs := math32.Abs(v) 235 | if abs <= 0.04045 { 236 | return v / 12.92 237 | } 238 | return sign * math32.Pow((abs+0.055)/1.055, 2.4) 239 | } 240 | 241 | // invTransferFunc is the inverse gamma function as defined by IEC2003. 242 | func invTransferFunc(v float32) float32 { 243 | sign := math32.Copysign(1, v) 244 | abs := math32.Abs(v) 245 | if abs <= 0.0031308 { 246 | return 12.92 * v 247 | } 248 | return sign * (1.055*math32.Pow(abs, 1./2.4) - 0.055) 249 | } 250 | 251 | func (c SRGB) LSRGB() LSRGB { 252 | return LSRGB{ 253 | R: transferFunc(c.R), 254 | G: transferFunc(c.G), 255 | B: transferFunc(c.B), 256 | } 257 | } 258 | 259 | func (c LSRGB) SRGB() SRGB { 260 | return SRGB{ 261 | R: invTransferFunc(c.R), 262 | G: invTransferFunc(c.G), 263 | B: invTransferFunc(c.B), 264 | } 265 | } 266 | 267 | func (c SRGB) vec() ms3.Vec { return ms3.Vec{X: c.R, Y: c.G, Z: c.B} } 268 | func (c LSRGB) vec() ms3.Vec { return ms3.Vec{X: c.R, Y: c.G, Z: c.B} } 269 | func (c CIEXYZ) vec() ms3.Vec { return ms3.Vec{X: c.X, Y: c.Y, Z: c.Z} } 270 | func (c CIELAB) vec() ms3.Vec { return ms3.Vec{X: c.L, Y: c.A, Z: c.B} } 271 | func (c CIELCH) vec() ms3.Vec { return ms3.Vec{X: c.L, Y: c.C, Z: c.H} } 272 | func (c OKLCH) vec() ms3.Vec { return ms3.Vec{X: c.L, Y: c.C, Z: c.H} } 273 | func (c OKLAB) vec() ms3.Vec { return ms3.Vec{X: c.L, Y: c.A, Z: c.B} } 274 | func (c HSV) vec() ms3.Vec { return ms3.Vec{X: c.H, Y: c.S, Z: c.V} } 275 | func (c HSL) vec() ms3.Vec { return ms3.Vec{X: c.H, Y: c.S, Z: c.L} } 276 | func (c SRGB) Array() [3]float32 { return c.vec().Array() } 277 | func (c LSRGB) Array() [3]float32 { return c.vec().Array() } 278 | func (c CIEXYZ) Array() [3]float32 { return c.vec().Array() } 279 | func (c CIELAB) Array() [3]float32 { return c.vec().Array() } 280 | func (c CIELCH) Array() [3]float32 { return c.vec().Array() } 281 | func (c OKLCH) Array() [3]float32 { return c.vec().Array() } 282 | func (c OKLAB) Array() [3]float32 { return c.vec().Array() } 283 | func (c HSV) Array() [3]float32 { return c.vec().Array() } 284 | func (c HSL) Array() [3]float32 { return c.vec().Array() } 285 | 286 | // HSV converts gamma-encoded sRGB to HSV (all in [0,1] except H in degrees). 287 | func (c SRGB) HSV() HSV { 288 | r, g, b := c.R, c.G, c.B 289 | v := c.vec() 290 | max := v.Max() 291 | min := v.Min() 292 | delta := max - min 293 | 294 | var h float32 295 | if delta == 0 { 296 | h = undefinedHue 297 | } else if max == r { 298 | h = 60 * ((g - b) / delta) 299 | } else if max == g { 300 | h = 60 * ((b-r)/delta + 2) 301 | } else { // max == b 302 | h = 60 * ((r-g)/delta + 4) 303 | } 304 | 305 | h = wrapHue(h) 306 | 307 | var s float32 308 | if max > 0 { 309 | s = delta / max 310 | } else { 311 | s = 0 312 | } 313 | return HSV{H: h, S: s, V: max} 314 | } 315 | 316 | // HSL converts gamma-encoded sRGB to HSL (all in [0,1] except H in degrees). 317 | func (c SRGB) HSL() HSL { 318 | v := c.vec() 319 | max := v.Max() 320 | min := v.Min() 321 | delta := max - min 322 | 323 | l := 0.5 * (max + min) 324 | 325 | var h float32 326 | if delta == 0 { 327 | h = undefinedHue 328 | } else if max == c.R { 329 | h = 60 * ((c.G - c.B) / delta) 330 | } else if max == c.G { 331 | h = 60 * ((c.B-c.R)/delta + 2) 332 | } else { // max == b 333 | h = 60 * ((c.R-c.G)/delta + 4) 334 | } 335 | h = wrapHue(h) 336 | 337 | var s float32 338 | if delta == 0 { 339 | s = 0 340 | } else if l <= 0.5 { 341 | s = delta / (max + min) 342 | } else { 343 | s = delta / (2 - max - min) 344 | } 345 | return HSL{H: h, S: s, L: l} 346 | } 347 | 348 | // SRGB converts HSV to gamma-encoded sRGB. Inputs: H in degrees, S,V in [0,1]. 349 | func (hsv HSV) SRGB() SRGB { 350 | h := wrapHue(hsv.H) 351 | s := ms1.Clamp(hsv.S, 0, 1) 352 | v := ms1.Clamp(hsv.V, 0, 1) 353 | 354 | if s <= epsUnit { // achromatic 355 | return SRGB{R: v, G: v, B: v} 356 | } 357 | 358 | c := v * s 359 | hp := h / 60 360 | x := c * (1 - math32.Abs(math32.Mod(hp, 2)-1)) 361 | 362 | var r1, g1, b1 float32 363 | switch { 364 | case 0 <= hp && hp < 1: 365 | r1, g1, b1 = c, x, 0 366 | case 1 <= hp && hp < 2: 367 | r1, g1, b1 = x, c, 0 368 | case 2 <= hp && hp < 3: 369 | r1, g1, b1 = 0, c, x 370 | case 3 <= hp && hp < 4: 371 | r1, g1, b1 = 0, x, c 372 | case 4 <= hp && hp < 5: 373 | r1, g1, b1 = x, 0, c 374 | default: // 5 <= hp && hp < 6 375 | r1, g1, b1 = c, 0, x 376 | } 377 | m := v - c 378 | return SRGB{R: r1 + m, G: g1 + m, B: b1 + m}.ClipToGamut() 379 | } 380 | 381 | // SRGB converts HSL to gamma-encoded sRGB. Inputs: H in degrees, S,L in [0,1]. 382 | func (hsl HSL) SRGB() SRGB { 383 | h := wrapHue(hsl.H) 384 | s := ms1.Clamp(hsl.S, 0, 1) 385 | l := ms1.Clamp(hsl.L, 0, 1) 386 | 387 | if s == 0 { // achromatic 388 | return SRGB{R: l, G: l, B: l} 389 | } 390 | 391 | c := (1 - math32.Abs(2*l-1)) * s 392 | hp := h / 60 393 | x := c * (1 - math32.Abs(math32.Mod(hp, 2)-1)) 394 | 395 | var r1, g1, b1 float32 396 | switch { 397 | case 0 <= hp && hp < 1: 398 | r1, g1, b1 = c, x, 0 399 | case 1 <= hp && hp < 2: 400 | r1, g1, b1 = x, c, 0 401 | case 2 <= hp && hp < 3: 402 | r1, g1, b1 = 0, c, x 403 | case 3 <= hp && hp < 4: 404 | r1, g1, b1 = 0, x, c 405 | case 4 <= hp && hp < 5: 406 | r1, g1, b1 = x, 0, c 407 | default: // 5 <= hp && hp < 6 408 | r1, g1, b1 = c, 0, x 409 | } 410 | m := l - c/2 411 | return SRGB{R: r1 + m, G: g1 + m, B: b1 + m}.ClipToGamut() 412 | } 413 | 414 | // wrapHue normalizes H to [0,360). 415 | func wrapHue(h float32) float32 { 416 | for h < 0 { 417 | h += 360 418 | } 419 | for h >= 360 { 420 | h -= 360 421 | } 422 | return h 423 | } 424 | 425 | func (c LSRGB) CIEXYZ() CIEXYZ { 426 | v := ms3.MulMatVec(linSRGBToXYZ, c.vec()) 427 | return CIEXYZ{ 428 | X: v.X, 429 | Y: v.Y, 430 | Z: v.Z, 431 | } 432 | } 433 | 434 | func (c CIEXYZ) LSRGB() LSRGB { 435 | v := ms3.MulMatVec(xyzToLinSRGB, c.vec()) 436 | return LSRGB{R: v.X, G: v.Y, B: v.Z} 437 | } 438 | 439 | func (c SRGB) RGBA() (r, g, b, a uint32) { 440 | // Add 0.5 to reduce bias. 441 | r = uint32(c.R*0xffff + 0.5) 442 | g = uint32(c.G*0xffff + 0.5) 443 | b = uint32(c.B*0xffff + 0.5) 444 | return r, g, b, 0xffff 445 | } 446 | 447 | func (c CIEXYZ) OKLAB() OKLAB { 448 | lms := ms3.MulMatVec(xyzToLMS, c.vec()) 449 | 450 | v := ms3.MulMatVec(lmsToOKLAB, ms3.Vec{ 451 | X: math32.Cbrt(lms.X), 452 | Y: math32.Cbrt(lms.Y), 453 | Z: math32.Cbrt(lms.Z), 454 | }) 455 | return OKLAB{ 456 | L: v.X, 457 | A: v.Y, 458 | B: v.Z, 459 | } 460 | } 461 | 462 | func (c OKLAB) CIEXYZ() CIEXYZ { 463 | LMSnl := ms3.MulMatVec(oklabToLMS, c.vec()) 464 | v := ms3.MulMatVec(lmsToXYZ, ms3.Vec{ 465 | X: LMSnl.X * LMSnl.X * LMSnl.X, 466 | Y: LMSnl.Y * LMSnl.Y * LMSnl.Y, 467 | Z: LMSnl.Z * LMSnl.Z * LMSnl.Z, 468 | }) 469 | return CIEXYZ{ 470 | X: v.X, 471 | Y: v.Y, 472 | Z: v.Z, 473 | } 474 | } 475 | 476 | func (c OKLAB) OKLCH() OKLCH { 477 | const eps = 0.000004 478 | hue := math32.Atan2(c.B, c.A) * 180 / math32.Pi 479 | chroma := math32.Sqrt(c.A*c.A + c.B*c.B) 480 | if hue < 0 { 481 | hue += 360 482 | } 483 | if chroma <= eps { 484 | hue = undefinedHue 485 | } 486 | return OKLCH{ 487 | L: c.L, 488 | C: chroma, 489 | H: hue, 490 | } 491 | } 492 | 493 | // GamutMappedLSRGB maps the OKLCH color into the sRGB gamut. 494 | // 495 | // If the color is already representable in sRGB, it is returned unchanged. 496 | // Otherwise, chroma is reduced until the color can be expressed in linear sRGB 497 | // without clipping, while keeping lightness and hue as stable as possible. 498 | // 499 | // Best used after interpolation in OKLab/OKLCH to ensure the result is displayable. 500 | func (c OKLCH) GamutMappedLSRGB() OKLCH { 501 | // Early return for Lightness exceed range. 502 | origin := c 503 | if origin.L < 0 || origin.L > 1 { 504 | return OKLCH{ 505 | L: math32.Min(math32.Max(origin.L, 0), 1), 506 | C: 0, 507 | H: 0, 508 | } 509 | } 510 | const ( 511 | JND = 0.02 512 | eps = 0.0001 513 | ) 514 | current := origin 515 | clipped := current.OKLAB().CIEXYZ().LSRGB().ClipToGamut() 516 | E := origin.OKLAB().DeltaE(clipped.CIEXYZ().OKLAB()) 517 | if E < JND { 518 | return clipped.CIEXYZ().OKLAB().OKLCH() 519 | } 520 | var cmin, cmax float32 = 0, origin.C 521 | minInGamut := true 522 | for cmax-cmin > eps { 523 | chroma := 0.5 * (cmin + cmax) 524 | current.C = chroma 525 | currentRGB := current.OKLAB().CIEXYZ().LSRGB() 526 | if minInGamut && currentRGB.InGamut() { 527 | cmin = chroma 528 | minInGamut = OKLCH{L: current.L, C: chroma, H: current.H}.OKLAB().CIEXYZ().LSRGB().InGamut() 529 | continue 530 | } 531 | clipped = currentRGB.ClipToGamut() 532 | E = clipped.CIEXYZ().OKLAB().DeltaE(current.OKLAB()) 533 | if E < JND { 534 | if JND-E < eps { 535 | return clipped.CIEXYZ().OKLAB().OKLCH() 536 | } 537 | minInGamut = false 538 | cmin = chroma 539 | minInGamut = OKLCH{L: current.L, C: chroma, H: current.H}.OKLAB().CIEXYZ().LSRGB().InGamut() 540 | } else { 541 | cmax = chroma 542 | } 543 | } 544 | return clipped.CIEXYZ().OKLAB().OKLCH() 545 | } 546 | 547 | // InGamut reports whether the linear-light RGB color lies inside the sRGB gamut. 548 | // Returns true if all channels are in [0,1], false otherwise. 549 | func (c LSRGB) InGamut() bool { 550 | return c.R <= 1 && c.G <= 1 && c.B <= 1 && c.R >= 0 && c.G >= 0 && c.B >= 0 551 | } 552 | 553 | // InGamut reports whether the gamma-encoded sRGB color lies inside the sRGB gamut. 554 | // Returns true if all channels are in [0,1], false otherwise. 555 | func (c SRGB) InGamut() bool { 556 | return c.R <= 1 && c.G <= 1 && c.B <= 1 && c.R >= 0 && c.G >= 0 && c.B >= 0 557 | } 558 | 559 | // ClipToGamut clamps each channel of the linear-light RGB color to [0,1]. 560 | // Useful after computations that may push values slightly outside the gamut. 561 | func (c LSRGB) ClipToGamut() LSRGB { 562 | return LSRGB{ 563 | R: ms1.Clamp(c.R, 0, 1), 564 | G: ms1.Clamp(c.G, 0, 1), 565 | B: ms1.Clamp(c.B, 0, 1), 566 | } 567 | } 568 | 569 | // ClipToGamut clamps each channel of the gamma-encoded sRGB color to [0,1]. 570 | // Useful to avoid invalid values when converting or interpolating. 571 | func (c SRGB) ClipToGamut() SRGB { 572 | return SRGB{ 573 | R: ms1.Clamp(c.R, 0, 1), 574 | G: ms1.Clamp(c.G, 0, 1), 575 | B: ms1.Clamp(c.B, 0, 1), 576 | } 577 | } 578 | 579 | // OKLAB converts the OKLCH cylindrical representation back to OKLab Cartesian form. Hue (H) is interpreted in degrees, and converted into a* (A) and b* (B) axes. 580 | func (c OKLCH) OKLAB() OKLAB { 581 | sin, cos := math32.Sincos(c.H * math32.Pi / 180) 582 | return OKLAB{ 583 | L: c.L, 584 | A: c.C * cos, 585 | B: c.C * sin, 586 | } 587 | } 588 | 589 | func (c CIEXYZ) CIELAB() CIELAB { 590 | // Assuming XYZ is relative to D50, convert to CIE Lab 591 | // from CIE standard, which now defines these as a rational fraction 592 | const ( 593 | ε = 216. / 24389 // 6^3/29^3 594 | κ = 24389. / 27 // 29^3/3^3 595 | ) 596 | // compute xyz, which is XYZ scaled relative to reference white 597 | xyz := ms3.DivElem(c.vec(), d50) 598 | f := func(x float32) float32 { 599 | if x > ε { 600 | return math32.Cbrt(x) 601 | } 602 | return (κ*x + 16) / 116 603 | } 604 | return CIELAB{ 605 | L: 116*f(xyz.Y) - 16, 606 | A: 500 * (f(xyz.X) - f(xyz.Y)), 607 | B: 200 * (f(xyz.Y) - f(xyz.Z)), 608 | } 609 | } 610 | 611 | func (c CIELAB) CIELCH() CIELCH { 612 | const eps = 0.0015 613 | chroma := math32.Sqrt(c.A*c.A + c.B*c.B) 614 | hue := math32.Atan2(c.B, c.A) * 180 / math32.Pi 615 | if hue < 0 { 616 | hue += 360 617 | } 618 | if chroma <= eps { 619 | hue = undefinedHue 620 | } 621 | return CIELCH{ 622 | L: c.L, 623 | C: chroma, 624 | H: hue, 625 | } 626 | } 627 | 628 | func (c CIELAB) CIEXYZ() CIEXYZ { 629 | const κ = 24389. / 27 // 29^3/3^3 630 | const ε = 216. / 24389 // 6^3/29^3 631 | const ecbrt = 6. / 29 632 | f1 := (c.L + 16) / 116 633 | f0 := c.A/500 + f1 634 | f2 := f1 - c.B/200 635 | 636 | var xyz CIEXYZ 637 | if f0 > ecbrt { 638 | xyz.X = f0 * f0 * f0 639 | } else { 640 | xyz.X = (116*f0 - 16) / κ 641 | } 642 | if c.L > κ*ε { 643 | ycbrt := (c.L + 16) / 116 644 | xyz.Y = ycbrt * ycbrt * ycbrt 645 | } else { 646 | xyz.Y = (116*c.L - 16) / κ 647 | } 648 | if f2 > ecbrt { 649 | xyz.Z = f2 * f2 * f2 650 | } else { 651 | xyz.Z = (116*f2 - 16) / κ 652 | } 653 | // Compute XYZ by scaling xyz by reference white 654 | v := ms3.MulElem(xyz.vec(), d50) 655 | return CIEXYZ{X: v.X, Y: v.Y, Z: v.Z} 656 | } 657 | 658 | func (reference OKLAB) DeltaE(sample OKLAB) float32 { 659 | e := ms3.Sub(reference.vec(), sample.vec()) 660 | return math32.Sqrt(ms3.Dot(e, e)) 661 | } 662 | 663 | func (c CIELCH) CIELAB() CIELAB { 664 | sin, cos := math32.Sincos(c.H * math32.Pi / 180) 665 | return CIELAB{ 666 | L: c.L, 667 | A: c.C * cos, 668 | B: c.C * sin, 669 | } 670 | } 671 | 672 | func (from OKLCH) Lerp(to OKLCH, v float32) OKLCH { 673 | return OKLCH(CIELCH(from).Lerp(CIELCH(to), v)) 674 | } 675 | 676 | func (from CIELCH) Lerp(to CIELCH, v float32) CIELCH { 677 | // First handle achromatic or "powerless hue" colors. 678 | const eps = 0.000004 679 | fromPowerless := from.C < eps 680 | toPowerless := to.C < eps 681 | if fromPowerless || toPowerless { 682 | if fromPowerless && toPowerless { 683 | // Both colors are grayish, just ignore hue interpolation entirely. 684 | return CIELCH{ 685 | L: ms1.Interp(from.L, to.L, v), 686 | C: 0, 687 | H: undefinedHue, 688 | } 689 | } else if !toPowerless { 690 | from.H = to.H 691 | } else { 692 | to.H = from.H 693 | } 694 | } 695 | return CIELCH{ 696 | L: ms1.Interp(from.L, to.L, v), 697 | H: ms1.InterpWrap(360, from.H, to.H, v), 698 | C: ms1.Interp(from.C, to.C, v), 699 | } 700 | } 701 | 702 | func (from CIELAB) Lerp(to CIELAB, v float32) CIELAB { 703 | return CIELAB{ 704 | L: ms1.Interp(from.L, to.L, v), 705 | A: ms1.Interp(from.A, to.A, v), 706 | B: ms1.Interp(from.B, to.B, v), 707 | } 708 | } 709 | 710 | func (from OKLAB) Lerp(to OKLAB, v float32) OKLAB { 711 | return OKLAB{ 712 | L: ms1.Interp(from.L, to.L, v), 713 | A: ms1.Interp(from.A, to.A, v), 714 | B: ms1.Interp(from.B, to.B, v), 715 | } 716 | } 717 | 718 | func (from CIEXYZ) Lerp(to CIEXYZ, v float32) CIEXYZ { 719 | return CIEXYZ{ 720 | X: ms1.Interp(from.X, to.X, v), 721 | Y: ms1.Interp(from.Y, to.Y, v), 722 | Z: ms1.Interp(from.Z, to.Z, v), 723 | } 724 | } 725 | 726 | func (from LSRGB) Lerp(to LSRGB, v float32) LSRGB { 727 | return LSRGB{ 728 | R: ms1.Interp(from.R, to.R, v), 729 | G: ms1.Interp(from.G, to.G, v), 730 | B: ms1.Interp(from.B, to.B, v), 731 | } 732 | } 733 | 734 | func (from SRGB) Lerp(to SRGB, v float32) SRGB { 735 | return SRGB{ 736 | R: ms1.Interp(from.R, to.R, v), 737 | G: ms1.Interp(from.G, to.G, v), 738 | B: ms1.Interp(from.B, to.B, v), 739 | } 740 | } 741 | --------------------------------------------------------------------------------