Repository: mazznoer/colorgrad Branch: master Commit: 146c700a7e77 Files: 37 Total size: 95.6 KB Directory structure: gitextract_an2mw5mv/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── PRESET.md ├── README.md ├── basis.go ├── basis_test.go ├── bench_test.go ├── builder.go ├── builder_test.go ├── catmull_rom.go ├── catmull_rom_test.go ├── css_gradient.go ├── example_test.go ├── examples/ │ ├── .gitignore │ ├── basic.go │ ├── ggr/ │ │ ├── Abstract_1.ggr │ │ └── Full_saturation_spectrum_CW.ggr │ └── gradients.go ├── gimp.go ├── gimp_test.go ├── go.mod ├── go.sum ├── gradient.go ├── gradient_test.go ├── linear.go ├── linear_test.go ├── preset.go ├── preset_test.go ├── sharp.go ├── sharp_test.go ├── smoothstep.go ├── util.go └── util_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms ko_fi: mazznoer liberapay: mazznoer custom: "https://paypal.me/mazznoer" ================================================ FILE: .github/workflows/go.yml ================================================ name: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: './go.mod' - name: Build run: go build -v . - name: Vet run: go vet . - name: Test run: go test -v -coverprofile coverage.out && go tool cover -html coverage.out -o coverage.html - name: Run examples run: | go run examples/basic.go go run examples/gradients.go - name: Format if: matrix.os == 'ubuntu-latest' run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out *.html # Dependency directories (remove the comment below to include it) # vendor/ README.html output/ gradient.png ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## v0.11.1 - Fix bug in Lab conversion ## v0.11.0 - New `GradientBuilder.Reset()` - New `BlendLab` - `Gradient`'s fields is now public - `GradientCore` is now public - New `InterpolationSmoothstep` ## v0.10.2 - Fix bug in `GradientBuilder.Css()` ## v0.10.1 - New `GradientBuilder.Css()` - Using `uint32` to store preset colors ## v0.10.0 - Added support for transparency - New `GoColor()` - Refactor lots of code - Remove color schemes ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Nor Khasyatillah 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ SHELL := /bin/bash .PHONY: all check test all: check test check: go build && go vet && gofmt -s -l . test: go test -v -coverprofile coverage.out && go tool cover -html coverage.out -o coverage.html bench: go test -bench . ================================================ FILE: PRESET.md ================================================ # Preset Gradients All preset gradients are in the domain [0..1]. ## Diverging `colorgrad.BrBG()` ![img](doc/images/preset/BrBG.png) `colorgrad.PRGn()` ![img](doc/images/preset/PRGn.png) `colorgrad.PiYG()` ![img](doc/images/preset/PiYG.png) `colorgrad.PuOr()` ![img](doc/images/preset/PuOr.png) `colorgrad.RdBu()` ![img](doc/images/preset/RdBu.png) `colorgrad.RdGy()` ![img](doc/images/preset/RdGy.png) `colorgrad.RdYlBu()` ![img](doc/images/preset/RdYlBu.png) `colorgrad.RdYlGn()` ![img](doc/images/preset/RdYlGn.png) `colorgrad.Spectral()` ![img](doc/images/preset/Spectral.png) ## Sequential (Single Hue) `colorgrad.Blues()` ![img](doc/images/preset/Blues.png) `colorgrad.Greens()` ![img](doc/images/preset/Greens.png) `colorgrad.Greys()` ![img](doc/images/preset/Greys.png) `colorgrad.Oranges()` ![img](doc/images/preset/Oranges.png) `colorgrad.Purples()` ![img](doc/images/preset/Purples.png) `colorgrad.Reds()` ![img](doc/images/preset/Reds.png) ## Sequential (Multi-Hue) `colorgrad.Turbo()` ![img](doc/images/preset/Turbo.png) `colorgrad.Viridis()` ![img](doc/images/preset/Viridis.png) `colorgrad.Inferno()` ![img](doc/images/preset/Inferno.png) `colorgrad.Magma()` ![img](doc/images/preset/Magma.png) `colorgrad.Plasma()` ![img](doc/images/preset/Plasma.png) `colorgrad.Cividis()` ![img](doc/images/preset/Cividis.png) `colorgrad.Warm()` ![img](doc/images/preset/Warm.png) `colorgrad.Cool()` ![img](doc/images/preset/Cool.png) `colorgrad.CubehelixDefault()` ![img](doc/images/preset/CubehelixDefault.png) `colorgrad.BuGn()` ![img](doc/images/preset/BuGn.png) `colorgrad.BuPu()` ![img](doc/images/preset/BuPu.png) `colorgrad.GnBu()` ![img](doc/images/preset/GnBu.png) `colorgrad.OrRd()` ![img](doc/images/preset/OrRd.png) `colorgrad.PuBuGn()` ![img](doc/images/preset/PuBuGn.png) `colorgrad.PuBu()` ![img](doc/images/preset/PuBu.png) `colorgrad.PuRd()` ![img](doc/images/preset/PuRd.png) `colorgrad.RdPu()` ![img](doc/images/preset/RdPu.png) `colorgrad.YlGnBu()` ![img](doc/images/preset/YlGnBu.png) `colorgrad.YlGn()` ![img](doc/images/preset/YlGn.png) `colorgrad.YlOrBr()` ![img](doc/images/preset/YlOrBr.png) `colorgrad.YlOrRd()` ![img](doc/images/preset/YlOrRd.png) ## Cyclical `colorgrad.Rainbow()` ![img](doc/images/preset/Rainbow.png) `colorgrad.Sinebow()` ![img](doc/images/preset/Sinebow.png) ================================================ FILE: README.md ================================================ # colorgrad [![Release](https://img.shields.io/github/release/mazznoer/colorgrad.svg)](https://github.com/mazznoer/colorgrad/releases/latest) [![PkgGoDev](https://pkg.go.dev/badge/github.com/mazznoer/colorgrad)](https://pkg.go.dev/github.com/mazznoer/colorgrad) ![Build Status](https://github.com/mazznoer/colorgrad/actions/workflows/go.yml/badge.svg) [![go report](https://goreportcard.com/badge/github.com/mazznoer/colorgrad)](https://goreportcard.com/report/github.com/mazznoer/colorgrad) Go (Golang) _color scales_ library for data visualization, charts, games, maps, generative art and others. ## Support This Project [![Donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/mazznoer/donate) ## Index * [Custom Gradient](#custom-gradient) * [Preset Gradients](#preset-gradients) * [Using the Gradient](#using-the-gradient) * [Examples](#examples) * [Playground](#playground) ```go import "github.com/mazznoer/colorgrad" ``` ## Custom Gradient ### Basic ```go grad, err := colorgrad.NewGradient().Build() ``` ![img](doc/images/custom-default.png) ### Custom Colors ```go grad, err := colorgrad.NewGradient(). Colors( colorgrad.Rgb8(0, 206, 209, 255), colorgrad.Rgb8(255, 105, 180, 255), colorgrad.Rgb(0.274, 0.5, 0.7, 1), colorgrad.Hsv(50, 1, 1, 1), colorgrad.Hsv(348, 0.9, 0.8, 1), ). Build() ``` ![img](doc/images/custom-colors.png) ### Using Web Color Format `HtmlColors()` method accepts [named colors](https://www.w3.org/TR/css-color-4/#named-colors), hexadecimal (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`), `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, and `hsv()`. ```go grad, err := colorgrad.NewGradient(). HtmlColors("#C41189", "#00BFFF", "#FFD700"). Build() ``` ![img](doc/images/custom-hex-colors.png) ```go grad, err := colorgrad.NewGradient(). HtmlColors("gold", "hotpink", "darkturquoise"). Build() ``` ![img](doc/images/custom-named-colors.png) ```go grad, err := colorgrad.NewGradient(). HtmlColors( "rgb(125,110,221)", "rgb(90%,45%,97%)", "hsl(229,79%,85%)", ). Build() ``` ![img](doc/images/custom-css-colors.png) ### Domain & Color Position Default domain is [0..1]. ```go grad, err := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Build() ``` ![img](https://raw.githubusercontent.com/mazznoer/colorgrad-rs/master/docs/images/domain-default.png) Set the domain to [0..100]. ```go grad, err := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(0, 100). Build() ``` ![img](https://raw.githubusercontent.com/mazznoer/colorgrad-rs/master/docs/images/domain-100.png) Set the domain to [-1..1]. ```go grad, err := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(-1, 1). Build() ``` ![img](https://raw.githubusercontent.com/mazznoer/colorgrad-rs/master/docs/images/domain-neg1-1.png) Set exact position for each color. The domain is [0..1]. ```go grad, err := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(0, 0.7, 1). Build() ``` ![img](https://raw.githubusercontent.com/mazznoer/colorgrad-rs/master/docs/images/color-position-1.png) Set exact position for each color. The domain is [15..80]. ```go grad, err := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(15, 30, 80). Build() ``` ![img](https://raw.githubusercontent.com/mazznoer/colorgrad-rs/master/docs/images/color-position-2.png) ### CSS Gradient Format ```go grad, err := colorgrad.NewGradient(). Css("deeppink, gold, seagreen"). Build() ``` ![img](doc/images/css-gradient-1.png) ```go grad, err := colorgrad.NewGradient(). Css("purple, gold 35%, green 35%, 55%, gold"). Interpolation(colorgrad.InterpolationCatmullRom). Build() ``` ![img](doc/images/css-gradient-2.png) ### Blending Mode ```go grad, err := colorgrad.NewGradient(). HtmlColors("#FFF", "#00F"). Mode(colorgrad.BlendRgb). Build() ``` ![blend-modes](doc/images/blend-modes-3.png) ### Interpolation Mode ```go grad, err := colorgrad.NewGradient(). HtmlColors("#C41189", "#00BFFF", "#FFD700"). Interpolation(colorgrad.InterpolationLinear). Build() ``` `InterpolationLinear` ![interpolation-linear](doc/images/interpolation-linear2.png) `InterpolationSmoothstep` ![interpolation-smoothstep](doc/images/interpolation-smoothstep.png) `InterpolationCatmullRom` ![interpolation-catmull-rom](doc/images/interpolation-catmull-rom2.png) `InterpolationBasis` ![interpolation-basis](doc/images/interpolation-basis2.png) ## Preset Gradients See [PRESET.md](PRESET.md) ## Parsing GIMP Gradient ```go import "os" foreground := colorgrad.Rgb(0, 0, 0, 1) background := colorgrad.Rgb(1, 1, 1, 1) file, err := os.Open("Abstract_1.ggr") if err != nil { panic(err) } defer file.Close() grad, name, err2 := colorgrad.ParseGgr(file, foreground, background) fmt.Println(name) // Abstract 1 ``` ![gimp-gradient](doc/images/ggr-abstract-1.png) ## Using the Gradient ### Get the domain ```go grad := colorgrad.Rainbow() fmt.Println(grad.Domain()) // 0 1 ``` ### Get single color at certain position ```go grad := colorgrad.Rainbow() fmt.Println(grad.At(0.0).HexString()) // #6e40aa fmt.Println(grad.At(0.5).HexString()) // #aff05b fmt.Println(grad.At(1.0).HexString()) // #6e40aa ``` ### Get n colors evenly spaced across gradient ```go grad := colorgrad.Rainbow() for _, c := range grad.Colors(10) { fmt.Println(c.HexString()) } ``` Output: ```console #6e40aa #c83dac #ff5375 #ff8c38 #c9d33a #7cf659 #5dea8d #48b8d0 #4775de #6e40aa ``` ### Hard-Edged Gradient Convert gradient to hard-edged gradient with 11 segments and 0 smoothness. ```go grad := colorgrad.Rainbow().Sharp(11, 0) ``` ![img](doc/images/rainbow-sharp.png) This is the effect of different smoothness. ![img](doc/images/sharp-gradients.png) ## Examples ### Gradient Image ```go package main import ( "image" "image/png" "os" "github.com/mazznoer/colorgrad" ) func main() { grad, _ := colorgrad.NewGradient(). HtmlColors("#C41189", "#00BFFF", "#FFD700"). Build() w := 1500 h := 70 fw := float64(w) img := image.NewRGBA(image.Rect(0, 0, w, h)) for x := 0; x < w; x++ { col := grad.At(float64(x) / fw) for y := 0; y < h; y++ { img.Set(x, y, col) } } file, err := os.Create("gradient.png") if err != nil { panic(err.Error()) } defer file.Close() png.Encode(file, img) } ``` Example output: ![img](doc/images/custom-hex-colors.png) ### Colored Noise ```go package main import ( "image" "image/png" "os" "github.com/mazznoer/colorgrad" "github.com/ojrac/opensimplex-go" ) func main() { w := 600 h := 350 scale := 0.02 grad := colorgrad.Rainbow().Sharp(7, 0.2) noise := opensimplex.NewNormalized(996) img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { for x := 0; x < w; x++ { t := noise.Eval2(float64(x)*scale, float64(y)*scale) img.Set(x, y, grad.At(t)) } } file, err := os.Create("noise.png") if err != nil { panic(err.Error()) } defer file.Close() png.Encode(file, img) } ``` Example output: ![noise](doc/images/noise.png) ## Playground * [Basic](https://play.golang.org/p/PlMov8BKfRc) * [Random colors](https://play.golang.org/p/d67x9di4sAF) ## Dependencies * [csscolorparser](https://github.com/mazznoer/csscolorparser) ## Inspirations * [chroma.js](https://gka.github.io/chroma.js/#color-scales) * [d3-scale-chromatic](https://github.com/d3/d3-scale-chromatic/) * colorful's [gradientgen.go](https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go) ## Links * [colorgrad-rs](https://github.com/mazznoer/colorgrad-rs) - Rust port of this library ================================================ FILE: basis.go ================================================ package colorgrad import ( "math" ) // https://github.com/d3/d3-interpolate/blob/master/src/basis.js type basisGradient struct { colors [][4]float64 positions []float64 min float64 max float64 mode BlendMode first Color last Color } func (lg basisGradient) At(t float64) Color { if t <= lg.min { return lg.first } if t >= lg.max { return lg.last } if math.IsNaN(t) { return Color{A: 1} } low := 0 high := len(lg.positions) n := high - 1 for low < high { mid := (low + high) / 2 if lg.positions[mid] < t { low = mid + 1 } else { high = mid } } if low == 0 { low = 1 } p1 := lg.positions[low-1] p2 := lg.positions[low] val0 := lg.colors[low-1] val1 := lg.colors[low] i := low - 1 t = (t - p1) / (p2 - p1) xx := func(v1, v2 float64, j int) float64 { v0 := 2*v1 - v2 if i > 0 { v0 = lg.colors[i-1][j] } v3 := 2*v2 - v1 if i < n-1 { v3 = lg.colors[i+2][j] } return basis(t, v0, v1, v2, v3) } a := xx(val0[0], val1[0], 0) b := xx(val0[1], val1[1], 1) c := xx(val0[2], val1[2], 2) d := xx(val0[3], val1[3], 3) switch lg.mode { case BlendRgb: return Color{R: a, G: b, B: c, A: d} case BlendLinearRgb: return LinearRgb(a, b, c, d) case BlendLab: return Lab(a, b, c, d).Clamp() case BlendOklab: return Oklab(a, b, c, d).Clamp() } return Color{} } func newBasisGradient(colors []Color, positions []float64, mode BlendMode) Gradient { gradbase := basisGradient{ colors: convertColors(colors, mode), positions: positions, min: positions[0], max: positions[len(positions)-1], mode: mode, first: colors[0], last: colors[len(colors)-1], } return Gradient{ Core: gradbase, Min: positions[0], Max: positions[len(positions)-1], } } func basis(t1, v0, v1, v2, v3 float64) float64 { t2 := t1 * t1 t3 := t2 * t1 return ((1-3*t1+3*t2-t3)*v0 + (4-6*t2+3*t3)*v1 + (1+3*t1+3*t2-3*t3)*v2 + t3*v3) / 6 } ================================================ FILE: basis_test.go ================================================ package colorgrad import ( "math" "testing" ) func Test_BasisGradient(t *testing.T) { grad, err := NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Mode(BlendRgb). Interpolation(InterpolationBasis). Build() test(t, err, nil) test(t, grad.At(0.00).HexString(), "#ff0000") test(t, grad.At(0.25).HexString(), "#857505") test(t, grad.At(0.50).HexString(), "#2baa2b") test(t, grad.At(0.75).HexString(), "#057585") test(t, grad.At(1.00).HexString(), "#0000ff") testSlice(t, colors2hex(grad.Colors(5)), []string{ "#ff0000", "#857505", "#2baa2b", "#057585", "#0000ff", }) test(t, grad.At(-0.1).HexString(), "#ff0000") test(t, grad.At(1.11).HexString(), "#0000ff") test(t, grad.At(math.NaN()).HexString(), "#000000") } ================================================ FILE: bench_test.go ================================================ package colorgrad import ( "fmt" "testing" ) var colors = []string{ "#87e575", "#e88ef2", "#7398ef", "#65c3f2", "#3e52a0", "#b659db", "#75b7ff", "#7555ba", "#fceac4", "#e8009e", "#cc7c26", "#e175f4", "#f959e7", "#31828e", "#e4bef7", "#a9fcc6", "#c122d6", "#81f9e1", "#caea81", "#47d192", "#db579d", "#ead36b", "#3c2bbc", "#9de544", "#e8e476", "#055d66", "#77c90c", "#bff49a", "#6b76db", "#3cf720", "#61bace", "#aa3405", "#a588d8", "#e2aef9", "#c0eff9", "#9b043b", "#b2ffe0", "#64e092", "#ff4cab", "#56d356", "#e185e2", "#ff72f3", "#ff4fbe", "#0a9366", "#dbc2f9", "#6cbacc", "#893009", "#13afaa", "#5208ad", "#9b1426", "#71e06d", "#c2ff0c", "#ce4244", "#ffebb5", "#169bf9", "#e58eb5", "#3c3ab2", "#2afca5", "#5946c4", "#ea7352", "#f46bbb", "#264daf", "#edaada", "#c6baf4", "#d984e8", "#61dd5f", "#1f26b7", "#f99345", "#b2d624", "#f911e2", "#bf882a", "#81f48b", "#a3ffba", "#13c139", "#dd7752", "#db755c", "#fcbdf2", "#f455b2", "#7414e2", "#074575", "#7cffef", "#dd778a", "#db55cb", "#7aa7cc", "#fcbfd2", "#b7f799", "#a65bc6", "#f242ff", "#f9c0b3", "#9890db", "#d01be8", "#20870e", "#f4426b", "#def260", "#521efc", "#ffbcc6", "#e285b9", "#0ed6f9", "#7825ed", "#f2c6ff", "#cdb2f4", "#5fd374", "#fc838d", "#27bec6", } var blendModes = []BlendMode{BlendRgb, BlendLinearRgb, BlendOklab} var positions = []float64{0.73} func BenchmarkLinearGradient(b *testing.B) { for _, mode := range blendModes { grad, err := NewGradient(). HtmlColors(colors...). Mode(mode). Interpolation(InterpolationLinear). Build() if err != nil { panic(err) } for _, pos := range positions { b.Run( fmt.Sprintf("%s_at_%.2f", mode, pos), func(b *testing.B) { for i := 0; i < b.N; i++ { grad.At(pos) } }) } } } func BenchmarkCatmullRomGradient(b *testing.B) { for _, mode := range blendModes { grad, err := NewGradient(). HtmlColors(colors...). Mode(mode). Interpolation(InterpolationCatmullRom). Build() if err != nil { panic(err) } for _, pos := range positions { b.Run( fmt.Sprintf("%s_at_%.2f", mode, pos), func(b *testing.B) { for i := 0; i < b.N; i++ { grad.At(pos) } }) } } } func BenchmarkBasisGradient(b *testing.B) { for _, mode := range blendModes { grad, err := NewGradient(). HtmlColors(colors...). Mode(mode). Interpolation(InterpolationBasis). Build() if err != nil { panic(err) } for _, pos := range positions { b.Run( fmt.Sprintf("%s_at_%.2f", mode, pos), func(b *testing.B) { for i := 0; i < b.N; i++ { grad.At(pos) } }) } } } ================================================ FILE: builder.go ================================================ package colorgrad import ( "fmt" "github.com/mazznoer/csscolorparser" ) type GradientBuilder struct { colors []Color positions []float64 mode BlendMode interpolation Interpolation invalidHtmlColors []string invalidCssGradient bool clean bool } func NewGradient() *GradientBuilder { return &GradientBuilder{ mode: BlendRgb, interpolation: InterpolationLinear, invalidCssGradient: false, clean: false, } } func (gb *GradientBuilder) Colors(colors ...Color) *GradientBuilder { for _, col := range colors { gb.colors = append(gb.colors, col) } gb.clean = false return gb } func (gb *GradientBuilder) HtmlColors(htmlColors ...string) *GradientBuilder { for _, s := range htmlColors { c, err := csscolorparser.Parse(s) if err != nil { gb.invalidHtmlColors = append(gb.invalidHtmlColors, s) continue } gb.colors = append(gb.colors, c) } gb.clean = false return gb } func (gb *GradientBuilder) Css(s string) *GradientBuilder { gb.clean = false stops, ok := parseCss(s) if !ok { gb.invalidCssGradient = true return gb } gb.colors = gb.colors[:0] gb.positions = gb.positions[:0] for _, st := range stops { gb.colors = append(gb.colors, *st.color) gb.positions = append(gb.positions, *st.pos) } return gb } func (gb *GradientBuilder) Domain(positions ...float64) *GradientBuilder { gb.positions = positions gb.clean = false return gb } func (gb *GradientBuilder) Mode(mode BlendMode) *GradientBuilder { gb.mode = mode return gb } func (gb *GradientBuilder) Interpolation(mode Interpolation) *GradientBuilder { gb.interpolation = mode return gb } func (gb *GradientBuilder) Reset() *GradientBuilder { gb.colors = gb.colors[:0] gb.positions = gb.positions[:0] gb.mode = BlendRgb gb.interpolation = InterpolationLinear gb.invalidHtmlColors = gb.invalidHtmlColors[:0] gb.invalidCssGradient = false gb.clean = false return gb } func (gb *GradientBuilder) prepareBuild() error { if gb.clean { return nil } if gb.invalidHtmlColors != nil { return fmt.Errorf("invalid HTML colors: %q", gb.invalidHtmlColors) } if gb.invalidCssGradient { return fmt.Errorf("invalid CSS gradient") } var colors []Color var positions []float64 if len(gb.colors) == 0 { // Default colors colors = []Color{ {R: 0, G: 0, B: 0, A: 1}, // black {R: 1, G: 1, B: 1, A: 1}, // white } } else if len(gb.colors) == 1 { colors = []Color{gb.colors[0], gb.colors[0]} } else { colors = make([]Color, len(gb.colors)) copy(colors, gb.colors) } if len(gb.positions) == 0 { positions = linspace(0, 1, uint(len(colors))) } else if len(gb.positions) == len(colors) { for i := 0; i < len(gb.positions)-1; i++ { if gb.positions[i] > gb.positions[i+1] { return fmt.Errorf("invalid domain") } } positions = make([]float64, len(gb.positions)) copy(positions, gb.positions) } else if len(gb.positions) == 2 { if gb.positions[0] >= gb.positions[1] { return fmt.Errorf("invalid domain") } positions = linspace(gb.positions[0], gb.positions[1], uint(len(colors))) } else { return fmt.Errorf("invalid domain") } gb.colors = gb.colors[:0] gb.positions = gb.positions[:0] prev := positions[0] lastIdx := len(positions) - 1 for i, col := range colors { pos := positions[i] var next float64 if i == lastIdx { next = positions[lastIdx] } else { next = positions[i+1] } if (pos-prev)+(next-pos) < epsilon { // skip } else { gb.colors = append(gb.colors, col) gb.positions = append(gb.positions, pos) } prev = pos } if len(gb.colors) != len(gb.positions) || len(gb.colors) < 2 { return fmt.Errorf("invalid stops") } gb.clean = true return nil } func (gb *GradientBuilder) Build() (Gradient, error) { if err := gb.prepareBuild(); err != nil { return Gradient{ Core: zeroGradient{}, Min: 0, Max: 1, }, err } if gb.interpolation == InterpolationLinear { return newLinearGradient(gb.colors, gb.positions, gb.mode), nil } if gb.interpolation == InterpolationSmoothstep { return newSmoothstepGradient(gb.colors, gb.positions, gb.mode), nil } if gb.interpolation == InterpolationBasis { return newBasisGradient(gb.colors, gb.positions, gb.mode), nil } return newCatmullRomGradient(gb.colors, gb.positions, gb.mode), nil } // For testing purposes func (gb *GradientBuilder) GetColors() *[]Color { return &gb.colors } // For testing purposes func (gb *GradientBuilder) GetPositions() *[]float64 { return &gb.positions } ================================================ FILE: builder_test.go ================================================ package colorgrad import ( "image/color" "testing" ) func domain(min, max float64) [2]float64 { return [2]float64{min, max} } func Test_Builder(t *testing.T) { var grad Gradient var err error // Default colors grad, err = NewGradient().Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{0, 1}) test(t, grad.At(0).HexString(), "#000000") test(t, grad.At(1).HexString(), "#ffffff") // Single color grad, err = NewGradient(). Colors(Rgb8(0, 255, 0, 255)). Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{0, 1}) test(t, grad.At(0).HexString(), "#00ff00") test(t, grad.At(1).HexString(), "#00ff00") // Default domain grad, err = NewGradient(). HtmlColors("red", "lime", "blue"). Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{0, 1}) test(t, grad.At(0.0).HexString(), "#ff0000") test(t, grad.At(0.5).HexString(), "#00ff00") test(t, grad.At(1.0).HexString(), "#0000ff") // Custom domain grad, err = NewGradient(). HtmlColors("red", "lime", "blue"). Domain(-100, 100). Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{-100, 100}) test(t, grad.At(-100).HexString(), "#ff0000") test(t, grad.At(0).HexString(), "#00ff00") test(t, grad.At(100).HexString(), "#0000ff") // Color position grad, err = NewGradient(). HtmlColors("red", "lime", "blue"). Domain(13, 27.3, 90). Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{13, 90}) test(t, grad.At(13).HexString(), "#ff0000") test(t, grad.At(27.3).HexString(), "#00ff00") test(t, grad.At(90).HexString(), "#0000ff") // Multiple colors, custom domain gb := NewGradient() grad, err = gb.HtmlColors("#00f", "#00ffff"). Colors( Rgb8(255, 255, 0, 255), Hwb(320, 0.1, 0.3, 1), GoColor(color.RGBA{R: 127, G: 0, B: 0, A: 127}), GoColor(color.Gray{185}), ). HtmlColors("gold", "hwb(320, 10%, 30%)"). Domain(10, 50). Mode(BlendRgb). Interpolation(InterpolationLinear). Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{10, 50}) testSlice(t, colors2hex(grad.Colors(8)), []string{ "#0000ff", "#00ffff", "#ffff00", "#b31980", // xxx "#ff00007f", "#b9b9b9", "#ffd700", "#b31a80", }) testSlice(t, colors2hex(*gb.GetColors()), []string{ "#0000ff", "#00ffff", "#ffff00", "#b31a80", "#ff00007f", "#b9b9b9", "#ffd700", "#b31a80", }) // Reset grad, err = gb.Reset().Build() test(t, err, nil) test(t, domain(grad.Domain()), [2]float64{0, 1}) test(t, grad.At(0).HexString(), "#000000") test(t, grad.At(1).HexString(), "#ffffff") // Filter stops gb = NewGradient() gb.HtmlColors("gold", "red", "blue", "yellow", "black", "white", "plum") gb.Domain(0, 0, 0.5, 0.5, 0.5, 1, 1) _, err = gb.Build() test(t, err, nil) testSlice(t, *gb.GetPositions(), []float64{0, 0.5, 0.5, 1}) testSlice(t, colors2hex(*gb.GetColors()), []string{ "#ff0000", "#0000ff", "#000000", "#ffffff", }) // --- Builder Error // Invalid HTML colors grad, err = NewGradient(). HtmlColors("#777", "bloodred", "#bbb", "#zzz"). Build() testTrue(t, err != nil) testTrue(t, isZeroGradient(grad)) // Invalid domain grad, err = NewGradient(). HtmlColors("#777", "#fff", "#ccc", "#222"). Domain(0, 0.5, 1). Build() testTrue(t, err != nil) testTrue(t, isZeroGradient(grad)) // Invalid domain grad, err = NewGradient(). HtmlColors("#777", "#fff", "#ccc", "#222"). Domain(0, 0.71, 0.70, 1). Build() testTrue(t, err != nil) testTrue(t, isZeroGradient(grad)) // Invalid domain grad, err = NewGradient(). HtmlColors("#f00", "#0f0"). Domain(1, 1). Build() testTrue(t, err != nil) testTrue(t, isZeroGradient(grad)) // Invalid domain grad, err = NewGradient(). HtmlColors("#777", "#fff", "#ccc", "#222"). Domain(1, 0). Build() testTrue(t, err != nil) testTrue(t, isZeroGradient(grad)) } func Test_CssGradient(t *testing.T) { testData := []struct { s string position []float64 colors []string }{ { "blue", []float64{0, 1}, []string{"#0000ff", "#0000ff"}, }, { "red, blue", []float64{0, 1}, []string{"#ff0000", "#0000ff"}, }, { "red, lime, blue", []float64{0, 0.5, 1}, []string{"#ff0000", "#00ff00", "#0000ff"}, }, { "red, lime 75%, blue", []float64{0, 0.75, 1}, []string{"#ff0000", "#00ff00", "#0000ff"}, }, { "red 70%, lime, blue", []float64{0, 0.7, 0.85, 1}, []string{"#ff0000", "#ff0000", "#00ff00", "#0000ff"}, }, { "red 5%, lime, blue 85%", []float64{0, 0.05, 0.45, 0.85, 1}, []string{"#ff0000", "#ff0000", "#00ff00", "#0000ff", "#0000ff"}, }, { "#00f, #ff0 10% 35%, #f00", []float64{0, 0.1, 0.35, 1}, []string{"#0000ff", "#ffff00", "#ffff00", "#ff0000"}, }, { "red, 75%, #ff0", []float64{0, 0.75, 1}, []string{"#ff0000", "#ff8000", "#ffff00"}, }, { "red -5, lime, blue", []float64{-5, -2, 1}, []string{"#ff0000", "#00ff00", "#0000ff"}, }, } for _, d := range testData { gb := NewGradient() gb.Css(d.s) _, err := gb.Build() test(t, err, nil) testSliceF(t, d.position, *gb.GetPositions()) testSlice(t, d.colors, colors2hex(*gb.GetColors())) /*dmin, dmax := grad.Domain() test(t, 0.0, dmin) test(t, 1.0, dmax)*/ } // Invalid format invalid := []string{ "", " ", "reds, blue", "0, red, lime", "red, lime, 100%", "deeppink, 0.4, 0.9, pink", "50%", "0%, 100%", "æ", "red â 15%, blue", "red, ä, blue", } for _, s := range invalid { _, err := NewGradient().Css(s).Build() testTrue(t, err != nil) test(t, err.Error(), "invalid CSS gradient") } } ================================================ FILE: catmull_rom.go ================================================ package colorgrad import ( "math" ) // Adapted from https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html func toCatmullRomSegments(values []float64) [][4]float64 { alpha := 0.5 tension := 0.0 n := len(values) vals := make([]float64, n+2) vals[0] = 2*values[0] - values[1] for i, v := range values { vals[i+1] = v } vals[n+1] = 2*values[n-1] - values[n-2] segments := [][4]float64{} for i := 1; i < len(vals)-2; i++ { v0 := vals[i-1] v1 := vals[i] v2 := vals[i+1] v3 := vals[i+2] t0 := 0.0 t1 := t0 + math.Pow(math.Abs(v0-v1), alpha) t2 := t1 + math.Pow(math.Abs(v1-v2), alpha) t3 := t2 + math.Pow(math.Abs(v2-v3), alpha) m1 := (1. - tension) * (t2 - t1) * ((v0-v1)/(t0-t1) - (v0-v2)/(t0-t2) + (v1-v2)/(t1-t2)) m2 := (1. - tension) * (t2 - t1) * ((v1-v2)/(t1-t2) - (v1-v3)/(t1-t3) + (v2-v3)/(t2-t3)) if math.IsNaN(m1) { m1 = 0 } if math.IsNaN(m2) { m2 = 0 } a := 2*v1 - 2*v2 + m1 + m2 b := -3*v1 + 3*v2 - 2*m1 - m2 c := m1 d := v1 segments = append(segments, [4]float64{a, b, c, d}) } return segments } type catmullRomGradient struct { segments [][4][4]float64 positions []float64 min float64 max float64 mode BlendMode first Color last Color } func newCatmullRomGradient(colors []Color, positions []float64, space BlendMode) Gradient { n := len(colors) a := make([]float64, n) b := make([]float64, n) c := make([]float64, n) d := make([]float64, n) for i, col := range colors { var arr [4]float64 switch space { case BlendRgb: arr = [4]float64{col.R, col.G, col.B, col.A} case BlendLinearRgb: arr = col2linearRgb(col) case BlendLab: arr = col2lab(col) case BlendOklab: arr = col2oklab(col) } a[i] = arr[0] b[i] = arr[1] c[i] = arr[2] d[i] = arr[3] } s1 := toCatmullRomSegments(a) s2 := toCatmullRomSegments(b) s3 := toCatmullRomSegments(c) s4 := toCatmullRomSegments(d) segments := make([][4][4]float64, len(s1)) for i, v1 := range s1 { segments[i] = [4][4]float64{ v1, s2[i], s3[i], s4[i], } } min := positions[0] max := positions[n-1] gradbase := catmullRomGradient{ segments: segments, positions: positions, min: min, max: max, mode: space, first: colors[0], last: colors[len(colors)-1], } return Gradient{ Core: gradbase, Min: min, Max: max, } } func (g catmullRomGradient) At(t float64) Color { if math.IsNaN(t) { return Color{A: 1} } if t <= g.min { return g.first } if t >= g.max { return g.last } low := 0 high := len(g.positions) for low < high { mid := (low + high) / 2 if g.positions[mid] < t { low = mid + 1 } else { high = mid } } if low == 0 { low = 1 } pos0 := g.positions[low-1] pos1 := g.positions[low] seg_a := g.segments[low-1][0] seg_b := g.segments[low-1][1] seg_c := g.segments[low-1][2] seg_d := g.segments[low-1][3] t1 := (t - pos0) / (pos1 - pos0) t2 := t1 * t1 t3 := t2 * t1 a := seg_a[0]*t3 + seg_a[1]*t2 + seg_a[2]*t1 + seg_a[3] b := seg_b[0]*t3 + seg_b[1]*t2 + seg_b[2]*t1 + seg_b[3] c := seg_c[0]*t3 + seg_c[1]*t2 + seg_c[2]*t1 + seg_c[3] d := seg_d[0]*t3 + seg_d[1]*t2 + seg_d[2]*t1 + seg_d[3] switch g.mode { case BlendRgb: return Color{R: a, G: b, B: c, A: d} case BlendLinearRgb: return LinearRgb(a, b, c, d) case BlendLab: return Lab(a, b, c, d).Clamp() case BlendOklab: return Oklab(a, b, c, d).Clamp() } return Color{} } ================================================ FILE: catmull_rom_test.go ================================================ package colorgrad import ( "math" "testing" ) func Test_CatmullRomGradient(t *testing.T) { grad, err := NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Mode(BlendRgb). Interpolation(InterpolationCatmullRom). Build() test(t, err, nil) test(t, grad.At(0.00).HexString(), "#ff0000") test(t, grad.At(0.25).HexString(), "#609f00") test(t, grad.At(0.50).HexString(), "#00ff00") test(t, grad.At(0.75).HexString(), "#009f60") test(t, grad.At(1.00).HexString(), "#0000ff") testSlice(t, colors2hex(grad.Colors(5)), []string{ "#ff0000", "#609f00", "#00ff00", "#009f60", "#0000ff", }) test(t, grad.At(-0.1).HexString(), "#ff0000") test(t, grad.At(1.11).HexString(), "#0000ff") test(t, grad.At(math.NaN()).HexString(), "#000000") } ================================================ FILE: css_gradient.go ================================================ package colorgrad import ( "math" "strings" "github.com/mazznoer/csscolorparser" ) func parseCss(s string) ([]cssGradientStop, bool) { stops := []cssGradientStop{} for _, stop := range splitByComma(s) { if !prosesStop(&stops, splitBySpace(stop)) { return stops, false } } if len(stops) == 0 { return stops, false } if stops[0].color == nil { return stops, false } if stops[0].pos == nil { stops[0].pos = ptr(0.0) } for i, stop := range stops { if i == len(stops)-1 { if stop.pos == nil { stops[i].pos = ptr(1.0) } break } if stop.color == nil { if stops[i+1].color == nil { return stops, false } stops[i].color = ptrColor(blendRgb(*stops[i-1].color, *stops[i+1].color, 0.5)) } } if *stops[0].pos > 0.0 { stops = append([]cssGradientStop{{ptr(0.0), stops[0].color}}, stops...) } if *stops[len(stops)-1].pos < 1.0 { stops = append(stops, cssGradientStop{ptr(1.0), stops[len(stops)-1].color}) } for i, stop := range stops { if stop.pos == nil { for j := i + 1; j < len(stops); j++ { if stops[j].pos != nil { prev := *stops[i-1].pos next := *stops[j].pos stops[i].pos = ptr(prev + (next-prev)/float64(j-i+1)) break } } } if i > 0 { stops[i].pos = ptr(math.Max(*stops[i].pos, *stops[i-1].pos)) } } for _, stop := range stops { if stop.color == nil || stop.pos == nil { return stops, false } } return stops, true } func ptr(f float64) *float64 { return &f } func ptrColor(c Color) *Color { return &c } type cssGradientStop struct { pos *float64 color *Color } func prosesStop(stops *[]cssGradientStop, arr []string) bool { switch len(arr) { case 1: col, err := csscolorparser.Parse(arr[0]) if err == nil { *stops = append(*stops, cssGradientStop{nil, &col}) return true } pos, ok := parsePos(arr[0]) if ok { *stops = append(*stops, cssGradientStop{&pos, nil}) return true } return false case 2: col, err := csscolorparser.Parse(arr[0]) if err != nil { return false } pos, ok := parsePos(arr[1]) if !ok { return false } *stops = append(*stops, cssGradientStop{&pos, &col}) case 3: col, err := csscolorparser.Parse(arr[0]) if err != nil { return false } pos1, ok1 := parsePos(arr[1]) if !ok1 { return false } pos2, ok2 := parsePos(arr[2]) if !ok2 { return false } *stops = append(*stops, cssGradientStop{&pos1, &col}) *stops = append(*stops, cssGradientStop{&pos2, &col}) default: return false } return true } func splitByComma(s string) []string { res := []string{} beg := 0 inside := false for i := 0; i < len(s); i++ { if s[i] == ',' && !inside { res = append(res, s[beg:i]) beg = i + 1 } else if s[i] == '(' { inside = true } else if s[i] == ')' { inside = false } } return append(res, s[beg:]) } func splitBySpace(s string) []string { res := []string{} beg := 0 inside := false for i := 0; i < len(s); i++ { if s[i] == ' ' && !inside { if len(s[beg:i]) > 0 { res = append(res, s[beg:i]) } beg = i + 1 } else if s[i] == '(' { inside = true } else if s[i] == ')' { inside = false } } if len(s[beg:]) > 0 { res = append(res, s[beg:]) } return res } func parsePos(s string) (float64, bool) { if strings.HasSuffix(s, "%") { f, ok := parseFloat(s[:len(s)-1]) if ok { return f / 100, true } return 0, false } f, ok := parseFloat(s) return f, ok } ================================================ FILE: example_test.go ================================================ package colorgrad_test import ( "fmt" "github.com/mazznoer/colorgrad" ) func Example_presetGradient() { grad := colorgrad.Rainbow() dmin, dmax := grad.Domain() fmt.Println(dmin, dmax) fmt.Println(grad.At(0).HexString()) // Output: // 0 1 // #6e40aa } func Example_customGradient() { grad, err := colorgrad.NewGradient(). HtmlColors("red", "#FFD700", "lime"). Domain(0, 0.35, 1). Mode(colorgrad.BlendOklab). Build() if err != nil { panic(err) } fmt.Println(grad.At(0).HexString()) fmt.Println(grad.At(1).HexString()) // Output: // #ff0000 // #00ff00 } ================================================ FILE: examples/.gitignore ================================================ basic gradients *.png output/*.png ================================================ FILE: examples/basic.go ================================================ //go:build ignore // +build ignore package main import ( "image" "image/png" "os" "github.com/mazznoer/colorgrad" ) func main() { grad, _ := colorgrad.NewGradient(). HtmlColors("#c41189", "#00BFFF", "#FFD700"). Build() w := 1500 h := 70 fw := float64(w) img := image.NewRGBA(image.Rect(0, 0, w, h)) for x := 0; x < w; x++ { col := grad.At(float64(x) / fw) for y := 0; y < h; y++ { img.Set(x, y, col) } } file, err := os.Create("gradient.png") if err != nil { panic(err.Error()) } defer file.Close() png.Encode(file, img) } ================================================ FILE: examples/ggr/Abstract_1.ggr ================================================ GIMP Gradient Name: Abstract 1 6 0.000000 0.286311 0.572621 0.269543 0.259267 1.000000 1.000000 0.215635 0.407414 0.984953 1.000000 0 0 0 0 0.572621 0.657763 0.716194 0.215635 0.407414 0.984953 1.000000 0.040368 0.833333 0.619375 1.000000 0 0 0 0 0.716194 0.734558 0.749583 0.040368 0.833333 0.619375 1.000000 0.680490 0.355264 0.977430 1.000000 0 0 0 0 0.749583 0.784641 0.824708 0.680490 0.355264 0.977430 1.000000 0.553909 0.351853 0.977430 1.000000 0 0 0 0 0.824708 0.853088 0.876461 0.553909 0.351853 0.977430 1.000000 1.000000 0.000000 1.000000 1.000000 0 0 0 0 0.876461 0.943172 1.000000 1.000000 0.000000 1.000000 1.000000 1.000000 1.000000 0.000000 1.000000 0 0 0 0 ================================================ FILE: examples/ggr/Full_saturation_spectrum_CW.ggr ================================================ GIMP Gradient Name: Full saturation spectrum CW 1 0.000000 0.500000 1.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 0.000000 1.000000 0 2 ================================================ FILE: examples/gradients.go ================================================ //go:build ignore // +build ignore package main import ( "flag" "fmt" "image" "image/color" "image/draw" "image/png" "os" "path/filepath" "strings" "github.com/mazznoer/colorgrad" ) type data struct { gradient colorgrad.Gradient name string } type Opt struct { testData bool saveImage bool } func main() { var opt Opt flag.BoolVar(&opt.testData, "test", false, "generate test data") flag.BoolVar(&opt.saveImage, "save-img", false, "save image file") flag.Parse() presetGradients := []data{ {colorgrad.CubehelixDefault(), "CubehelixDefault"}, {colorgrad.Warm(), "Warm"}, {colorgrad.Cool(), "Cool"}, {colorgrad.Rainbow(), "Rainbow"}, {colorgrad.Cividis(), "Cividis"}, {colorgrad.Sinebow(), "Sinebow"}, {colorgrad.Turbo(), "Turbo"}, {colorgrad.Viridis(), "Viridis"}, {colorgrad.Plasma(), "Plasma"}, {colorgrad.Magma(), "Magma"}, {colorgrad.Inferno(), "Inferno"}, {colorgrad.BrBG(), "BrBG"}, {colorgrad.PRGn(), "PRGn"}, {colorgrad.PiYG(), "PiYG"}, {colorgrad.PuOr(), "PuOr"}, {colorgrad.RdBu(), "RdBu"}, {colorgrad.RdGy(), "RdGy"}, {colorgrad.RdYlBu(), "RdYlBu"}, {colorgrad.RdYlGn(), "RdYlGn"}, {colorgrad.Spectral(), "Spectral"}, {colorgrad.Blues(), "Blues"}, {colorgrad.Greens(), "Greens"}, {colorgrad.Greys(), "Greys"}, {colorgrad.Oranges(), "Oranges"}, {colorgrad.Purples(), "Purples"}, {colorgrad.Reds(), "Reds"}, {colorgrad.BuGn(), "BuGn"}, {colorgrad.BuPu(), "BuPu"}, {colorgrad.GnBu(), "GnBu"}, {colorgrad.OrRd(), "OrRd"}, {colorgrad.PuBuGn(), "PuBuGn"}, {colorgrad.PuBu(), "PuBu"}, {colorgrad.PuRd(), "PuRd"}, {colorgrad.RdPu(), "RdPu"}, {colorgrad.YlGnBu(), "YlGnBu"}, {colorgrad.YlGn(), "YlGn"}, {colorgrad.YlOrBr(), "YlOrBr"}, {colorgrad.YlOrRd(), "YlOrRd"}, } // Custom gradients grad1, _ := colorgrad.NewGradient().Build() grad2, _ := colorgrad.NewGradient(). Colors( colorgrad.Rgb8(0, 206, 209, 255), colorgrad.Rgb8(255, 105, 180, 255), colorgrad.Rgb(0.274, 0.5, 0.7, 1), colorgrad.Hsv(50, 1, 1, 1), colorgrad.Hsv(348, 0.9, 0.8, 1), ). Build() grad3, _ := colorgrad.NewGradient(). HtmlColors("#C41189", "#00BFFF", "#FFD700"). Build() grad4, _ := colorgrad.NewGradient(). HtmlColors("gold", "hotpink", "darkturquoise"). Build() grad5, _ := colorgrad.NewGradient(). HtmlColors( "rgb(125,110,221)", "rgb(90%,45%,97%)", "hsl(229,79%,85%)", ). Build() // Domain & color position domain1, _ := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Build() domain2, _ := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(0, 100). Build() domain3, _ := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(-1, 1). Build() colorPos1, _ := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(0, 0.7, 1). Build() colorPos2, _ := colorgrad.NewGradient(). HtmlColors("deeppink", "gold", "seagreen"). Domain(15, 30, 80). Build() colorPos3, _ := colorgrad.NewGradient(). HtmlColors("deeppink", "#6d27a1", "#ff0", "#1185e4"). Domain(0, 0.7, 0.7, 1). Build() // Blending modes colors := []string{"#fff", "#00f"} blendRgb, _ := colorgrad.NewGradient(). HtmlColors(colors...). Mode(colorgrad.BlendRgb). Build() blendLinearRgb, _ := colorgrad.NewGradient(). HtmlColors(colors...). Mode(colorgrad.BlendLinearRgb). Build() blendOklab, _ := colorgrad.NewGradient(). HtmlColors(colors...). Mode(colorgrad.BlendOklab). Build() // Interpolation modes colors = []string{"#C41189", "#00BFFF", "#FFD700"} interpLinear, _ := colorgrad.NewGradient(). HtmlColors(colors...). Interpolation(colorgrad.InterpolationLinear). Build() interpCatmullRom, _ := colorgrad.NewGradient(). HtmlColors(colors...). Interpolation(colorgrad.InterpolationCatmullRom). Build() interpBasis, _ := colorgrad.NewGradient(). HtmlColors(colors...). Interpolation(colorgrad.InterpolationBasis). Build() customGradients := []data{ {grad1, "custom-default"}, {grad2, "custom-colors"}, {grad3, "custom-hex-colors"}, {grad4, "custom-named-colors"}, {grad5, "custom-css-colors"}, {domain1, "domain-default"}, {domain2, "domain-0-100"}, {domain3, "domain-neg1-1"}, {colorPos1, "color-position-1"}, {colorPos2, "color-position-2"}, {colorPos3, "color-position-3"}, {blendRgb, "blend-rgb"}, {blendLinearRgb, "blend-linear-rgb"}, {blendOklab, "blend-oklab"}, {interpLinear, "interpolation-linear"}, {interpCatmullRom, "interpolation-catmull-rom"}, {interpBasis, "interpolation-basis"}, } // Sharp gradients grad := colorgrad.Rainbow() var segments uint = 11 sharpGradients := []data{ {grad.Sharp(segments, 0.0), "0.0"}, {grad.Sharp(segments, 0.25), "0.25"}, {grad.Sharp(segments, 0.5), "0.5"}, {grad.Sharp(segments, 0.75), "0.75"}, {grad.Sharp(segments, 1.0), "1.0"}, } if opt.testData { sample := 12 for _, d := range presetGradients { colors := d.gradient.Colors(uint(sample)) hexColors := make([]string, len(colors)) for i, c := range colors { hexColors[i] = fmt.Sprintf("%q", c.HexString()) } fmt.Printf("grad = %s()\n", d.name) fmt.Printf("testSlice(t, colors2hex(grad.Colors(%v)), []string{\n", sample) fmt.Printf(" %v,\n", strings.Join(hexColors, ", ")) fmt.Printf("})\n\n") } return } width := 1000 height := 150 padding := 10 err := os.Mkdir("output", 0750) if err != nil && !os.IsExist(err) { panic(err) } for _, d := range presetGradients { filepath := fmt.Sprintf("output/preset-%s.png", d.name) fmt.Println(filepath) if opt.saveImage { img := gradRgbPlot(d.gradient, width, height, padding) savePNG(img, filepath) } } for _, d := range customGradients { filepath := fmt.Sprintf("output/%s.png", d.name) fmt.Println(filepath) if opt.saveImage { img := gradRgbPlot(d.gradient, width, height, padding) savePNG(img, filepath) } } for _, d := range sharpGradients { filepath := fmt.Sprintf("output/sharp-smoothness-%s.png", d.name) fmt.Println(filepath) if opt.saveImage { img := gradRgbPlot(d.gradient, width, height, padding) savePNG(img, filepath) } } // GIMP gradients ggrPath := "./ggr/*.ggr" //ggrPath = "/usr/share/gimp/2.0/gradients/*.ggr" ggrs, ggrsErr := filepath.Glob(ggrPath) if ggrsErr == nil { for _, s := range ggrs { grad := parseGgr(s) filepath := fmt.Sprintf("output/ggr_%s.png", filepath.Base(s)) fmt.Println(filepath) if opt.saveImage { img := gradRgbPlot(grad, width, height, padding) savePNG(img, filepath) } } } else { fmt.Println(ggrsErr) } } func parseGgr(filepath string) colorgrad.Gradient { black := colorgrad.Rgb(0, 0, 0, 1) white := colorgrad.Rgb(1, 1, 1, 1) file, err := os.Open(filepath) if err != nil { panic(err) } defer file.Close() grad, _, err2 := colorgrad.ParseGgr(file, black, white) if err2 != nil { panic(err2) } return grad } func gradientImage(gradient colorgrad.Gradient, width, height int) image.Image { fw := float64(width) dmin, dmax := gradient.Domain() img := image.NewRGBA(image.Rect(0, 0, width, height)) for x := 0; x < width; x++ { col := gradient.At(remap(float64(x), 0, fw, dmin, dmax)) for y := 0; y < height; y++ { img.Set(x, y, col) } } return img } func rgbPlot(gradient colorgrad.Gradient, width, height int) image.Image { img := image.NewRGBA(image.Rect(0, 0, width, height)) draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray{235}}, image.Point{}, draw.Src) dmin, dmax := gradient.Domain() fw := float64(width) y1 := 0.0 y2 := float64(height) for x := 0; x < width; x++ { col := gradient.At(remap(float64(x), 0, fw, dmin, dmax)) r := remap(col.R, 0, 1, y2, y1) g := remap(col.G, 0, 1, y2, y1) b := remap(col.B, 0, 1, y2, y1) img.Set(x, int(r), color.NRGBA{255, 0, 0, 255}) img.Set(x, int(g), color.NRGBA{0, 128, 0, 255}) img.Set(x, int(b), color.NRGBA{0, 0, 255, 255}) } return img } func gradRgbPlot(gradient colorgrad.Gradient, width, height, padding int) image.Image { w := width + padding*2 h := height*2 + padding*3 img := image.NewRGBA(image.Rect(0, 0, w, h)) draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray{255}}, image.Point{}, draw.Src) gradImg := gradientImage(gradient, width, height) plotImg := rgbPlot(gradient, width, height) x1 := padding y1 := padding x2 := x1 + width y2 := y1 + height draw.Draw(img, image.Rect(x1, y1, x2, y2), gradImg, image.Point{}, draw.Src) y1 = y2 + padding y2 = y1 + height draw.Draw(img, image.Rect(x1, y1, x2, y2), plotImg, image.Point{}, draw.Src) return img } // Map t which is in range [a, b] to range [c, d] func remap(t, a, b, c, d float64) float64 { return (t-a)*((d-c)/(b-a)) + c } func savePNG(img image.Image, filepath string) { file, err := os.Create(filepath) if err != nil { panic(err.Error()) } defer file.Close() png.Encode(file, img) } ================================================ FILE: gimp.go ================================================ package colorgrad import ( "bufio" "fmt" "io" "math" "strings" ) // References: // https://gitlab.gnome.org/GNOME/gimp/-/blob/master/devel-docs/ggr.txt // https://gitlab.gnome.org/GNOME/gimp/-/blob/master/app/core/gimpgradient.c // https://gitlab.gnome.org/GNOME/gimp/-/blob/master/app/core/gimpgradient-load.c const epsilon = 1e-10 const fracPi2 = math.Pi / 2 type blendingType int const ( linear blendingType = iota curved sinusoidal sphericalIncreasing sphericalDecreasing step ) type coloringType int const ( rgb coloringType = iota hsvCcw hsvCw ) type gimpSegment struct { // Left endpoint color lcolor Color // Right endpoint color rcolor Color // Left endpoint coordinate lpos float64 // Midpoint coordinate mpos float64 // Right endpoint coordinate rpos float64 // Blending function type blending blendingType // Coloring type coloring coloringType } type gimpGradient struct { segments []gimpSegment min float64 max float64 } func (ggr gimpGradient) At(t float64) Color { if t <= ggr.min { return ggr.segments[0].lcolor } if t >= ggr.max { return ggr.segments[len(ggr.segments)-1].rcolor } if math.IsNaN(t) { return Color{A: 1} } low := 0 high := len(ggr.segments) mid := 0 for low < high { mid = (low + high) / 2 if t > ggr.segments[mid].rpos { low = mid + 1 } else if t < ggr.segments[mid].lpos { high = mid } else { break } } seg := ggr.segments[mid] seg_len := seg.rpos - seg.lpos var middle float64 var pos float64 if seg_len < epsilon { middle = 0.5 pos = 0.5 } else { middle = (seg.mpos - seg.lpos) / seg_len pos = (t - seg.lpos) / seg_len } var f float64 switch seg.blending { case linear: f = calc_linear_factor(middle, pos) case curved: if middle < epsilon { return seg.rcolor } else if math.Abs(1-middle) < epsilon { return seg.lcolor } else { f = math.Exp(-math.Ln2 * math.Log10(pos) / math.Log10(middle)) } case sinusoidal: x := calc_linear_factor(middle, pos) f = (math.Sin(-fracPi2+math.Pi*x) + 1) / 2 case sphericalIncreasing: x := calc_linear_factor(middle, pos) - 1 f = math.Sqrt(1 - x*x) case sphericalDecreasing: x := calc_linear_factor(middle, pos) f = 1 - math.Sqrt(1-x*x) case step: if pos >= middle { return seg.rcolor } else { return seg.lcolor } } switch seg.coloring { case rgb: return blendRgb(seg.lcolor, seg.rcolor, f) case hsvCcw: return blendHsvCcw(seg.lcolor, seg.rcolor, f) case hsvCw: return blendHsvCw(seg.lcolor, seg.rcolor, f) } return ggr.segments[0].lcolor } func calc_linear_factor(middle, pos float64) float64 { if pos <= middle { if middle < epsilon { return 0 } else { return 0.5 * pos / middle } } else { pos = pos - middle middle = 1 - middle if middle < epsilon { return 1 } else { return 0.5 + 0.5*pos/middle } } } func blendHsvCcw(c1, c2 Color, t float64) Color { hsvA := col2hsv(c1) hsvB := col2hsv(c2) var hue float64 if hsvA[0] < hsvB[0] { hue = hsvA[0] + ((hsvB[0] - hsvA[0]) * t) } else { h := hsvA[0] + ((360 - (hsvA[0] - hsvB[0])) * t) if h > 360 { hue = h - 360 } else { hue = h } } return Hsv( hue, hsvA[1]+t*(hsvB[1]-hsvA[1]), hsvA[2]+t*(hsvB[2]-hsvA[2]), hsvA[3]+t*(hsvB[3]-hsvA[3]), ) } func blendHsvCw(c1, c2 Color, t float64) Color { hsvA := col2hsv(c1) hsvB := col2hsv(c2) var hue float64 if hsvB[0] < hsvA[0] { hue = hsvA[0] - ((hsvA[0] - hsvB[0]) * t) } else { h := hsvA[0] - ((360 - (hsvB[0] - hsvA[0])) * t) if h < 0 { hue = h + 360 } else { hue = h } } return Hsv( hue, hsvA[1]+t*(hsvB[1]-hsvA[1]), hsvA[2]+t*(hsvB[2]-hsvA[2]), hsvA[3]+t*(hsvB[3]-hsvA[3]), ) } func ParseGgr(r io.Reader, fg, bg Color) (Gradient, string, error) { zgrad := Gradient{ Core: zeroGradient{}, Min: 0, Max: 1, } segments := []gimpSegment{} var nseg int var name string xseg := 0 i := 0 scanner := bufio.NewScanner(r) for scanner.Scan() { if i == 0 { if scanner.Text() != "GIMP Gradient" { return zgrad, name, fmt.Errorf("invalid header") } } else if i == 1 { if !strings.HasPrefix(scanner.Text(), "Name:") { return zgrad, name, fmt.Errorf("invalid header") } name = strings.TrimSpace(scanner.Text()[5:]) } else if i == 2 { t, ok := parseFloat(scanner.Text()) if ok { nseg = int(t) } else { return zgrad, name, fmt.Errorf("invalid header") } } else { if i >= nseg+3 { break } seg, ok := parseSegment(scanner.Text(), fg, bg) if ok { segments = append(segments, seg) xseg++ } else { return zgrad, name, fmt.Errorf("invalid segment") } } i++ } if err := scanner.Err(); err != nil { return zgrad, name, err } if len(segments) == 0 { return zgrad, name, fmt.Errorf("segments %v", i) } if xseg < nseg { return zgrad, name, fmt.Errorf("wrong segments count, %v, %v", nseg, xseg) } gradbase := gimpGradient{ segments: segments, min: 0, max: 1, } return Gradient{ Core: gradbase, Min: 0, Max: 1, }, name, nil } func parseSegment(s string, fg, bg Color) (gimpSegment, bool) { params := strings.Fields(s) plen := len(params) if plen != 13 && plen != 15 { return gimpSegment{}, false } d := make([]float64, 15) for i, x := range params { t, ok := parseFloat(x) if ok { d[i] = t continue } return gimpSegment{}, false } if plen == 13 { d[13] = 0 d[14] = 0 } var blending blendingType switch int(d[11]) { case 0: blending = linear case 1: blending = curved case 2: blending = sinusoidal case 3: blending = sphericalIncreasing case 4: blending = sphericalDecreasing case 5: blending = step default: return gimpSegment{}, false } var coloring coloringType switch int(d[12]) { case 0: coloring = rgb case 1: coloring = hsvCcw case 2: coloring = hsvCw default: return gimpSegment{}, false } var lcolor Color switch int(d[13]) { case 0: lcolor = Color{R: d[3], G: d[4], B: d[5], A: d[6]} case 1: lcolor = fg case 2: lcolor = Rgb(fg.R, fg.G, fg.B, 0) case 3: lcolor = bg case 4: lcolor = Rgb(bg.R, bg.G, bg.B, 0) default: return gimpSegment{}, false } var rcolor Color switch int(d[14]) { case 0: rcolor = Color{R: d[7], G: d[8], B: d[9], A: d[10]} case 1: rcolor = fg case 2: rcolor = Rgb(fg.R, fg.G, fg.B, 0) case 3: rcolor = bg case 4: rcolor = Rgb(bg.R, bg.G, bg.B, 0) default: return gimpSegment{}, false } return gimpSegment{ lcolor: lcolor, rcolor: rcolor, lpos: d[0], mpos: d[1], rpos: d[2], blending: blending, coloring: coloring, }, true } ================================================ FILE: gimp_test.go ================================================ package colorgrad import ( "math" "strings" "testing" ) func Test_GIMPGradient(t *testing.T) { black := Rgb(0, 0, 0, 1) red := Rgb(1, 0, 0, 1) blue := Rgb(0, 0, 1, 1) // Black to white ggr := "GIMP Gradient\nName: My Gradient\n1\n0 0.5 1 0 0 0 1 1 1 1 1 0 0 0 0" grad, name, err := ParseGgr(strings.NewReader(ggr), black, black) test(t, err, nil) test(t, name, "My Gradient") test(t, grad.At(0).HexString(), "#000000") test(t, grad.At(1).HexString(), "#ffffff") test(t, grad.At(-0.5).HexString(), "#000000") test(t, grad.At(1.5).HexString(), "#ffffff") test(t, grad.At(math.NaN()).HexString(), "#000000") // Foreground to background ggr = "GIMP Gradient\nName: My Gradient\n1\n0 0.5 1 0 0 0 1 1 1 1 1 0 0 1 3" grad, name, err = ParseGgr(strings.NewReader(ggr), red, blue) test(t, err, nil) test(t, name, "My Gradient") test(t, grad.At(0).HexString(), "#ff0000") test(t, grad.At(1).HexString(), "#0000ff") // Blending function: step ggr = "GIMP Gradient\nName: My Gradient\n1\n0 0.5 1 1 0 0 1 0 0 1 1 5 0 0 0" grad, name, err = ParseGgr(strings.NewReader(ggr), black, black) test(t, err, nil) test(t, name, "My Gradient") test(t, grad.At(0.00).HexString(), "#ff0000") test(t, grad.At(0.25).HexString(), "#ff0000") test(t, grad.At(0.49).HexString(), "#ff0000") test(t, grad.At(0.51).HexString(), "#0000ff") test(t, grad.At(0.75).HexString(), "#0000ff") test(t, grad.At(1.00).HexString(), "#0000ff") // Coloring type: HSV CCW (white to blue) ggr = "GIMP Gradient\nName: My Gradient\n1\n0 0.5 1 1 1 1 1 0 0 1 1 0 1 0 0" grad, name, err = ParseGgr(strings.NewReader(ggr), black, black) test(t, err, nil) test(t, name, "My Gradient") test(t, grad.At(0.0).HexString(), "#ffffff") test(t, grad.At(0.5).HexString(), "#80ff80") test(t, grad.At(1.0).HexString(), "#0000ff") // Coloring type: HSV CW (white to blue) ggr = "GIMP Gradient\nName: My Gradient\n1\n0 0.5 1 1 1 1 1 0 0 1 1 0 2 0 0" grad, name, err = ParseGgr(strings.NewReader(ggr), black, black) test(t, err, nil) test(t, name, "My Gradient") test(t, grad.At(0.0).HexString(), "#ffffff") test(t, grad.At(0.5).HexString(), "#ff80ff") test(t, grad.At(1.0).HexString(), "#0000ff") // Invalid formats data := []string{ "", " ", "GIMP Palette\nName: Gold\n#\n252 252 128", "GIMP Gradient\nxx", "GIMP Gradient\nName: Gradient\nx", "GIMP Gradient\nName: Gradient\n1\n0 0 0", } for _, s := range data { _, _, err := ParseGgr(strings.NewReader(s), black, black) testTrue(t, err != nil) } } ================================================ FILE: go.mod ================================================ module github.com/mazznoer/colorgrad go 1.18 require github.com/mazznoer/csscolorparser v0.1.8 ================================================ FILE: go.sum ================================================ github.com/mazznoer/csscolorparser v0.1.8 h1:i7w3wHW99d0q0KZv1ONkU/efXFAKcw1mgEgW6gj8KUA= github.com/mazznoer/csscolorparser v0.1.8/go.mod h1:OQRVvgCyHDCAquR1YWfSwwaDcM0LhnSffGnlbOew/3I= ================================================ FILE: gradient.go ================================================ package colorgrad import ( "image/color" "math" "github.com/mazznoer/csscolorparser" ) type BlendMode int const ( BlendRgb BlendMode = iota BlendLinearRgb BlendLab BlendOklab ) func (b BlendMode) String() string { switch b { case BlendRgb: return "BlendRgb" case BlendLinearRgb: return "BlendLinearRgb" case BlendLab: return "BlendLab" case BlendOklab: return "BlendOklab" } return "" } type Interpolation int const ( InterpolationLinear Interpolation = iota InterpolationSmoothstep InterpolationCatmullRom InterpolationBasis ) func (i Interpolation) String() string { switch i { case InterpolationLinear: return "InterpolationLinear" case InterpolationSmoothstep: return "InterpolationSmoothstep" case InterpolationCatmullRom: return "InterpolationCatmullRom" case InterpolationBasis: return "InterpolationBasis" } return "" } type Color = csscolorparser.Color var Hwb = csscolorparser.FromHwb var Hsv = csscolorparser.FromHsv var Hsl = csscolorparser.FromHsl var LinearRgb = csscolorparser.FromLinearRGB var Lab = csscolorparser.FromLab var Lch = csscolorparser.FromLch var Oklab = csscolorparser.FromOklab var Oklch = csscolorparser.FromOklch func Rgb(r, g, b, a float64) Color { return Color{R: r, G: g, B: b, A: a} } func Rgb8(r, g, b, a uint8) Color { return Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255, A: float64(a) / 255} } func GoColor(col color.Color) Color { r, g, b, a := col.RGBA() if a == 0 { return csscolorparser.Color{} } r *= 0xffff r /= a g *= 0xffff g /= a b *= 0xffff b /= a return csscolorparser.Color{R: float64(r) / 65535.0, G: float64(g) / 65535.0, B: float64(b) / 65535.0, A: float64(a) / 65535.0} } type GradientCore interface { // Get color at certain position At(float64) Color } type Gradient struct { Core GradientCore Min float64 Max float64 } // Get color at certain position func (g Gradient) At(t float64) Color { return g.Core.At(t) } // Get color at certain position func (g Gradient) RepeatAt(t float64) Color { t = norm(t, g.Min, g.Max) return g.Core.At(g.Min + modulo(t, 1)*(g.Max-g.Min)) } // Get color at certain position func (g Gradient) ReflectAt(t float64) Color { t = norm(t, g.Min, g.Max) return g.Core.At(g.Min + math.Abs(modulo(1+t, 2)-1)*(g.Max-g.Min)) } // Get n colors evenly spaced across gradient func (g Gradient) Colors(count uint) []Color { d := g.Max - g.Min l := float64(count) - 1 colors := make([]Color, count) for i := range colors { colors[i] = g.Core.At(g.Min + (float64(i)*d)/l).Clamp() } return colors } // Get the gradient domain min and max func (g Gradient) Domain() (float64, float64) { return g.Min, g.Max } // Return a new hard-edge gradient func (g Gradient) Sharp(segment uint, smoothness float64) Gradient { colors := []Color{} if segment >= 2 { colors = g.Colors(segment) } else { colors = append(colors, g.At(g.Min)) colors = append(colors, g.At(g.Min)) } return newSharpGradient(colors, g.Min, g.Max, smoothness) } type zeroGradient struct { } func (zg zeroGradient) At(t float64) Color { return Color{R: 0, G: 0, B: 0, A: 0} } ================================================ FILE: gradient_test.go ================================================ package colorgrad import ( "fmt" "image/color" "testing" ) func Test_Basic(t *testing.T) { test(t, Rgb(1, 0.8431, 0, 1).HexString(), "#ffd700") test(t, Rgb8(46, 139, 87, 255).HexString(), "#2e8b57") test(t, Hwb(330, 0.4118, 0, 1).HexString(), "#ff69b4") // Go color test(t, GoColor(color.RGBA{R: 255, G: 0, B: 0, A: 255}).HexString(), "#ff0000") test(t, GoColor(color.RGBA{R: 127, G: 0, B: 0, A: 127}).HexString(), "#ff00007f") test(t, GoColor(color.RGBA{R: 0, G: 0, B: 0, A: 0}).HexString(), "#00000000") test(t, GoColor(color.NRGBA{R: 0, G: 255, B: 0, A: 255}).HexString(), "#00ff00") test(t, GoColor(color.NRGBA{R: 0, G: 255, B: 0, A: 127}).HexString(), "#00ff007f") test(t, GoColor(color.Gray{0}).HexString(), "#000000") test(t, GoColor(color.Gray{127}).HexString(), "#7f7f7f") // Enums test(t, BlendRgb.String(), "BlendRgb") test(t, fmt.Sprintf("%s", BlendLinearRgb), "BlendLinearRgb") test(t, fmt.Sprintf("%v", BlendOklab), "BlendOklab") test(t, InterpolationLinear.String(), "InterpolationLinear") test(t, fmt.Sprintf("%s", InterpolationCatmullRom), "InterpolationCatmullRom") test(t, fmt.Sprintf("%v", InterpolationBasis), "InterpolationBasis") } func Test_GetColors(t *testing.T) { grad, _ := NewGradient().Build() test(t, len(grad.Colors(0)), 0) test(t, grad.Colors(1)[0].HexString(), "#000000") testSlice(t, colors2hex(grad.Colors(2)), []string{ "#000000", "#ffffff", }) testSlice(t, colors2hex(grad.Colors(3)), []string{ "#000000", "#808080", "#ffffff", }) grad, _ = NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Domain(-1, 1). Build() testSlice(t, colors2hex(grad.Colors(5)), []string{ "#ff0000", "#808000", "#00ff00", "#008080", "#0000ff", }) } func Test_SpreadRepeat(t *testing.T) { grad, _ := NewGradient(). HtmlColors("#000", "#fff"). Build() test(t, grad.RepeatAt(-2.0).HexString(), "#000000") test(t, grad.RepeatAt(-1.9).HexString(), "#1a1a1a") test(t, grad.RepeatAt(-1.5).HexString(), "#808080") test(t, grad.RepeatAt(-1.1).HexString(), "#e5e5e5") test(t, grad.RepeatAt(-1.0).HexString(), "#000000") test(t, grad.RepeatAt(-0.9).HexString(), "#191919") test(t, grad.RepeatAt(-0.5).HexString(), "#808080") test(t, grad.RepeatAt(-0.1).HexString(), "#e6e6e6") test(t, grad.RepeatAt(0.0).HexString(), "#000000") test(t, grad.RepeatAt(0.1).HexString(), "#1a1a1a") test(t, grad.RepeatAt(0.5).HexString(), "#808080") test(t, grad.RepeatAt(0.9).HexString(), "#e5e5e5") test(t, grad.RepeatAt(1.0).HexString(), "#000000") test(t, grad.RepeatAt(1.1).HexString(), "#1a1a1a") test(t, grad.RepeatAt(1.5).HexString(), "#808080") test(t, grad.RepeatAt(1.9).HexString(), "#e5e5e5") test(t, grad.RepeatAt(2.0).HexString(), "#000000") test(t, grad.RepeatAt(2.1).HexString(), "#1a1a1a") test(t, grad.RepeatAt(2.5).HexString(), "#808080") test(t, grad.RepeatAt(2.9).HexString(), "#e5e5e5") } func Test_SpreadReflect(t *testing.T) { grad, _ := NewGradient(). HtmlColors("#000", "#fff"). Build() test(t, grad.ReflectAt(-2.0).HexString(), "#000000") test(t, grad.ReflectAt(-1.9).HexString(), "#1a1a1a") test(t, grad.ReflectAt(-1.5).HexString(), "#808080") test(t, grad.ReflectAt(-1.1).HexString(), "#e5e5e5") test(t, grad.ReflectAt(-1.0).HexString(), "#ffffff") test(t, grad.ReflectAt(-0.9).HexString(), "#e5e5e5") test(t, grad.ReflectAt(-0.5).HexString(), "#808080") test(t, grad.ReflectAt(-0.1).HexString(), "#1a1a1a") test(t, grad.ReflectAt(0.0).HexString(), "#000000") test(t, grad.ReflectAt(0.1).HexString(), "#1a1a1a") test(t, grad.ReflectAt(0.5).HexString(), "#808080") test(t, grad.ReflectAt(0.9).HexString(), "#e5e5e5") test(t, grad.ReflectAt(1.0).HexString(), "#ffffff") test(t, grad.ReflectAt(1.1).HexString(), "#e5e5e5") test(t, grad.ReflectAt(1.5).HexString(), "#808080") test(t, grad.ReflectAt(1.9).HexString(), "#1a1a1a") test(t, grad.ReflectAt(2.0).HexString(), "#000000") test(t, grad.ReflectAt(2.1).HexString(), "#1a1a1a") test(t, grad.ReflectAt(2.5).HexString(), "#808080") test(t, grad.ReflectAt(2.9).HexString(), "#e5e5e5") } ================================================ FILE: linear.go ================================================ package colorgrad import ( "math" ) type linearGradient struct { colors [][4]float64 positions []float64 min float64 max float64 mode BlendMode first Color last Color } func (lg linearGradient) At(t float64) Color { if t <= lg.min { return lg.first } if t >= lg.max { return lg.last } if math.IsNaN(t) { return Color{A: 1} } low := 0 high := len(lg.positions) for low < high { mid := (low + high) / 2 if lg.positions[mid] < t { low = mid + 1 } else { high = mid } } if low == 0 { low = 1 } p1 := lg.positions[low-1] p2 := lg.positions[low] t = (t - p1) / (p2 - p1) a, b, c, d := linearInterpolate(lg.colors[low-1], lg.colors[low], t) switch lg.mode { case BlendRgb: return Color{R: a, G: b, B: c, A: d} case BlendLinearRgb: return LinearRgb(a, b, c, d) case BlendLab: return Lab(a, b, c, d).Clamp() case BlendOklab: return Oklab(a, b, c, d).Clamp() } return Color{} } func newLinearGradient(colors []Color, positions []float64, mode BlendMode) Gradient { gradbase := linearGradient{ colors: convertColors(colors, mode), positions: positions, min: positions[0], max: positions[len(positions)-1], mode: mode, first: colors[0], last: colors[len(colors)-1], } return Gradient{ Core: gradbase, Min: positions[0], Max: positions[len(positions)-1], } } ================================================ FILE: linear_test.go ================================================ package colorgrad import ( "math" "testing" ) func Test_LinearGradient(t *testing.T) { grad, err := NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Mode(BlendRgb). Interpolation(InterpolationLinear). Build() test(t, err, nil) test(t, grad.At(0.00).HexString(), "#ff0000") test(t, grad.At(0.25).HexString(), "#808000") test(t, grad.At(0.50).HexString(), "#00ff00") test(t, grad.At(0.75).HexString(), "#008080") test(t, grad.At(1.00).HexString(), "#0000ff") testSlice(t, colors2hex(grad.Colors(5)), []string{ "#ff0000", "#808000", "#00ff00", "#008080", "#0000ff", }) test(t, grad.At(-0.1).HexString(), "#ff0000") test(t, grad.At(1.11).HexString(), "#0000ff") test(t, grad.At(math.NaN()).HexString(), "#000000") } ================================================ FILE: preset.go ================================================ package colorgrad import ( "math" ) // Reference: https://github.com/d3/d3-scale-chromatic const deg2rad = math.Pi / 180 const pi1_3 = math.Pi / 3 const pi2_3 = math.Pi * 2 / 3 // Sinebow type sinebowGradient struct{} func Sinebow() Gradient { return Gradient{ Core: sinebowGradient{}, Min: 0, Max: 1, } } func (sg sinebowGradient) At(t float64) Color { t = (0.5 - t) * math.Pi return Color{ R: math.Pow(math.Sin(t), 2), G: math.Pow(math.Sin(t+pi1_3), 2), B: math.Pow(math.Sin(t+pi2_3), 2), A: 1, } } // Turbo type turboGradient struct{} func Turbo() Gradient { return Gradient{ Core: turboGradient{}, Min: 0, Max: 1, } } func (tg turboGradient) At(t float64) Color { t = math.Max(0, math.Min(1, t)) r := math.Round(34.61 + t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-t*14825.05))))) g := math.Round(23.31 + t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+t*707.56))))) b := math.Round(27.2 + t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-t*6838.66))))) return Color{ R: clamp01(r / 255), G: clamp01(g / 255), B: clamp01(b / 255), A: 1, } } // Cividis type cividisGradient struct{} func Cividis() Gradient { return Gradient{ Core: cividisGradient{}, Min: 0, Max: 1, } } func (cg cividisGradient) At(t float64) Color { t = math.Max(0, math.Min(1, t)) r := math.Round(-4.54 - t*(35.34-t*(2381.73-t*(6402.7-t*(7024.72-t*2710.57))))) g := math.Round(32.49 + t*(170.73+t*(52.82-t*(131.46-t*(176.58-t*67.37))))) b := math.Round(81.24 + t*(442.36-t*(2482.43-t*(6167.24-t*(6614.94-t*2475.67))))) return Color{ R: clamp01(r / 255), G: clamp01(g / 255), B: clamp01(b / 255), A: 1, } } // Cubehelix type cubehelix struct { h, s, l float64 } func (c cubehelix) toColor() Color { h := (c.h + 120) * deg2rad l := c.l a := c.s * l * (1 - l) cosh := math.Cos(h) sinh := math.Sin(h) r := (l - a*math.Min(0.14861*cosh-1.78277*sinh, 1.0)) g := (l - a*math.Min(0.29227*cosh+0.90649*sinh, 1.0)) b := l + a*(1.97294*cosh) return Color{ R: clamp01(r), G: clamp01(g), B: clamp01(b), A: 1, } } func (c cubehelix) interpolate(c2 cubehelix, t float64) cubehelix { return cubehelix{ h: c.h + t*(c2.h-c.h), s: c.s + t*(c2.s-c.s), l: c.l + t*(c2.l-c.l), } } // Cubehelix gradient type cubehelixGradient struct { start, end cubehelix } func CubehelixDefault() Gradient { gradbase := cubehelixGradient{ start: cubehelix{300, 0.5, 0.0}, end: cubehelix{-240, 0.5, 1.0}, } return Gradient{ Core: gradbase, Min: 0, Max: 1, } } func Warm() Gradient { gradbase := cubehelixGradient{ start: cubehelix{-100, 0.75, 0.35}, end: cubehelix{80, 1.50, 0.8}, } return Gradient{ Core: gradbase, Min: 0, Max: 1, } } func Cool() Gradient { gradbase := cubehelixGradient{ start: cubehelix{260, 0.75, 0.35}, end: cubehelix{80, 1.50, 0.8}, } return Gradient{ Core: gradbase, Min: 0, Max: 1, } } func (cg cubehelixGradient) At(t float64) Color { return cg.start.interpolate(cg.end, clamp01(t)).toColor() } // Rainbow type rainbowGradient struct{} func Rainbow() Gradient { return Gradient{ Core: rainbowGradient{}, Min: 0, Max: 1, } } func (rg rainbowGradient) At(t float64) Color { t = math.Max(0, math.Min(1, t)) ts := math.Abs(t - 0.5) return cubehelix{ h: 360*t - 100, s: 1.5 - 1.5*ts, l: 0.8 - 0.9*ts, }.toColor() } // --- Presets from color ramps func u32ToColor(v uint32) Color { r := uint8(v >> 16) g := uint8(v >> 8) b := uint8(v) return Rgb8(r, g, b, 255) } func preset(data []uint32) Gradient { colors := make([]Color, len(data)) for i, v := range data { colors[i] = u32ToColor(v) } pos := linspace(0, 1, uint(len(colors))) return newBasisGradient(colors, pos, BlendRgb) } // Diverging func BrBG() Gradient { colors := []uint32{0x543005, 0x8c510a, 0xbf812d, 0xdfc27d, 0xf6e8c3, 0xf5f5f5, 0xc7eae5, 0x80cdc1, 0x35978f, 0x01665e, 0x003c30} return preset(colors) } func PRGn() Gradient { colors := []uint32{0x40004b, 0x762a83, 0x9970ab, 0xc2a5cf, 0xe7d4e8, 0xf7f7f7, 0xd9f0d3, 0xa6dba0, 0x5aae61, 0x1b7837, 0x00441b} return preset(colors) } func PiYG() Gradient { colors := []uint32{0x8e0152, 0xc51b7d, 0xde77ae, 0xf1b6da, 0xfde0ef, 0xf7f7f7, 0xe6f5d0, 0xb8e186, 0x7fbc41, 0x4d9221, 0x276419} return preset(colors) } func PuOr() Gradient { colors := []uint32{0x2d004b, 0x542788, 0x8073ac, 0xb2abd2, 0xd8daeb, 0xf7f7f7, 0xfee0b6, 0xfdb863, 0xe08214, 0xb35806, 0x7f3b08} return preset(colors) } func RdBu() Gradient { colors := []uint32{0x67001f, 0xb2182b, 0xd6604d, 0xf4a582, 0xfddbc7, 0xf7f7f7, 0xd1e5f0, 0x92c5de, 0x4393c3, 0x2166ac, 0x053061} return preset(colors) } func RdGy() Gradient { colors := []uint32{0x67001f, 0xb2182b, 0xd6604d, 0xf4a582, 0xfddbc7, 0xffffff, 0xe0e0e0, 0xbababa, 0x878787, 0x4d4d4d, 0x1a1a1a} return preset(colors) } func RdYlBu() Gradient { colors := []uint32{0xa50026, 0xd73027, 0xf46d43, 0xfdae61, 0xfee090, 0xffffbf, 0xe0f3f8, 0xabd9e9, 0x74add1, 0x4575b4, 0x313695} return preset(colors) } func RdYlGn() Gradient { colors := []uint32{0xa50026, 0xd73027, 0xf46d43, 0xfdae61, 0xfee08b, 0xffffbf, 0xd9ef8b, 0xa6d96a, 0x66bd63, 0x1a9850, 0x006837} return preset(colors) } func Spectral() Gradient { colors := []uint32{0x9e0142, 0xd53e4f, 0xf46d43, 0xfdae61, 0xfee08b, 0xffffbf, 0xe6f598, 0xabdda4, 0x66c2a5, 0x3288bd, 0x5e4fa2} return preset(colors) } // Sequential (Single Hue) func Blues() Gradient { colors := []uint32{0xf7fbff, 0xdeebf7, 0xc6dbef, 0x9ecae1, 0x6baed6, 0x4292c6, 0x2171b5, 0x08519c, 0x08306b} return preset(colors) } func Greens() Gradient { colors := []uint32{0xf7fcf5, 0xe5f5e0, 0xc7e9c0, 0xa1d99b, 0x74c476, 0x41ab5d, 0x238b45, 0x006d2c, 0x00441b} return preset(colors) } func Greys() Gradient { colors := []uint32{0xffffff, 0xf0f0f0, 0xd9d9d9, 0xbdbdbd, 0x969696, 0x737373, 0x525252, 0x252525, 0x000000} return preset(colors) } func Oranges() Gradient { colors := []uint32{0xfff5eb, 0xfee6ce, 0xfdd0a2, 0xfdae6b, 0xfd8d3c, 0xf16913, 0xd94801, 0xa63603, 0x7f2704} return preset(colors) } func Purples() Gradient { colors := []uint32{0xfcfbfd, 0xefedf5, 0xdadaeb, 0xbcbddc, 0x9e9ac8, 0x807dba, 0x6a51a3, 0x54278f, 0x3f007d} return preset(colors) } func Reds() Gradient { colors := []uint32{0xfff5f0, 0xfee0d2, 0xfcbba1, 0xfc9272, 0xfb6a4a, 0xef3b2c, 0xcb181d, 0xa50f15, 0x67000d} return preset(colors) } // Sequential (Multi-Hue) func Viridis() Gradient { colors := []uint32{0x440154, 0x482777, 0x3f4a8a, 0x31678e, 0x26838f, 0x1f9d8a, 0x6cce5a, 0xb6de2b, 0xfee825} return preset(colors) } func Inferno() Gradient { colors := []uint32{0x000004, 0x170b3a, 0x420a68, 0x6b176e, 0x932667, 0xbb3654, 0xdd513a, 0xf3771a, 0xfca50a, 0xf6d644, 0xfcffa4} return preset(colors) } func Magma() Gradient { colors := []uint32{0x000004, 0x140e37, 0x3b0f70, 0x641a80, 0x8c2981, 0xb63679, 0xde4968, 0xf66f5c, 0xfe9f6d, 0xfece91, 0xfcfdbf} return preset(colors) } func Plasma() Gradient { colors := []uint32{0x0d0887, 0x42039d, 0x6a00a8, 0x900da3, 0xb12a90, 0xcb4678, 0xe16462, 0xf1834b, 0xfca636, 0xfccd25, 0xf0f921} return preset(colors) } func BuGn() Gradient { colors := []uint32{0xf7fcfd, 0xe5f5f9, 0xccece6, 0x99d8c9, 0x66c2a4, 0x41ae76, 0x238b45, 0x006d2c, 0x00441b} return preset(colors) } func BuPu() Gradient { colors := []uint32{0xf7fcfd, 0xe0ecf4, 0xbfd3e6, 0x9ebcda, 0x8c96c6, 0x8c6bb1, 0x88419d, 0x810f7c, 0x4d004b} return preset(colors) } func GnBu() Gradient { colors := []uint32{0xf7fcf0, 0xe0f3db, 0xccebc5, 0xa8ddb5, 0x7bccc4, 0x4eb3d3, 0x2b8cbe, 0x0868ac, 0x084081} return preset(colors) } func OrRd() Gradient { colors := []uint32{0xfff7ec, 0xfee8c8, 0xfdd49e, 0xfdbb84, 0xfc8d59, 0xef6548, 0xd7301f, 0xb30000, 0x7f0000} return preset(colors) } func PuBuGn() Gradient { colors := []uint32{0xfff7fb, 0xece2f0, 0xd0d1e6, 0xa6bddb, 0x67a9cf, 0x3690c0, 0x02818a, 0x016c59, 0x014636} return preset(colors) } func PuBu() Gradient { colors := []uint32{0xfff7fb, 0xece7f2, 0xd0d1e6, 0xa6bddb, 0x74a9cf, 0x3690c0, 0x0570b0, 0x045a8d, 0x023858} return preset(colors) } func PuRd() Gradient { colors := []uint32{0xf7f4f9, 0xe7e1ef, 0xd4b9da, 0xc994c7, 0xdf65b0, 0xe7298a, 0xce1256, 0x980043, 0x67001f} return preset(colors) } func RdPu() Gradient { colors := []uint32{0xfff7f3, 0xfde0dd, 0xfcc5c0, 0xfa9fb5, 0xf768a1, 0xdd3497, 0xae017e, 0x7a0177, 0x49006a} return preset(colors) } func YlGnBu() Gradient { colors := []uint32{0xffffd9, 0xedf8b1, 0xc7e9b4, 0x7fcdbb, 0x41b6c4, 0x1d91c0, 0x225ea8, 0x253494, 0x081d58} return preset(colors) } func YlGn() Gradient { colors := []uint32{0xffffe5, 0xf7fcb9, 0xd9f0a3, 0xaddd8e, 0x78c679, 0x41ab5d, 0x238443, 0x006837, 0x004529} return preset(colors) } func YlOrBr() Gradient { colors := []uint32{0xffffe5, 0xfff7bc, 0xfee391, 0xfec44f, 0xfe9929, 0xec7014, 0xcc4c02, 0x993404, 0x662506} return preset(colors) } func YlOrRd() Gradient { colors := []uint32{0xffffcc, 0xffeda0, 0xfed976, 0xfeb24c, 0xfd8d3c, 0xfc4e2a, 0xe31a1c, 0xbd0026, 0x800026} return preset(colors) } ================================================ FILE: preset_test.go ================================================ package colorgrad import ( "testing" ) func Test_PresetGradients(t *testing.T) { var grad Gradient grad = CubehelixDefault() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#000000", "#19122c", "#1b354c", "#2c5c48", "#3f7533", "#7e7a36", "#bc7967", "#d486b0", "#cba8e6", "#c1d2f3", "#ddf0ef", "#ffffff", }) grad = Warm() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#6e40aa", "#923db3", "#b83cb0", "#da3fa3", "#f6478d", "#ff5572", "#ff6956", "#ff823e", "#f59f30", "#ddbd30", "#c4d93e", "#aff05b", }) grad = Cool() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#6e40aa", "#6252c5", "#5069d9", "#3c84e1", "#42a0dd", "#49bbcd", "#51d3b5", "#5ae597", "#65f17a", "#71f663", "#83f557", "#aff05b", }) grad = Rainbow() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#6e40aa", "#b83cb0", "#f6478d", "#ff6956", "#f59f30", "#c4d93e", "#83f557", "#65f17a", "#51d3b5", "#42a0dd", "#5069d9", "#6e40aa", }) grad = Cividis() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#002051", "#083069", "#24416e", "#44516d", "#5f626e", "#757372", "#898477", "#9d9778", "#b4aa73", "#d0be67", "#ecd354", "#fdea45", }) grad = Sinebow() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#ff4040", "#eb860e", "#b4c901", "#6df61b", "#2cfd56", "#05dc9e", "#059edc", "#2c56fd", "#6d1bf6", "#b401c9", "#eb0e86", "#ff4040", }) grad = Turbo() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#23171b", "#4a51d4", "#3491f8", "#25c9d5", "#3aef9a", "#71fe65", "#b8f140", "#f2cb2c", "#ff9220", "#ed5215", "#b41d07", "#900c00", }) grad = Viridis() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#440154", "#461c6c", "#43377f", "#3c4f89", "#33648d", "#2a798e", "#248d8d", "#31a480", "#5ec263", "#94d641", "#cae02c", "#fee825", }) grad = Plasma() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#0d0887", "#3c049a", "#6302a5", "#850ba3", "#a52097", "#bf3983", "#d5546e", "#e76f5a", "#f48d45", "#fbad33", "#f9d226", "#f0f921", }) grad = Magma() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#000004", "#150b33", "#341062", "#59177b", "#7e2380", "#a3307c", "#c83f71", "#e65864", "#f77d63", "#fda775", "#fed296", "#fcfdbf", }) grad = Inferno() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#000004", "#170834", "#3a0b5c", "#60146b", "#842169", "#a92f5c", "#ca4348", "#e45f2e", "#f58417", "#faae1b", "#f8d951", "#fcffa4", }) grad = BrBG() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#543005", "#86500d", "#b47a2b", "#d6af67", "#edd9a9", "#f4eedc", "#deefec", "#acded7", "#6bbdb2", "#2e8f86", "#07635a", "#003c30", }) grad = PRGn() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#40004b", "#6f2a7c", "#9362a3", "#b796c4", "#d9c2de", "#eee6ef", "#e8f2e5", "#c5e8c0", "#90cd8e", "#50a35b", "#1d7436", "#00441b", }) grad = PiYG() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#8e0152", "#bc217a", "#d964a5", "#eba3cd", "#f8d0e7", "#f9ecf2", "#eff5e3", "#d4edb4", "#a8d674", "#77b43f", "#4b8d23", "#276419", }) grad = PuOr() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#2d004b", "#51287f", "#7963a6", "#a49bc7", "#cac8e1", "#e8e8ef", "#f9ebd7", "#fdd197", "#f3a84e", "#d67b17", "#ad5708", "#7f3b08", }) grad = RdBu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#67001f", "#a61c2d", "#cf5349", "#ea9175", "#f9c6ad", "#f9e9df", "#e4edf2", "#b9d9e9", "#7cb6d6", "#418bbf", "#1f609f", "#053061", }) grad = RdGy() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#67001f", "#a61c2d", "#cf5349", "#ea9175", "#f9c6ad", "#fdede3", "#f0efee", "#d2d2d2", "#ababab", "#7c7c7c", "#494949", "#1a1a1a", }) grad = RdYlBu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#a50026", "#d02d2a", "#ed623e", "#fa9b5a", "#fecd7f", "#feefaa", "#f0f8d8", "#cce9ef", "#9ccce2", "#6ca2cb", "#476eb1", "#313695", }) grad = RdYlGn() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#a50026", "#d02d2a", "#ed623e", "#fa9b5a", "#fecd7c", "#fdefa5", "#ecf6a5", "#c6e780", "#94d16a", "#57b55e", "#1e924d", "#006837", }) grad = Spectral() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#9e0142", "#cd374b", "#ec6649", "#fa9b5a", "#fecd7c", "#fef0a5", "#f2f9ac", "#cfec9e", "#98d5a4", "#5eb5ab", "#4283b4", "#5e4fa2", }) grad = Blues() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#f7fbff", "#e5eff9", "#d3e4f3", "#bdd8ec", "#a0cae3", "#7eb8da", "#5da4d0", "#408ec4", "#2877b7", "#1460a7", "#0a488d", "#08306b", }) grad = Greens() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#f7fcf5", "#e9f7e5", "#d7efd1", "#bfe6b9", "#a4da9e", "#84cb84", "#61bb6d", "#41a75b", "#289149", "#117a38", "#026128", "#00441b", }) grad = Greys() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#ffffff", "#f4f4f4", "#e5e5e5", "#d3d3d3", "#bebebe", "#a4a4a4", "#898989", "#707070", "#575757", "#393939", "#1b1b1b", "#000000", }) grad = Oranges() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fff5eb", "#feead5", "#fedcb9", "#fdc997", "#fdb171", "#fc994d", "#f8802e", "#ed6614", "#db4f06", "#bd3e02", "#9c3203", "#7f2704", }) grad = Purples() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fcfbfd", "#f2f0f7", "#e5e4f0", "#d4d4e8", "#bfbfdd", "#a9a7cf", "#9390c3", "#7f77b7", "#6e59a7", "#5e3a98", "#4e1d8a", "#3f007d", }) grad = Reds() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fff5f0", "#fee5d9", "#fdcfbb", "#fcb399", "#fc9677", "#fb7859", "#f65940", "#e9392d", "#d12120", "#b61319", "#930b13", "#67000d", }) grad = BuGn() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#f7fcfd", "#e9f7f9", "#d9f1f0", "#c0e7e0", "#9edacb", "#79cab1", "#59bb93", "#3fa971", "#289250", "#117a38", "#026128", "#00441b", }) grad = BuPu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#f7fcfd", "#e6f0f6", "#d1e0ee", "#b9cfe4", "#a3bcda", "#93a3cd", "#8d86be", "#8b67af", "#88489f", "#83278a", "#700d6e", "#4d004b", }) grad = GnBu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#f7fcf0", "#e6f6e1", "#d7efd1", "#c4e8c3", "#aadeba", "#8bd2bf", "#6bc2c9", "#4caecd", "#3193c2", "#1978b4", "#0a5d9f", "#084081", }) grad = OrRd() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fff7ec", "#feecd1", "#fedfb5", "#fdcf9b", "#fdbb84", "#fc9e6a", "#f77f54", "#eb5f41", "#da3a27", "#c3170f", "#a40302", "#7f0000", }) grad = PuBuGn() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fff7fb", "#f1e8f3", "#dfdaeb", "#c7cde4", "#a7bfdc", "#7eb0d3", "#56a0c9", "#3190b6", "#108394", "#027570", "#016150", "#014636", }) grad = PuBu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fff7fb", "#f1ebf4", "#dfddec", "#c7cee4", "#a9bfdc", "#86b0d3", "#5da0c9", "#338cbe", "#1277b1", "#05649c", "#03507d", "#023858", }) grad = PuRd() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#f7f4f9", "#ebe5f1", "#decee5", "#d3b3d7", "#ce96c8", "#d775b8", "#e14fa1", "#e12c84", "#d01762", "#b0094c", "#8b0138", "#67001f", }) grad = RdPu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#fff7f3", "#fee6e3", "#fdd3d0", "#fcbdc0", "#faa0b5", "#f77ca9", "#ec559e", "#d62f93", "#b60f84", "#92027a", "#6d0173", "#49006a", }) grad = YlGnBu() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#ffffd9", "#f1f9bf", "#dbf1b4", "#b7e3b6", "#87d0bb", "#59bec0", "#35a8c2", "#238bbb", "#2168ad", "#23489c", "#1b2f81", "#081d58", }) grad = YlGn() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#ffffe5", "#f8fcc6", "#e9f6b0", "#d0ec9f", "#b0de90", "#8bce80", "#64bc6f", "#41a65b", "#288c49", "#11753d", "#025e33", "#004529", }) grad = YlOrBr() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#ffffe5", "#fff8c7", "#ffeda8", "#fedc83", "#fec559", "#fda938", "#f78a22", "#e76d13", "#d05407", "#b03f03", "#8b3005", "#662506", }) grad = YlOrRd() testSlice(t, colors2hex(grad.Colors(12)), []string{ "#ffffcc", "#fff2ac", "#ffe48d", "#fed06e", "#feb653", "#fd9942", "#fc7535", "#f74b29", "#e62621", "#cd0d22", "#ab0225", "#800026", }) // Cyclical gradients grad = Rainbow() test(t, grad.At(0).HexString(), grad.At(1).HexString()) grad = Sinebow() test(t, grad.At(0).HexString(), grad.At(1).HexString()) } ================================================ FILE: sharp.go ================================================ package colorgrad import ( "math" ) type sharpGradient struct { colors []Color positions []float64 last int min float64 max float64 } func (sg sharpGradient) At(t float64) Color { if t <= sg.min { return sg.colors[0] } if t >= sg.max { return sg.colors[sg.last] } if math.IsNaN(t) { return Color{A: 1} } low := 0 high := len(sg.positions) for low < high { mid := (low + high) / 2 if sg.positions[mid] < t { low = mid + 1 } else { high = mid } } if low == 0 { low = 1 } i := low - 1 p1 := sg.positions[i] p2 := sg.positions[low] if i%2 == 0 { return sg.colors[i] } t = (t - p1) / (p2 - p1) a := sg.colors[i] b := sg.colors[low] return blendRgb(a, b, t) } func newSharpGradient(colorsIn []Color, dmin, dmax float64, smoothness float64) Gradient { n := len(colorsIn) colors := make([]Color, n*2) i := 0 for _, c := range colorsIn { colors[i] = c i++ colors[i] = c i++ } t := clamp01(smoothness) * (dmax - dmin) / float64(n) / 4 p := linspace(dmin, dmax, uint(n+1)) positions := make([]float64, n*2) i = 0 j := 0 for x := 0; x < int(n); x++ { positions[i] = p[j] if i > 0 { positions[i] += t } i++ j++ positions[i] = p[j] if i < len(colors)-1 { positions[i] -= t } i++ } gradbase := sharpGradient{ colors: colors, positions: positions, last: int(n*2 - 1), min: dmin, max: dmax, } return Gradient{ Core: gradbase, Min: dmin, Max: dmax, } } ================================================ FILE: sharp_test.go ================================================ package colorgrad import ( "math" "testing" ) func Test_SharpGradient(t *testing.T) { var grad, gradBase Gradient gradBase, _ = NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Build() // Sharp(0) grad = gradBase.Sharp(0, 0) test(t, grad.At(0.0).HexString(), "#ff0000") test(t, grad.At(0.5).HexString(), "#ff0000") test(t, grad.At(1.0).HexString(), "#ff0000") // Sharp(1) grad = gradBase.Sharp(1, 0) test(t, grad.At(0.0).HexString(), "#ff0000") test(t, grad.At(0.5).HexString(), "#ff0000") test(t, grad.At(1.0).HexString(), "#ff0000") // Sharp(3) grad = gradBase.Sharp(3, 0) test(t, grad.At(0.0).HexString(), "#ff0000") test(t, grad.At(0.2).HexString(), "#ff0000") test(t, grad.At(0.4).HexString(), "#00ff00") test(t, grad.At(0.5).HexString(), "#00ff00") test(t, grad.At(0.6).HexString(), "#00ff00") test(t, grad.At(0.9).HexString(), "#0000ff") test(t, grad.At(1.0).HexString(), "#0000ff") test(t, grad.At(-0.1).HexString(), "#ff0000") test(t, grad.At(1.1).HexString(), "#0000ff") test(t, grad.At(math.NaN()).HexString(), "#000000") // Sharp(2) gradBase, _ = NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Domain(-1, 1). Build() grad = gradBase.Sharp(2, 0) test(t, grad.At(-1.0).HexString(), "#ff0000") test(t, grad.At(-0.5).HexString(), "#ff0000") test(t, grad.At(-0.1).HexString(), "#ff0000") test(t, grad.At(0.1).HexString(), "#0000ff") test(t, grad.At(0.5).HexString(), "#0000ff") test(t, grad.At(1.0).HexString(), "#0000ff") // Smoothness gradBase, _ = NewGradient(). HtmlColors("#f00", "#0f0", "#00f"). Build() grad = gradBase.Sharp(0, 0.5) test(t, grad.At(0.0).HexString(), "#ff0000") test(t, grad.At(0.5).HexString(), "#ff0000") test(t, grad.At(1.0).HexString(), "#ff0000") grad = gradBase.Sharp(1, 0.5) test(t, grad.At(0.0).HexString(), "#ff0000") test(t, grad.At(0.5).HexString(), "#ff0000") test(t, grad.At(1.0).HexString(), "#ff0000") grad = gradBase.Sharp(3, 0.1) test(t, grad.At(0).HexString(), "#ff0000") test(t, grad.At(0.1).HexString(), "#ff0000") test(t, grad.At(1.0/3).HexString(), "#808000") test(t, grad.At(0.45).HexString(), "#00ff00") test(t, grad.At(0.55).HexString(), "#00ff00") test(t, grad.At(1.0/3*2).HexString(), "#008080") test(t, grad.At(0.9).HexString(), "#0000ff") test(t, grad.At(1).HexString(), "#0000ff") test(t, grad.At(-0.01).HexString(), "#ff0000") test(t, grad.At(1.01).HexString(), "#0000ff") test(t, grad.At(math.NaN()).HexString(), "#000000") } ================================================ FILE: smoothstep.go ================================================ package colorgrad import ( "math" ) type smoothstepGradient struct { colors [][4]float64 positions []float64 min float64 max float64 mode BlendMode first Color last Color } func (sg smoothstepGradient) At(t float64) Color { if t <= sg.min { return sg.first } if t >= sg.max { return sg.last } if math.IsNaN(t) { return Color{A: 1} } low := 0 high := len(sg.positions) for low < high { mid := (low + high) / 2 if sg.positions[mid] < t { low = mid + 1 } else { high = mid } } if low == 0 { low = 1 } p1 := sg.positions[low-1] p2 := sg.positions[low] t = (t - p1) / (p2 - p1) a, b, c, d := smoothstepInterpolate(sg.colors[low-1], sg.colors[low], t) switch sg.mode { case BlendRgb: return Color{R: a, G: b, B: c, A: d} case BlendLinearRgb: return LinearRgb(a, b, c, d) case BlendLab: return Lab(a, b, c, d).Clamp() case BlendOklab: return Oklab(a, b, c, d).Clamp() } return Color{} } func newSmoothstepGradient(colors []Color, positions []float64, mode BlendMode) Gradient { gradbase := smoothstepGradient{ colors: convertColors(colors, mode), positions: positions, min: positions[0], max: positions[len(positions)-1], mode: mode, first: colors[0], last: colors[len(colors)-1], } return Gradient{ Core: gradbase, Min: positions[0], Max: positions[len(positions)-1], } } func smoothstepInterpolate(a, b [4]float64, t float64) (i, j, k, l float64) { i = (b[0]-a[0])*(3.0-t*2.0)*t*t + a[0] j = (b[1]-a[1])*(3.0-t*2.0)*t*t + a[1] k = (b[2]-a[2])*(3.0-t*2.0)*t*t + a[2] l = (b[3]-a[3])*(3.0-t*2.0)*t*t + a[3] return } ================================================ FILE: util.go ================================================ package colorgrad import ( "math" "strconv" "strings" ) func linspace(min, max float64, n uint) []float64 { if n == 1 { return []float64{min} } d := max - min l := float64(n) - 1 res := make([]float64, n) for i := range res { res[i] = (min + (float64(i)*d)/l) } return res } // Map t from range [a, b] to range [0, 1] func norm(t, a, b float64) float64 { return (t - a) * (1 / (b - a)) } func modulo(x, y float64) float64 { return math.Mod(math.Mod(x, y)+y, y) } func clamp01(t float64) float64 { return math.Max(0, math.Min(1, t)) } func parseFloat(s string) (float64, bool) { f, err := strconv.ParseFloat(strings.TrimSpace(s), 64) return f, err == nil } func toLinear(x float64) float64 { if x >= 0.04045 { return math.Pow((x+0.055)/1.055, 2.4) } return x / 12.92 } func col2linearRgb(col Color) [4]float64 { return [4]float64{ toLinear(col.R), toLinear(col.G), toLinear(col.B), col.A, } } func col2oklab(col Color) [4]float64 { arr := col2linearRgb(col) l := math.Cbrt(0.4121656120*arr[0] + 0.5362752080*arr[1] + 0.0514575653*arr[2]) m := math.Cbrt(0.2118591070*arr[0] + 0.6807189584*arr[1] + 0.1074065790*arr[2]) s := math.Cbrt(0.0883097947*arr[0] + 0.2818474174*arr[1] + 0.6302613616*arr[2]) return [4]float64{ 0.2104542553*l + 0.7936177850*m - 0.0040720468*s, 1.9779984951*l - 2.4285922050*m + 0.4505937099*s, 0.0259040371*l + 0.7827717662*m - 0.8086757660*s, col.A, } } func col2hsv(col Color) [4]float64 { v := math.Max(col.R, math.Max(col.G, col.B)) d := v - math.Min(col.R, math.Min(col.G, col.B)) if math.Abs(d) < epsilon { return [4]float64{0, 0, v, col.A} } s := d / v dr := (v - col.R) / d dg := (v - col.G) / d db := (v - col.B) / d var h float64 if math.Abs(col.R-v) < epsilon { h = db - dg } else if math.Abs(col.G-v) < epsilon { h = 2.0 + dr - db } else { h = 4.0 + dg - dr } h = math.Mod(h*60.0, 360.0) return [4]float64{normalizeAngle(h), s, v, col.A} } func normalizeAngle(t float64) float64 { t = math.Mod(t, 360.0) if t < 0.0 { t += 360.0 } return t } func convertColors(colorsIn []Color, mode BlendMode) [][4]float64 { colors := make([][4]float64, len(colorsIn)) for i, col := range colorsIn { switch mode { case BlendRgb: colors[i] = [4]float64{col.R, col.G, col.B, col.A} case BlendLinearRgb: colors[i] = col2linearRgb(col) case BlendLab: colors[i] = col2lab(col) case BlendOklab: colors[i] = col2oklab(col) } } return colors } func linearInterpolate(a, b [4]float64, t float64) (i, j, k, l float64) { i = a[0] + t*(b[0]-a[0]) j = a[1] + t*(b[1]-a[1]) k = a[2] + t*(b[2]-a[2]) l = a[3] + t*(b[3]-a[3]) return } func blendRgb(a, b Color, t float64) Color { return Color{ R: a.R + t*(b.R-a.R), G: a.G + t*(b.G-a.G), B: a.B + t*(b.B-a.B), A: a.A + t*(b.A-a.A), } } // --- Lab const ( d65X = 0.95047 d65Y = 1.0 d65Z = 1.08883 delta = 6.0 / 29.0 delta2 = delta * delta delta3 = delta2 * delta ) func linearRGBToXYZ(r, g, b float64) [3]float64 { // Inverse sRGB matrix (D65) x := 0.4124564*r + 0.3575761*g + 0.1804375*b y := 0.2126729*r + 0.7151522*g + 0.0721750*b z := 0.0193339*r + 0.1191920*g + 0.9503041*b return [3]float64{x, y, z} } func xyzToLab(x, y, z float64) [3]float64 { labF := func(t float64) float64 { if t > delta3 { return math.Cbrt(t) } return (t / (3.0 * delta2)) + (4.0 / 29.0) } fx := labF(x / d65X) fy := labF(y / d65Y) fz := labF(z / d65Z) l := 116.0*fy - 16.0 a := 500.0 * (fx - fy) b := 200.0 * (fy - fz) return [3]float64{l, a, b} } func col2lab(col Color) [4]float64 { c := col2linearRgb(col) x := linearRGBToXYZ(c[0], c[1], c[2]) l := xyzToLab(x[0], x[1], x[2]) return [4]float64{l[0], l[1], l[2], col.A} } ================================================ FILE: util_test.go ================================================ package colorgrad import ( "math" "testing" "github.com/mazznoer/csscolorparser" ) func Test_Utils(t *testing.T) { // linspace test(t, len(linspace(0, 1, 0)), 0) testTrue(t, linspace(0, 1, 1)[0] == 0.0) testSlice(t, linspace(0, 1, 2), []float64{0, 1}) testSlice(t, linspace(0, 1, 3), []float64{0, 0.5, 1}) testSlice(t, linspace(0, 100, 3), []float64{0, 50, 100}) // norm test(t, norm(0.99, 0, 1), 0.99) test(t, norm(12, 0, 100), 0.12) test(t, norm(753, 0, 1000), 0.753) // modulo test(t, modulo(2.73, 1), 0.73) test(t, modulo(32, 25), 7.0) // clamp01 test(t, clamp01(0), 0.0) test(t, clamp01(1), 1.0) test(t, clamp01(0.997), 0.997) test(t, clamp01(-0.51), 0.0) test(t, clamp01(1.0001), 1.0) // parseFloat validData := []struct { str string num float64 }{ {"0", 0}, {"0.0", 0}, {"1234", 1234}, {"0.00027", 0.00027}, {"-56.03", -56.03}, } for _, dt := range validData { f, ok := parseFloat(dt.str) testTrue(t, ok) test(t, f, dt.num) } invalidData := []string{ "", " ", "25.0x", "1.0d7", "x10", "o", } for _, s := range invalidData { _, ok := parseFloat(s) testTrue(t, !ok) } // convertColors colors := []Color{ Rgb(1, 0.7, 0.1, 0.5), //Rgb8(10, 255, 125, 0), // LinearRgb(0.1, 0.9, 1, 1), Hwb(0, 0, 0, 1), Hwb(320, 0.1, 0.3, 1), Hsv(120, 0.3, 0.2, 0.1), Hsl(120, 0.3, 0.2, 1), } for i, arr := range convertColors(colors, BlendRgb) { col := Rgb(spreadF64(arr)) test(t, colors[i].HexString(), col.HexString()) } for i, arr := range convertColors(colors, BlendLinearRgb) { col := LinearRgb(spreadF64(arr)) test(t, colors[i].HexString(), col.HexString()) } /*for i, arr := range convertColors(colors, BlendOklab) { col := Oklab(spreadF64(arr)) test(t, colors[i].HexString(), col.HexString()) }*/ for _, c := range colors { col, err := csscolorparser.Parse(c.HexString()) test(t, err, nil) x := Oklab(spreadF64(col2oklab(col))) test(t, x.HexString(), c.HexString()) y := LinearRgb(spreadF64(col2linearRgb(col))) test(t, y.HexString(), c.HexString()) } hexColors := []string{ "#000000", "#ffffff", "#999999", "#135cdf", "#ff0000", "#00ff7f", //"#0aff7d", // //"#09ff7d", // "#abc5679b", } for _, s := range hexColors { col, err := csscolorparser.Parse(s) test(t, err, nil) test(t, col.HexString(), s) x := Oklab(spreadF64(col2oklab(col))) test(t, x.HexString(), s) y := LinearRgb(spreadF64(col2linearRgb(col))) test(t, y.HexString(), s) } } func spreadF64(arr [4]float64) (a, b, c, d float64) { a = arr[0] b = arr[1] c = arr[2] d = arr[3] return } func Test_Lab(t *testing.T) { testData := []string{ "#000000", "#ffffff", "#ff0000", "#123abc", "#bad455", } for _, s := range testData { c, err := csscolorparser.Parse(s) test(t, err, nil) lab := col2lab(c) cc := Lab(lab[0], lab[1], lab[2], lab[3]) test(t, s, cc.HexString()) } } // --- Helper functions func test(t *testing.T, a, b any) { if a != b { t.Helper() t.Errorf("left: %v, right: %v", a, b) } } func testTrue(t *testing.T, b bool) { if !b { t.Helper() t.Errorf("it false") } } func testSlice[S ~[]E, E comparable](t *testing.T, a, b S) { if len(a) != len(b) { t.Helper() t.Errorf("different length -> left: %v, right: %v", len(a), len(b)) return } for i, val := range a { if val != b[i] { t.Helper() t.Errorf("diff at index: %v, left: %v, right: %v", i, val, b[i]) return } } } func testSliceF(t *testing.T, a, b []float64) { if len(a) != len(b) { t.Helper() t.Errorf("different length -> left: %v, right: %v", len(a), len(b)) return } epsilon := math.Nextafter(1.0, 2.0) - 1.0 for i, val := range a { if math.Abs(val-b[i]) > epsilon { t.Helper() t.Errorf("diff at index: %v, left: %v, right: %v", i, val, b[i]) return } } } func colors2hex(colors []Color) []string { hexColors := make([]string, len(colors)) for i, c := range colors { hexColors[i] = c.HexString() } return hexColors } func isZeroGradient(grad Gradient) bool { for _, col := range grad.Colors(13) { if col.HexString() != "#00000000" { return false } } return true }