Full Code of mazznoer/colorgrad for AI

master 146c700a7e77 cached
37 files
95.6 KB
37.5k tokens
200 symbols
1 requests
Download .txt
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
}
Download .txt
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
Download .txt
SYMBOL INDEX (200 symbols across 24 files)

FILE: basis.go
  type basisGradient (line 9) | type basisGradient struct
    method At (line 19) | func (lg basisGradient) At(t float64) Color {
  function newBasisGradient (line 89) | func newBasisGradient(colors []Color, positions []float64, mode BlendMod...
  function basis (line 107) | func basis(t1, v0, v1, v2, v3 float64) float64 {

FILE: basis_test.go
  function Test_BasisGradient (line 8) | func Test_BasisGradient(t *testing.T) {

FILE: bench_test.go
  function BenchmarkLinearGradient (line 26) | func BenchmarkLinearGradient(b *testing.B) {
  function BenchmarkCatmullRomGradient (line 49) | func BenchmarkCatmullRomGradient(b *testing.B) {
  function BenchmarkBasisGradient (line 72) | func BenchmarkBasisGradient(b *testing.B) {

FILE: builder.go
  type GradientBuilder (line 9) | type GradientBuilder struct
    method Colors (line 28) | func (gb *GradientBuilder) Colors(colors ...Color) *GradientBuilder {
    method HtmlColors (line 36) | func (gb *GradientBuilder) HtmlColors(htmlColors ...string) *GradientB...
    method Css (line 49) | func (gb *GradientBuilder) Css(s string) *GradientBuilder {
    method Domain (line 65) | func (gb *GradientBuilder) Domain(positions ...float64) *GradientBuild...
    method Mode (line 71) | func (gb *GradientBuilder) Mode(mode BlendMode) *GradientBuilder {
    method Interpolation (line 76) | func (gb *GradientBuilder) Interpolation(mode Interpolation) *Gradient...
    method Reset (line 81) | func (gb *GradientBuilder) Reset() *GradientBuilder {
    method prepareBuild (line 92) | func (gb *GradientBuilder) prepareBuild() error {
    method Build (line 171) | func (gb *GradientBuilder) Build() (Gradient, error) {
    method GetColors (line 196) | func (gb *GradientBuilder) GetColors() *[]Color {
    method GetPositions (line 201) | func (gb *GradientBuilder) GetPositions() *[]float64 {
  function NewGradient (line 19) | func NewGradient() *GradientBuilder {

FILE: builder_test.go
  function domain (line 8) | func domain(min, max float64) [2]float64 {
  function Test_Builder (line 12) | func Test_Builder(t *testing.T) {
  function Test_CssGradient (line 164) | func Test_CssGradient(t *testing.T) {

FILE: catmull_rom.go
  function toCatmullRomSegments (line 9) | func toCatmullRomSegments(values []float64) [][4]float64 {
  type catmullRomGradient (line 49) | type catmullRomGradient struct
    method At (line 113) | func (g catmullRomGradient) At(t float64) Color {
  function newCatmullRomGradient (line 59) | func newCatmullRomGradient(colors []Color, positions []float64, space Bl...

FILE: catmull_rom_test.go
  function Test_CatmullRomGradient (line 8) | func Test_CatmullRomGradient(t *testing.T) {

FILE: css_gradient.go
  function parseCss (line 10) | func parseCss(s string) ([]cssGradientStop, bool) {
  function ptr (line 81) | func ptr(f float64) *float64 {
  function ptrColor (line 85) | func ptrColor(c Color) *Color {
  type cssGradientStop (line 89) | type cssGradientStop struct
  function prosesStop (line 94) | func prosesStop(stops *[]cssGradientStop, arr []string) bool {
  function splitByComma (line 145) | func splitByComma(s string) []string {
  function splitBySpace (line 163) | func splitBySpace(s string) []string {
  function parsePos (line 186) | func parsePos(s string) (float64, bool) {

FILE: example_test.go
  function Example_presetGradient (line 9) | func Example_presetGradient() {
  function Example_customGradient (line 20) | func Example_customGradient() {

FILE: examples/basic.go
  function main (line 14) | func main() {

FILE: examples/gradients.go
  type data (line 20) | type data struct
  type Opt (line 25) | type Opt struct
  function main (line 30) | func main() {
  function parseGgr (line 283) | func parseGgr(filepath string) colorgrad.Gradient {
  function gradientImage (line 298) | func gradientImage(gradient colorgrad.Gradient, width, height int) image...
  function rgbPlot (line 311) | func rgbPlot(gradient colorgrad.Gradient, width, height int) image.Image {
  function gradRgbPlot (line 334) | func gradRgbPlot(gradient colorgrad.Gradient, width, height, padding int...
  function remap (line 357) | func remap(t, a, b, c, d float64) float64 {
  function savePNG (line 361) | func savePNG(img image.Image, filepath string) {

FILE: gimp.go
  constant epsilon (line 16) | epsilon = 1e-10
  constant fracPi2 (line 17) | fracPi2 = math.Pi / 2
  type blendingType (line 19) | type blendingType
  constant linear (line 22) | linear blendingType = iota
  constant curved (line 23) | curved
  constant sinusoidal (line 24) | sinusoidal
  constant sphericalIncreasing (line 25) | sphericalIncreasing
  constant sphericalDecreasing (line 26) | sphericalDecreasing
  constant step (line 27) | step
  type coloringType (line 30) | type coloringType
  constant rgb (line 33) | rgb coloringType = iota
  constant hsvCcw (line 34) | hsvCcw
  constant hsvCw (line 35) | hsvCw
  type gimpSegment (line 38) | type gimpSegment struct
  type gimpGradient (line 55) | type gimpGradient struct
    method At (line 61) | func (ggr gimpGradient) At(t float64) Color {
  function calc_linear_factor (line 145) | func calc_linear_factor(middle, pos float64) float64 {
  function blendHsvCcw (line 164) | func blendHsvCcw(c1, c2 Color, t float64) Color {
  function blendHsvCw (line 190) | func blendHsvCw(c1, c2 Color, t float64) Color {
  function ParseGgr (line 216) | func ParseGgr(r io.Reader, fg, bg Color) (Gradient, string, error) {
  function parseSegment (line 291) | func parseSegment(s string, fg, bg Color) (gimpSegment, bool) {

FILE: gimp_test.go
  function Test_GIMPGradient (line 9) | func Test_GIMPGradient(t *testing.T) {

FILE: gradient.go
  type BlendMode (line 10) | type BlendMode
    method String (line 19) | func (b BlendMode) String() string {
  constant BlendRgb (line 13) | BlendRgb BlendMode = iota
  constant BlendLinearRgb (line 14) | BlendLinearRgb
  constant BlendLab (line 15) | BlendLab
  constant BlendOklab (line 16) | BlendOklab
  type Interpolation (line 33) | type Interpolation
    method String (line 42) | func (i Interpolation) String() string {
  constant InterpolationLinear (line 36) | InterpolationLinear Interpolation = iota
  constant InterpolationSmoothstep (line 37) | InterpolationSmoothstep
  constant InterpolationCatmullRom (line 38) | InterpolationCatmullRom
  constant InterpolationBasis (line 39) | InterpolationBasis
  function Rgb (line 67) | func Rgb(r, g, b, a float64) Color {
  function Rgb8 (line 71) | func Rgb8(r, g, b, a uint8) Color {
  function GoColor (line 75) | func GoColor(col color.Color) Color {
  type GradientCore (line 89) | type GradientCore interface
  type Gradient (line 94) | type Gradient struct
    method At (line 101) | func (g Gradient) At(t float64) Color {
    method RepeatAt (line 106) | func (g Gradient) RepeatAt(t float64) Color {
    method ReflectAt (line 112) | func (g Gradient) ReflectAt(t float64) Color {
    method Colors (line 118) | func (g Gradient) Colors(count uint) []Color {
    method Domain (line 129) | func (g Gradient) Domain() (float64, float64) {
    method Sharp (line 134) | func (g Gradient) Sharp(segment uint, smoothness float64) Gradient {
  type zeroGradient (line 145) | type zeroGradient struct
    method At (line 148) | func (zg zeroGradient) At(t float64) Color {

FILE: gradient_test.go
  function Test_Basic (line 9) | func Test_Basic(t *testing.T) {
  function Test_GetColors (line 35) | func Test_GetColors(t *testing.T) {
  function Test_SpreadRepeat (line 63) | func Test_SpreadRepeat(t *testing.T) {
  function Test_SpreadReflect (line 94) | func Test_SpreadReflect(t *testing.T) {

FILE: linear.go
  type linearGradient (line 7) | type linearGradient struct
    method At (line 17) | func (lg linearGradient) At(t float64) Color {
  function newLinearGradient (line 65) | func newLinearGradient(colors []Color, positions []float64, mode BlendMo...

FILE: linear_test.go
  function Test_LinearGradient (line 8) | func Test_LinearGradient(t *testing.T) {

FILE: preset.go
  constant deg2rad (line 9) | deg2rad = math.Pi / 180
  constant pi1_3 (line 10) | pi1_3 = math.Pi / 3
  constant pi2_3 (line 11) | pi2_3 = math.Pi * 2 / 3
  type sinebowGradient (line 15) | type sinebowGradient struct
    method At (line 25) | func (sg sinebowGradient) At(t float64) Color {
  function Sinebow (line 17) | func Sinebow() Gradient {
  type turboGradient (line 37) | type turboGradient struct
    method At (line 47) | func (tg turboGradient) At(t float64) Color {
  function Turbo (line 39) | func Turbo() Gradient {
  type cividisGradient (line 62) | type cividisGradient struct
    method At (line 72) | func (cg cividisGradient) At(t float64) Color {
  function Cividis (line 64) | func Cividis() Gradient {
  type cubehelix (line 87) | type cubehelix struct
    method toColor (line 91) | func (c cubehelix) toColor() Color {
    method interpolate (line 108) | func (c cubehelix) interpolate(c2 cubehelix, t float64) cubehelix {
  type cubehelixGradient (line 118) | type cubehelixGradient struct
    method At (line 158) | func (cg cubehelixGradient) At(t float64) Color {
  function CubehelixDefault (line 122) | func CubehelixDefault() Gradient {
  function Warm (line 134) | func Warm() Gradient {
  function Cool (line 146) | func Cool() Gradient {
  type rainbowGradient (line 164) | type rainbowGradient struct
    method At (line 174) | func (rg rainbowGradient) At(t float64) Color {
  function Rainbow (line 166) | func Rainbow() Gradient {
  function u32ToColor (line 186) | func u32ToColor(v uint32) Color {
  function preset (line 193) | func preset(data []uint32) Gradient {
  function BrBG (line 204) | func BrBG() Gradient {
  function PRGn (line 209) | func PRGn() Gradient {
  function PiYG (line 214) | func PiYG() Gradient {
  function PuOr (line 219) | func PuOr() Gradient {
  function RdBu (line 224) | func RdBu() Gradient {
  function RdGy (line 229) | func RdGy() Gradient {
  function RdYlBu (line 234) | func RdYlBu() Gradient {
  function RdYlGn (line 239) | func RdYlGn() Gradient {
  function Spectral (line 244) | func Spectral() Gradient {
  function Blues (line 251) | func Blues() Gradient {
  function Greens (line 256) | func Greens() Gradient {
  function Greys (line 261) | func Greys() Gradient {
  function Oranges (line 266) | func Oranges() Gradient {
  function Purples (line 271) | func Purples() Gradient {
  function Reds (line 276) | func Reds() Gradient {
  function Viridis (line 283) | func Viridis() Gradient {
  function Inferno (line 288) | func Inferno() Gradient {
  function Magma (line 293) | func Magma() Gradient {
  function Plasma (line 298) | func Plasma() Gradient {
  function BuGn (line 303) | func BuGn() Gradient {
  function BuPu (line 308) | func BuPu() Gradient {
  function GnBu (line 313) | func GnBu() Gradient {
  function OrRd (line 318) | func OrRd() Gradient {
  function PuBuGn (line 323) | func PuBuGn() Gradient {
  function PuBu (line 328) | func PuBu() Gradient {
  function PuRd (line 333) | func PuRd() Gradient {
  function RdPu (line 338) | func RdPu() Gradient {
  function YlGnBu (line 343) | func YlGnBu() Gradient {
  function YlGn (line 348) | func YlGn() Gradient {
  function YlOrBr (line 353) | func YlOrBr() Gradient {
  function YlOrRd (line 358) | func YlOrRd() Gradient {

FILE: preset_test.go
  function Test_PresetGradients (line 7) | func Test_PresetGradients(t *testing.T) {

FILE: sharp.go
  type sharpGradient (line 7) | type sharpGradient struct
    method At (line 15) | func (sg sharpGradient) At(t float64) Color {
  function newSharpGradient (line 58) | func newSharpGradient(colorsIn []Color, dmin, dmax float64, smoothness f...

FILE: sharp_test.go
  function Test_SharpGradient (line 8) | func Test_SharpGradient(t *testing.T) {

FILE: smoothstep.go
  type smoothstepGradient (line 7) | type smoothstepGradient struct
    method At (line 17) | func (sg smoothstepGradient) At(t float64) Color {
  function newSmoothstepGradient (line 65) | func newSmoothstepGradient(colors []Color, positions []float64, mode Ble...
  function smoothstepInterpolate (line 83) | func smoothstepInterpolate(a, b [4]float64, t float64) (i, j, k, l float...

FILE: util.go
  function linspace (line 9) | func linspace(min, max float64, n uint) []float64 {
  function norm (line 23) | func norm(t, a, b float64) float64 {
  function modulo (line 27) | func modulo(x, y float64) float64 {
  function clamp01 (line 31) | func clamp01(t float64) float64 {
  function parseFloat (line 35) | func parseFloat(s string) (float64, bool) {
  function toLinear (line 40) | func toLinear(x float64) float64 {
  function col2linearRgb (line 47) | func col2linearRgb(col Color) [4]float64 {
  function col2oklab (line 56) | func col2oklab(col Color) [4]float64 {
  function col2hsv (line 69) | func col2hsv(col Color) [4]float64 {
  function normalizeAngle (line 96) | func normalizeAngle(t float64) float64 {
  function convertColors (line 104) | func convertColors(colorsIn []Color, mode BlendMode) [][4]float64 {
  function linearInterpolate (line 121) | func linearInterpolate(a, b [4]float64, t float64) (i, j, k, l float64) {
  function blendRgb (line 129) | func blendRgb(a, b Color, t float64) Color {
  constant d65X (line 141) | d65X = 0.95047
  constant d65Y (line 142) | d65Y = 1.0
  constant d65Z (line 143) | d65Z = 1.08883
  constant delta (line 145) | delta  = 6.0 / 29.0
  constant delta2 (line 146) | delta2 = delta * delta
  constant delta3 (line 147) | delta3 = delta2 * delta
  function linearRGBToXYZ (line 150) | func linearRGBToXYZ(r, g, b float64) [3]float64 {
  function xyzToLab (line 158) | func xyzToLab(x, y, z float64) [3]float64 {
  function col2lab (line 177) | func col2lab(col Color) [4]float64 {

FILE: util_test.go
  function Test_Utils (line 10) | func Test_Utils(t *testing.T) {
  function spreadF64 (line 131) | func spreadF64(arr [4]float64) (a, b, c, d float64) {
  function Test_Lab (line 139) | func Test_Lab(t *testing.T) {
  function test (line 158) | func test(t *testing.T, a, b any) {
  function testTrue (line 165) | func testTrue(t *testing.T, b bool) {
  function testSlice (line 172) | func testSlice[S ~[]E, E comparable](t *testing.T, a, b S) {
  function testSliceF (line 187) | func testSliceF(t *testing.T, a, b []float64) {
  function colors2hex (line 203) | func colors2hex(colors []Color) []string {
  function isZeroGradient (line 211) | func isZeroGradient(grad Gradient) bool {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (110K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 120,
    "preview": "# These are supported funding model platforms\n\nko_fi: mazznoer\nliberapay: mazznoer\ncustom: \"https://paypal.me/mazznoer\"\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "chars": 860,
    "preview": "name: CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    strategy:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 310,
    "preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 458,
    "preview": "# Changelog\n\n## v0.11.1\n\n- Fix bug in Lab conversion\n\n## v0.11.0\n\n- New `GradientBuilder.Reset()`\n- New `BlendLab`\n- `Gr"
  },
  {
    "path": "LICENSE",
    "chars": 1771,
    "preview": "MIT License\n\nCopyright (c) 2020 Nor Khasyatillah\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "Makefile",
    "chars": 230,
    "preview": "SHELL := /bin/bash\n\n.PHONY: all check test\n\nall: check test\n\ncheck:\n\tgo build && go vet && gofmt -s -l .\n\ntest:\n\tgo test"
  },
  {
    "path": "PRESET.md",
    "chars": 2353,
    "preview": "# Preset Gradients\n\nAll preset gradients are in the domain [0..1].\n\n## Diverging\n\n`colorgrad.BrBG()`\n![img](doc/images/p"
  },
  {
    "path": "README.md",
    "chars": 7982,
    "preview": "# colorgrad\n\n[![Release](https://img.shields.io/github/release/mazznoer/colorgrad.svg)](https://github.com/mazznoer/colo"
  },
  {
    "path": "basis.go",
    "chars": 1972,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n)\n\n// https://github.com/d3/d3-interpolate/blob/master/src/basis.js\n\ntype basisGradi"
  },
  {
    "path": "basis_test.go",
    "chars": 749,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc Test_BasisGradient(t *testing.T) {\n\tgrad, err := NewGradient().\n\t"
  },
  {
    "path": "bench_test.go",
    "chars": 2599,
    "preview": "package colorgrad\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nvar colors = []string{\n\t\"#87e575\", \"#e88ef2\", \"#7398ef\", \"#65c3f2\", \"#3"
  },
  {
    "path": "builder.go",
    "chars": 4574,
    "preview": "package colorgrad\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/mazznoer/csscolorparser\"\n)\n\ntype GradientBuilder struct {\n\tcolors      "
  },
  {
    "path": "builder_test.go",
    "chars": 5623,
    "preview": "package colorgrad\n\nimport (\n\t\"image/color\"\n\t\"testing\"\n)\n\nfunc domain(min, max float64) [2]float64 {\n\treturn [2]float64{m"
  },
  {
    "path": "catmull_rom.go",
    "chars": 3477,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n)\n\n// Adapted from https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom"
  },
  {
    "path": "catmull_rom_test.go",
    "chars": 759,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc Test_CatmullRomGradient(t *testing.T) {\n\tgrad, err := NewGradient"
  },
  {
    "path": "css_gradient.go",
    "chars": 3464,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/mazznoer/csscolorparser\"\n)\n\nfunc parseCss(s string) ([]cssG"
  },
  {
    "path": "example_test.go",
    "chars": 585,
    "preview": "package colorgrad_test\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/mazznoer/colorgrad\"\n)\n\nfunc Example_presetGradient() {\n\tgrad := co"
  },
  {
    "path": "examples/.gitignore",
    "chars": 35,
    "preview": "basic\ngradients\n*.png\noutput/*.png\n"
  },
  {
    "path": "examples/basic.go",
    "chars": 562,
    "preview": "//go:build ignore\n// +build ignore\n\npackage main\n\nimport (\n\t\"image\"\n\t\"image/png\"\n\t\"os\"\n\n\t\"github.com/mazznoer/colorgrad\""
  },
  {
    "path": "examples/ggr/Abstract_1.ggr",
    "chars": 675,
    "preview": "GIMP Gradient\nName: Abstract 1\n6\n0.000000 0.286311 0.572621 0.269543 0.259267 1.000000 1.000000 0.215635 0.407414 0.9849"
  },
  {
    "path": "examples/ggr/Full_saturation_spectrum_CW.ggr",
    "chars": 153,
    "preview": "GIMP Gradient\nName: Full saturation spectrum CW\n1\n0.000000 0.500000 1.000000 1.000000 0.000000 0.000000 1.000000 1.00000"
  },
  {
    "path": "examples/gradients.go",
    "chars": 8962,
    "preview": "//go:build ignore\n// +build ignore\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"image/"
  },
  {
    "path": "gimp.go",
    "chars": 6685,
    "preview": "package colorgrad\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"strings\"\n)\n\n// References:\n// https://gitlab.gnome.org/GNOME"
  },
  {
    "path": "gimp_test.go",
    "chars": 2511,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc Test_GIMPGradient(t *testing.T) {\n\tblack := Rgb(0, 0, "
  },
  {
    "path": "go.mod",
    "chars": 97,
    "preview": "module github.com/mazznoer/colorgrad\n\ngo 1.18\n\nrequire github.com/mazznoer/csscolorparser v0.1.8\n"
  },
  {
    "path": "go.sum",
    "chars": 187,
    "preview": "github.com/mazznoer/csscolorparser v0.1.8 h1:i7w3wHW99d0q0KZv1ONkU/efXFAKcw1mgEgW6gj8KUA=\ngithub.com/mazznoer/csscolorpa"
  },
  {
    "path": "gradient.go",
    "chars": 3143,
    "preview": "package colorgrad\n\nimport (\n\t\"image/color\"\n\t\"math\"\n\n\t\"github.com/mazznoer/csscolorparser\"\n)\n\ntype BlendMode int\n\nconst ("
  },
  {
    "path": "gradient_test.go",
    "chars": 4076,
    "preview": "package colorgrad\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"testing\"\n)\n\nfunc Test_Basic(t *testing.T) {\n\ttest(t, Rgb(1, 0.8431, "
  },
  {
    "path": "linear.go",
    "chars": 1404,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n)\n\ntype linearGradient struct {\n\tcolors    [][4]float64\n\tpositions []float64\n\tmin   "
  },
  {
    "path": "linear_test.go",
    "chars": 751,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc Test_LinearGradient(t *testing.T) {\n\tgrad, err := NewGradient().\n"
  },
  {
    "path": "preset.go",
    "chars": 9014,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n)\n\n// Reference: https://github.com/d3/d3-scale-chromatic\n\nconst deg2rad = math.Pi /"
  },
  {
    "path": "preset_test.go",
    "chars": 8204,
    "preview": "package colorgrad\n\nimport (\n\t\"testing\"\n)\n\nfunc Test_PresetGradients(t *testing.T) {\n\tvar grad Gradient\n\n\tgrad = Cubeheli"
  },
  {
    "path": "sharp.go",
    "chars": 1504,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n)\n\ntype sharpGradient struct {\n\tcolors    []Color\n\tpositions []float64\n\tlast      in"
  },
  {
    "path": "sharp_test.go",
    "chars": 2487,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\nfunc Test_SharpGradient(t *testing.T) {\n\tvar grad, gradBase Gradient\n\n"
  },
  {
    "path": "smoothstep.go",
    "chars": 1673,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n)\n\ntype smoothstepGradient struct {\n\tcolors    [][4]float64\n\tpositions []float64\n\tmi"
  },
  {
    "path": "util.go",
    "chars": 3738,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc linspace(min, max float64, n uint) []float64 {\n\tif n ="
  },
  {
    "path": "util_test.go",
    "chars": 4174,
    "preview": "package colorgrad\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/mazznoer/csscolorparser\"\n)\n\nfunc Test_Utils(t *testing.T) {"
  }
]

About this extraction

This page contains the full source code of the mazznoer/colorgrad GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (95.6 KB), approximately 37.5k tokens, and a symbol index with 200 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!