gradient.go

  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}