-
Notifications
You must be signed in to change notification settings - Fork 0
/
gradient.go
242 lines (217 loc) · 6.03 KB
/
gradient.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package gradient provides linear and radial gradient images.
package gradient
import (
"image"
"image/color"
"math"
"github.com/Andyfoo/golang/x/image/math/f64"
)
// TODO: gamma correction / non-linear color interpolation?
// TODO: move this out of an internal directory, either under
// github.com/Andyfoo/golang/x/image or under the standard library's image, so that
// github.com/Andyfoo/golang/x/image/{draw,vector} and possibly image/draw can type switch on
// the gradient.Gradient type and provide fast path code.
//
// Doing so requires coming up with a stable API that we'd be happy to support
// in the long term. This would probably include an easier way to create
// linear, circular and elliptical gradients, without having to explicitly
// calculate the f64.Aff3 matrix.
// Shape is the gradient shape.
type Shape uint8
const (
ShapeLinear Shape = iota
ShapeRadial
)
// Spread is the gradient spread, or how to spread a gradient past its nominal
// bounds (from offset being 0.0 to offset being 1.0).
type Spread uint8
const (
// SpreadNone means that offsets outside of the [0, 1] range map to
// transparent black.
SpreadNone Spread = iota
// SpreadPad means that offsets below 0 and above 1 map to the colors that
// 0 and 1 would map to.
SpreadPad
// SpreadReflect means that the offset mapping is reflected start-to-end,
// end-to-start, start-to-end, etc.
SpreadReflect
// SpreadRepeat means that the offset mapping is repeated start-to-end,
// start-to-end, start-to-end, etc.
SpreadRepeat
)
// Clamp clamps x to the range [0, 1]. If x is outside that range, it is
// converted to a value in that range according to s's semantics. It returns -1
// if s is SpreadNone and x is outside the range [0, 1].
func (s Spread) Clamp(x float64) float64 {
if x >= 0 {
if x <= 1 {
return x
}
switch s {
case SpreadPad:
return 1
case SpreadReflect:
if int(x)&1 == 0 {
return x - math.Floor(x)
}
return math.Ceil(x) - x
case SpreadRepeat:
return x - math.Floor(x)
}
return -1
}
switch s {
case SpreadPad:
return 0
case SpreadReflect:
x = -x
if int(x)&1 == 0 {
return x - math.Floor(x)
}
return math.Ceil(x) - x
case SpreadRepeat:
return x - math.Floor(x)
}
return -1
}
// Stop is an offset and color.
type Stop struct {
Offset float64
RGBA64 color.RGBA64
}
// Range is the range between two stops.
type Range struct {
Offset0 float64
Offset1 float64
Width float64
R0 float64
R1 float64
G0 float64
G1 float64
B0 float64
B1 float64
A0 float64
A1 float64
}
// MakeRange returns the range between two stops.
func MakeRange(s0, s1 Stop) Range {
return Range{
Offset0: s0.Offset,
Offset1: s1.Offset,
Width: s1.Offset - s0.Offset,
R0: float64(s0.RGBA64.R),
R1: float64(s1.RGBA64.R),
G0: float64(s0.RGBA64.G),
G1: float64(s1.RGBA64.G),
B0: float64(s0.RGBA64.B),
B1: float64(s1.RGBA64.B),
A0: float64(s0.RGBA64.A),
A1: float64(s1.RGBA64.A),
}
}
// AppendRanges appends to a the ranges defined by a's implicit final stop (if
// any exist) and stops.
func AppendRanges(a []Range, stops []Stop) []Range {
if len(stops) == 0 {
return nil
}
if len(a) != 0 {
z := a[len(a)-1]
a = append(a, MakeRange(Stop{
Offset: z.Offset1,
RGBA64: color.RGBA64{
R: uint16(z.R1),
G: uint16(z.G1),
B: uint16(z.B1),
A: uint16(z.A1),
},
}, stops[0]))
}
for i := 0; i < len(stops)-1; i++ {
a = append(a, MakeRange(stops[i], stops[i+1]))
}
return a
}
// Gradient is a very large image.Image (the same size as an image.Uniform)
// whose colors form a gradient.
type Gradient struct {
Shape Shape
Spread Spread
// Pix2Grad transforms coordinates from pixel space (the arguments to the
// Image.At method) to gradient space. Gradient space is where a linear
// gradient ranges from x == 0 to x == 1, and a radial gradient has center
// (0, 0) and radius 1.
//
// This is an affine transform, so it can represent elliptical gradients in
// pixel space, including non-axis-aligned ellipses.
//
// For a linear gradient, the bottom row is ignored.
Pix2Grad f64.Aff3
Ranges []Range
// First and Last are the first and last stop's colors.
First, Last color.RGBA64
}
// Init initializes g to a gradient whose geometry is defined by shape and
// pix2Grad and whose colors are defined by spread and stops.
func (g *Gradient) Init(shape Shape, spread Spread, pix2Grad f64.Aff3, stops []Stop) {
g.Shape = shape
g.Spread = spread
g.Pix2Grad = pix2Grad
g.Ranges = AppendRanges(g.Ranges[:0], stops)
if len(stops) == 0 {
g.First = color.RGBA64{}
g.Last = color.RGBA64{}
} else {
g.First = stops[0].RGBA64
g.Last = stops[len(stops)-1].RGBA64
}
}
// ColorModel satisfies the image.Image interface.
func (g *Gradient) ColorModel() color.Model {
return color.RGBA64Model
}
// Bounds satisfies the image.Image interface.
func (g *Gradient) Bounds() image.Rectangle {
return image.Rectangle{
Min: image.Point{-1e9, -1e9},
Max: image.Point{+1e9, +1e9},
}
}
// At satisfies the image.Image interface.
func (g *Gradient) At(x, y int) color.Color {
if len(g.Ranges) == 0 {
return color.RGBA64{}
}
px := float64(x) + 0.5
py := float64(y) + 0.5
offset := 0.0
if g.Shape == ShapeLinear {
offset = g.Spread.Clamp(g.Pix2Grad[0]*px + g.Pix2Grad[1]*py + g.Pix2Grad[2])
} else {
gx := g.Pix2Grad[0]*px + g.Pix2Grad[1]*py + g.Pix2Grad[2]
gy := g.Pix2Grad[3]*px + g.Pix2Grad[4]*py + g.Pix2Grad[5]
offset = g.Spread.Clamp(math.Sqrt(gx*gx + gy*gy))
}
if !(offset >= 0) {
return color.RGBA64{}
}
if offset < g.Ranges[0].Offset0 {
return g.First
}
for _, r := range g.Ranges {
if r.Offset0 <= offset && offset <= r.Offset1 {
t := (offset - r.Offset0) / r.Width
s := 1 - t
return color.RGBA64{
uint16(s*r.R0 + t*r.R1),
uint16(s*r.G0 + t*r.G1),
uint16(s*r.B0 + t*r.B1),
uint16(s*r.A0 + t*r.A1),
}
}
}
return g.Last
}