1// Gradient implementation fo rasterx package
2// Copyright 2018 All rights reserved.
3// Created: 5/12/2018 by S.R.Wiley
4
5package rasterx
6
7import (
8 "image/color"
9 "math"
10 "sort"
11)
12
13// SVG bounds paremater constants
14const (
15 ObjectBoundingBox GradientUnits = iota
16 UserSpaceOnUse
17)
18
19// SVG spread parameter constants
20const (
21 PadSpread SpreadMethod = iota
22 ReflectSpread
23 RepeatSpread
24)
25
26const epsilonF = 1e-5
27
28type (
29 // SpreadMethod is the type for spread parameters
30 SpreadMethod byte
31 // GradientUnits is the type for gradient units
32 GradientUnits byte
33 // GradStop represents a stop in the SVG 2.0 gradient specification
34 GradStop struct {
35 StopColor color.Color
36 Offset float64
37 Opacity float64
38 }
39 // Gradient holds a description of an SVG 2.0 gradient
40 Gradient struct {
41 Points [5]float64
42 Stops []GradStop
43 Bounds struct{ X, Y, W, H float64 }
44 Matrix Matrix2D
45 Spread SpreadMethod
46 Units GradientUnits
47 IsRadial bool
48 }
49)
50
51// ApplyOpacity sets the color's alpha channel to the given value
52func ApplyOpacity(c color.Color, opacity float64) color.NRGBA {
53 r, g, b, _ := c.RGBA()
54 return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(opacity * 0xFF)}
55}
56
57// tColor takes the paramaterized value along the gradient's stops and
58// returns a color depending on the spreadMethod value of the gradient and
59// the gradient's slice of stop values.
60func (g *Gradient) tColor(t, opacity float64) color.Color {
61 d := len(g.Stops)
62 // These cases can be taken care of early on
63 if t >= 1.0 && g.Spread == PadSpread {
64 s := g.Stops[d-1]
65 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
66 }
67 if t <= 0.0 && g.Spread == PadSpread {
68 return ApplyOpacity(g.Stops[0].StopColor, g.Stops[0].Opacity*opacity)
69 }
70
71 var modRange = 1.0
72 if g.Spread == ReflectSpread {
73 modRange = 2.0
74 }
75 mod := math.Mod(t, modRange)
76 if mod < 0 {
77 mod += modRange
78 }
79
80 place := 0 // Advance to place where mod is greater than the indicated stop
81 for place != len(g.Stops) && mod > g.Stops[place].Offset {
82 place++
83 }
84 switch g.Spread {
85 case RepeatSpread:
86 var s1, s2 GradStop
87 switch place {
88 case 0, d:
89 s1, s2 = g.Stops[d-1], g.Stops[0]
90 default:
91 s1, s2 = g.Stops[place-1], g.Stops[place]
92 }
93 return g.blendStops(mod, opacity, s1, s2, false)
94 case ReflectSpread:
95 switch place {
96 case 0:
97 return ApplyOpacity(g.Stops[0].StopColor, g.Stops[0].Opacity*opacity)
98 case d:
99 // Advance to place where mod-1 is greater than the stop indicated by place in reverse of the stop slice.
100 // Since this is the reflect spead mode, the mod interval is two, allowing the stop list to be
101 // iterated in reverse before repeating the sequence.
102 for place != d*2 && mod-1 > (1-g.Stops[d*2-place-1].Offset) {
103 place++
104 }
105 switch place {
106 case d:
107 s := g.Stops[d-1]
108 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
109 case d * 2:
110 return ApplyOpacity(g.Stops[0].StopColor, g.Stops[0].Opacity*opacity)
111 default:
112 return g.blendStops(mod-1, opacity,
113 g.Stops[d*2-place], g.Stops[d*2-place-1], true)
114 }
115 default:
116 return g.blendStops(mod, opacity,
117 g.Stops[place-1], g.Stops[place], false)
118 }
119 default: // PadSpread
120 switch place {
121 case 0:
122 return ApplyOpacity(g.Stops[0].StopColor, g.Stops[0].Opacity*opacity)
123 case len(g.Stops):
124 s := g.Stops[len(g.Stops)-1]
125 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
126 default:
127 return g.blendStops(mod, opacity, g.Stops[place-1], g.Stops[place], false)
128 }
129 }
130}
131
132func (g *Gradient) blendStops(t, opacity float64, s1, s2 GradStop, flip bool) color.Color {
133 s1off := s1.Offset
134 if s1.Offset > s2.Offset && !flip { // happens in repeat spread mode
135 s1off--
136 if t > 1 {
137 t--
138 }
139 }
140 if s2.Offset == s1off {
141 return ApplyOpacity(s2.StopColor, s2.Opacity)
142 }
143 if flip {
144 t = 1 - t
145 }
146 tp := (t - s1off) / (s2.Offset - s1off)
147 r1, g1, b1, _ := s1.StopColor.RGBA()
148 r2, g2, b2, _ := s2.StopColor.RGBA()
149
150 return ApplyOpacity(color.RGBA{
151 uint8((float64(r1)*(1-tp) + float64(r2)*tp) / 256),
152 uint8((float64(g1)*(1-tp) + float64(g2)*tp) / 256),
153 uint8((float64(b1)*(1-tp) + float64(b2)*tp) / 256),
154 0xFF}, (s1.Opacity*(1-tp)+s2.Opacity*tp)*opacity)
155}
156
157//GetColorFunction returns the color function
158func (g *Gradient) GetColorFunction(opacity float64) interface{} {
159 return g.GetColorFunctionUS(opacity, Identity)
160}
161
162//GetColorFunctionUS returns the color function using the User Space objMatrix
163func (g *Gradient) GetColorFunctionUS(opacity float64, objMatrix Matrix2D) interface{} {
164 switch len(g.Stops) {
165 case 0:
166 return ApplyOpacity(color.RGBA{0, 0, 0, 255}, opacity) // default error color for gradient w/o stops.
167 case 1:
168 return ApplyOpacity(g.Stops[0].StopColor, opacity) // Illegal, I think, should really should not happen.
169 }
170
171 // sort by offset in ascending order
172 sort.Slice(g.Stops, func(i, j int) bool {
173 return g.Stops[i].Offset < g.Stops[j].Offset
174 })
175
176 w, h := float64(g.Bounds.W), float64(g.Bounds.H)
177 oriX, oriY := float64(g.Bounds.X), float64(g.Bounds.Y)
178 gradT := Identity.Translate(oriX, oriY).Scale(w, h).
179 Mult(g.Matrix).Scale(1/w, 1/h).Translate(-oriX, -oriY).Invert()
180
181 if g.IsRadial {
182 cx, cy, fx, fy, rx, ry := g.Points[0], g.Points[1], g.Points[2], g.Points[3], g.Points[4], g.Points[4]
183 if g.Units == ObjectBoundingBox {
184 cx = g.Bounds.X + g.Bounds.W*cx
185 cy = g.Bounds.Y + g.Bounds.H*cy
186 fx = g.Bounds.X + g.Bounds.W*fx
187 fy = g.Bounds.Y + g.Bounds.H*fy
188 rx *= g.Bounds.W
189 ry *= g.Bounds.H
190 } else {
191 cx, cy = g.Matrix.Transform(cx, cy)
192 fx, fy = g.Matrix.Transform(fx, fy)
193 rx, ry = g.Matrix.TransformVector(rx, ry)
194 cx, cy = objMatrix.Transform(cx, cy)
195 fx, fy = objMatrix.Transform(fx, fy)
196 rx, ry = objMatrix.TransformVector(rx, ry)
197 }
198
199 if cx == fx && cy == fy {
200 // When the focus and center are the same things are much simpler;
201 // t is just distance from center
202 // scaled by the bounds aspect ratio times r
203 if g.Units == ObjectBoundingBox {
204 return ColorFunc(func(xi, yi int) color.Color {
205 x, y := gradT.Transform(float64(xi)+0.5, float64(yi)+0.5)
206 dx := float64(x) - cx
207 dy := float64(y) - cy
208 return g.tColor(math.Sqrt(dx*dx/(rx*rx)+(dy*dy)/(ry*ry)), opacity)
209 })
210 }
211 return ColorFunc(func(xi, yi int) color.Color {
212 x := float64(xi) + 0.5
213 y := float64(yi) + 0.5
214 dx := x - cx
215 dy := y - cy
216 return g.tColor(math.Sqrt(dx*dx/(rx*rx)+(dy*dy)/(ry*ry)), opacity)
217 })
218 }
219 fx /= rx
220 fy /= ry
221 cx /= rx
222 cy /= ry
223
224 dfx := fx - cx
225 dfy := fy - cy
226
227 if dfx*dfx+dfy*dfy > 1 { // Focus outside of circle; use intersection
228 // point of line from center to focus and circle as per SVG specs.
229 nfx, nfy, intersects := RayCircleIntersectionF(fx, fy, cx, cy, cx, cy, 1.0-epsilonF)
230 fx, fy = nfx, nfy
231 if intersects == false {
232 return color.RGBA{255, 255, 0, 255} // should not happen
233 }
234 }
235 if g.Units == ObjectBoundingBox {
236 return ColorFunc(func(xi, yi int) color.Color {
237 x, y := gradT.Transform(float64(xi)+0.5, float64(yi)+0.5)
238 ex := x / rx
239 ey := y / ry
240
241 t1x, t1y, intersects := RayCircleIntersectionF(ex, ey, fx, fy, cx, cy, 1.0)
242 if intersects == false { //In this case, use the last stop color
243 s := g.Stops[len(g.Stops)-1]
244 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
245 }
246 tdx, tdy := t1x-fx, t1y-fy
247 dx, dy := ex-fx, ey-fy
248 if tdx*tdx+tdy*tdy < epsilonF {
249 s := g.Stops[len(g.Stops)-1]
250 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
251 }
252 return g.tColor(math.Sqrt(dx*dx+dy*dy)/math.Sqrt(tdx*tdx+tdy*tdy), opacity)
253 })
254 }
255 return ColorFunc(func(xi, yi int) color.Color {
256 x := float64(xi) + 0.5
257 y := float64(yi) + 0.5
258 ex := x / rx
259 ey := y / ry
260
261 t1x, t1y, intersects := RayCircleIntersectionF(ex, ey, fx, fy, cx, cy, 1.0)
262 if intersects == false { //In this case, use the last stop color
263 s := g.Stops[len(g.Stops)-1]
264 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
265 }
266 tdx, tdy := t1x-fx, t1y-fy
267 dx, dy := ex-fx, ey-fy
268 if tdx*tdx+tdy*tdy < epsilonF {
269 s := g.Stops[len(g.Stops)-1]
270 return ApplyOpacity(s.StopColor, s.Opacity*opacity)
271 }
272 return g.tColor(math.Sqrt(dx*dx+dy*dy)/math.Sqrt(tdx*tdx+tdy*tdy), opacity)
273 })
274 }
275 p1x, p1y, p2x, p2y := g.Points[0], g.Points[1], g.Points[2], g.Points[3]
276 if g.Units == ObjectBoundingBox {
277 p1x = g.Bounds.X + g.Bounds.W*p1x
278 p1y = g.Bounds.Y + g.Bounds.H*p1y
279 p2x = g.Bounds.X + g.Bounds.W*p2x
280 p2y = g.Bounds.Y + g.Bounds.H*p2y
281
282 dx := p2x - p1x
283 dy := p2y - p1y
284 d := (dx*dx + dy*dy) // self inner prod
285 return ColorFunc(func(xi, yi int) color.Color {
286 x, y := gradT.Transform(float64(xi)+0.5, float64(yi)+0.5)
287 dfx := x - p1x
288 dfy := y - p1y
289 return g.tColor((dx*dfx+dy*dfy)/d, opacity)
290 })
291 }
292
293 p1x, p1y = g.Matrix.Transform(p1x, p1y)
294 p2x, p2y = g.Matrix.Transform(p2x, p2y)
295 p1x, p1y = objMatrix.Transform(p1x, p1y)
296 p2x, p2y = objMatrix.Transform(p2x, p2y)
297 dx := p2x - p1x
298 dy := p2y - p1y
299 d := (dx*dx + dy*dy)
300 // if d == 0.0 {
301 // fmt.Println("zero delta")
302 // }
303 return ColorFunc(func(xi, yi int) color.Color {
304 x := float64(xi) + 0.5
305 y := float64(yi) + 0.5
306 dfx := x - p1x
307 dfy := y - p1y
308 return g.tColor((dx*dfx+dy*dfy)/d, opacity)
309 })
310}