Skip to content

Commit

Permalink
Step 5: gimpGradient
Browse files Browse the repository at this point in the history
  • Loading branch information
mazznoer committed Jul 19, 2024
1 parent 49be39c commit a7a9f1b
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 62 deletions.
71 changes: 35 additions & 36 deletions gimp.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//go:build ignore
package colorgrad

import (
Expand All @@ -7,8 +6,6 @@ import (
"io"
"math"
"strings"

"github.com/lucasb-eyer/go-colorful"
)

// References:
Expand Down Expand Up @@ -40,9 +37,9 @@ const (

type gimpSegment struct {
// Left endpoint color
lcolor colorful.Color
lcolor Color
// Right endpoint color
rcolor colorful.Color
rcolor Color
// Left endpoint coordinate
lpos float64
// Midpoint coordinate
Expand All @@ -61,7 +58,7 @@ type gimpGradient struct {
dmax float64
}

func (ggr gimpGradient) At(t float64) colorful.Color {
func (ggr gimpGradient) At(t float64) Color {
if t <= ggr.dmin {
return ggr.segments[0].lcolor
}
Expand All @@ -71,7 +68,7 @@ func (ggr gimpGradient) At(t float64) colorful.Color {
}

if math.IsNaN(t) {
return colorful.Color{R: 0, G: 0, B: 0}
return Color{R: 0, G: 0, B: 0, A: 1}
}

low := 0
Expand Down Expand Up @@ -135,7 +132,7 @@ func (ggr gimpGradient) At(t float64) colorful.Color {

switch seg.coloring {
case rgb:
return seg.lcolor.BlendRgb(seg.rcolor, f)
return blendRgb(seg.lcolor, seg.rcolor, f)
case hsvCcw:
return blendHsvCcw(seg.lcolor, seg.rcolor, f)
case hsvCw:
Expand Down Expand Up @@ -164,16 +161,16 @@ func calc_linear_factor(middle, pos float64) float64 {
}
}

func blendHsvCcw(c1, c2 colorful.Color, t float64) colorful.Color {
h1, s1, v1 := c1.Hsv()
h2, s2, v2 := c2.Hsv()
func blendHsvCcw(c1, c2 Color, t float64) Color {
hsvA := col2hsv(c1)
hsvB := col2hsv(c2)

var hue float64

if h1 < h2 {
hue = h1 + ((h2 - h1) * t)
if hsvA[0] < hsvB[0] {
hue = hsvA[0] + ((hsvB[0] - hsvA[0]) * t)
} else {
h := h1 + ((360 - (h1 - h2)) * t)
h := hsvA[0] + ((360 - (hsvA[0] - hsvB[0])) * t)

if h > 360 {
hue = h - 360
Expand All @@ -182,23 +179,24 @@ func blendHsvCcw(c1, c2 colorful.Color, t float64) colorful.Color {
}
}

return colorful.Hsv(
return Hsv(
hue,
s1+t*(s2-s1),
v1+t*(v2-v1),
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 colorful.Color, t float64) colorful.Color {
h1, s1, v1 := c1.Hsv()
h2, s2, v2 := c2.Hsv()
func blendHsvCw(c1, c2 Color, t float64) Color {
hsvA := col2hsv(c1)
hsvB := col2hsv(c2)

var hue float64

if h2 < h1 {
hue = h1 - ((h1 - h2) * t)
if hsvB[0] < hsvA[0] {
hue = hsvA[0] - ((hsvA[0] - hsvB[0]) * t)
} else {
h := h1 - ((360 - (h2 - h1)) * t)
h := hsvA[0] - ((360 - (hsvB[0] - hsvA[0])) * t)

if h < 0 {
hue = h + 360
Expand All @@ -207,14 +205,15 @@ func blendHsvCw(c1, c2 colorful.Color, t float64) colorful.Color {
}
}

return colorful.Hsv(
return Hsv(
hue,
s1+t*(s2-s1),
v1+t*(v2-v1),
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 colorful.Color) (Gradient, string, error) {
func ParseGgr(r io.Reader, fg, bg Color) (Gradient, string, error) {
zgrad := Gradient{
grad: zeroGradient{},
dmin: 0,
Expand Down Expand Up @@ -289,7 +288,7 @@ func ParseGgr(r io.Reader, fg, bg colorful.Color) (Gradient, string, error) {
}, name, nil
}

func parseSegment(s string, fg, bg colorful.Color) (gimpSegment, bool) {
func parseSegment(s string, fg, bg Color) (gimpSegment, bool) {
params := strings.Fields(s)
plen := len(params)

Expand Down Expand Up @@ -347,36 +346,36 @@ func parseSegment(s string, fg, bg colorful.Color) (gimpSegment, bool) {
return gimpSegment{}, false
}

var lcolor colorful.Color
var lcolor Color

switch int(d[13]) {
case 0:
lcolor = colorful.Color{R: d[3], G: d[4], B: d[5]}
lcolor = Color{R: d[3], G: d[4], B: d[5], A: d[6]}
case 1:
lcolor = fg
case 2:
lcolor = fg // TODO transparent
lcolor = Rgb(fg.R, fg.G, fg.B, 0)
case 3:
lcolor = bg
case 4:
lcolor = bg // TODO transparent
lcolor = Rgb(bg.R, bg.G, bg.B, 0)
default:
return gimpSegment{}, false
}

var rcolor colorful.Color
var rcolor Color

switch int(d[14]) {
case 0:
rcolor = colorful.Color{R: d[7], G: d[8], B: d[9]}
rcolor = Color{R: d[7], G: d[8], B: d[9], A: d[10]}
case 1:
rcolor = fg
case 2:
rcolor = fg // TODO transparent
rcolor = Rgb(fg.R, fg.G, fg.B, 0)
case 3:
rcolor = bg
case 4:
rcolor = bg // TODO transparent
rcolor = Rgb(bg.R, bg.G, bg.B, 0)
default:
return gimpSegment{}, false
}
Expand Down
49 changes: 23 additions & 26 deletions gimp_test.go
Original file line number Diff line number Diff line change
@@ -1,62 +1,59 @@
//go:build ignore
package colorgrad

import (
"math"
"strings"
"testing"

"github.com/lucasb-eyer/go-colorful"
)

func TestParseGgr(t *testing.T) {
black := colorful.Color{R: 0, G: 0, B: 0}
red := colorful.Color{R: 1, G: 0, B: 0}
blue := colorful.Color{R: 0, G: 0, B: 1}
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, _ := ParseGgr(strings.NewReader(ggr), black, black)
testStr(t, name, "My Gradient")
testStr(t, grad.At(0).Hex(), "#000000")
testStr(t, grad.At(1).Hex(), "#ffffff")
testStr(t, grad.At(-0.5).Hex(), "#000000")
testStr(t, grad.At(1.5).Hex(), "#ffffff")
testStr(t, grad.At(math.NaN()).Hex(), "#000000")
testStr(t, grad.At(0).HexString(), "#000000")
testStr(t, grad.At(1).HexString(), "#ffffff")
testStr(t, grad.At(-0.5).HexString(), "#000000")
testStr(t, grad.At(1.5).HexString(), "#ffffff")
testStr(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, _ = ParseGgr(strings.NewReader(ggr), red, blue)
testStr(t, grad.At(0).Hex(), "#ff0000")
testStr(t, grad.At(1).Hex(), "#0000ff")
testStr(t, grad.At(0).HexString(), "#ff0000")
testStr(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, _ = ParseGgr(strings.NewReader(ggr), black, black)
testStr(t, grad.At(0.00).Hex(), "#ff0000")
testStr(t, grad.At(0.25).Hex(), "#ff0000")
testStr(t, grad.At(0.49).Hex(), "#ff0000")
testStr(t, grad.At(0.51).Hex(), "#0000ff")
testStr(t, grad.At(0.75).Hex(), "#0000ff")
testStr(t, grad.At(1.00).Hex(), "#0000ff")
testStr(t, grad.At(0.00).HexString(), "#ff0000")
testStr(t, grad.At(0.25).HexString(), "#ff0000")
testStr(t, grad.At(0.49).HexString(), "#ff0000")
testStr(t, grad.At(0.51).HexString(), "#0000ff")
testStr(t, grad.At(0.75).HexString(), "#0000ff")
testStr(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, _ = ParseGgr(strings.NewReader(ggr), black, black)
testStr(t, grad.At(0.0).Hex(), "#ffffff")
testStr(t, grad.At(0.5).Hex(), "#80ff80")
testStr(t, grad.At(1.0).Hex(), "#0000ff")
testStr(t, grad.At(0.0).HexString(), "#ffffff")
testStr(t, grad.At(0.5).HexString(), "#80ff80")
testStr(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, _ = ParseGgr(strings.NewReader(ggr), black, black)
testStr(t, grad.At(0.0).Hex(), "#ffffff")
testStr(t, grad.At(0.5).Hex(), "#ff80ff")
testStr(t, grad.At(1.0).Hex(), "#0000ff")
testStr(t, grad.At(0.0).HexString(), "#ffffff")
testStr(t, grad.At(0.5).HexString(), "#ff80ff")
testStr(t, grad.At(1.0).HexString(), "#0000ff")
}

func TestParseGgrError(t *testing.T) {
black := colorful.Color{R: 0, G: 0, B: 0}
black := Rgb(0, 0, 0, 1)

_, _, err0 := ParseGgr(strings.NewReader(""), black, black)
if err0 == nil {
Expand Down
12 changes: 12 additions & 0 deletions gradient.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@ func (i Interpolation) String() string {

type Color = csscolorparser.Color

var Hwb = csscolorparser.FromHwb
var Hsv = csscolorparser.FromHsv
var Hsl = csscolorparser.FromHsl
var LinearRgb = csscolorparser.FromLinearRGB
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}
}

type gradientBase interface {
// Get color at certain position
Expand Down
35 changes: 35 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,41 @@ func col2oklab(col Color) [4]float64 {
}
}

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 {
Expand Down

0 comments on commit a7a9f1b

Please sign in to comment.