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()`

`colorgrad.PRGn()`

`colorgrad.PiYG()`

`colorgrad.PuOr()`

`colorgrad.RdBu()`

`colorgrad.RdGy()`

`colorgrad.RdYlBu()`

`colorgrad.RdYlGn()`

`colorgrad.Spectral()`

## Sequential (Single Hue)
`colorgrad.Blues()`

`colorgrad.Greens()`

`colorgrad.Greys()`

`colorgrad.Oranges()`

`colorgrad.Purples()`

`colorgrad.Reds()`

## Sequential (Multi-Hue)
`colorgrad.Turbo()`

`colorgrad.Viridis()`

`colorgrad.Inferno()`

`colorgrad.Magma()`

`colorgrad.Plasma()`

`colorgrad.Cividis()`

`colorgrad.Warm()`

`colorgrad.Cool()`

`colorgrad.CubehelixDefault()`

`colorgrad.BuGn()`

`colorgrad.BuPu()`

`colorgrad.GnBu()`

`colorgrad.OrRd()`

`colorgrad.PuBuGn()`

`colorgrad.PuBu()`

`colorgrad.PuRd()`

`colorgrad.RdPu()`

`colorgrad.YlGnBu()`

`colorgrad.YlGn()`

`colorgrad.YlOrBr()`

`colorgrad.YlOrRd()`

## Cyclical
`colorgrad.Rainbow()`

`colorgrad.Sinebow()`

================================================
FILE: README.md
================================================
# colorgrad
[](https://github.com/mazznoer/colorgrad/releases/latest)
[](https://pkg.go.dev/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
[](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()
```

### 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()
```

### 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()
```

```go
grad, err := colorgrad.NewGradient().
HtmlColors("gold", "hotpink", "darkturquoise").
Build()
```

```go
grad, err := colorgrad.NewGradient().
HtmlColors(
"rgb(125,110,221)",
"rgb(90%,45%,97%)",
"hsl(229,79%,85%)",
).
Build()
```

### Domain & Color Position
Default domain is [0..1].
```go
grad, err := colorgrad.NewGradient().
HtmlColors("deeppink", "gold", "seagreen").
Build()
```

Set the domain to [0..100].
```go
grad, err := colorgrad.NewGradient().
HtmlColors("deeppink", "gold", "seagreen").
Domain(0, 100).
Build()
```

Set the domain to [-1..1].
```go
grad, err := colorgrad.NewGradient().
HtmlColors("deeppink", "gold", "seagreen").
Domain(-1, 1).
Build()
```

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()
```

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()
```

### CSS Gradient Format
```go
grad, err := colorgrad.NewGradient().
Css("deeppink, gold, seagreen").
Build()
```

```go
grad, err := colorgrad.NewGradient().
Css("purple, gold 35%, green 35%, 55%, gold").
Interpolation(colorgrad.InterpolationCatmullRom).
Build()
```

### Blending Mode
```go
grad, err := colorgrad.NewGradient().
HtmlColors("#FFF", "#00F").
Mode(colorgrad.BlendRgb).
Build()
```

### Interpolation Mode
```go
grad, err := colorgrad.NewGradient().
HtmlColors("#C41189", "#00BFFF", "#FFD700").
Interpolation(colorgrad.InterpolationLinear).
Build()
```
`InterpolationLinear`

`InterpolationSmoothstep`

`InterpolationCatmullRom`

`InterpolationBasis`

## 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
```

## 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)
```

This is the effect of different smoothness.

## 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:

### 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:

## 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
}
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
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](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.