shiny/iconvg: implement gradients.

Change-Id: I7510d7f9b4182882853840987510522ad7ab0935
Reviewed-on: https://go-review.googlesource.com/31375
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
index d941766..e23ccf1 100644
--- a/shiny/iconvg/decode_test.go
+++ b/shiny/iconvg/decode_test.go
@@ -135,6 +135,7 @@
 	{"testdata/arcs", ""},
 	{"testdata/blank", ""},
 	{"testdata/favicon", ""},
+	{"testdata/gradient", ""},
 	{"testdata/lod-polygon", ";64"},
 	{"testdata/video-005.primitive", ""},
 }
diff --git a/shiny/iconvg/doc.go b/shiny/iconvg/doc.go
index fd88ca8..10bcb84 100644
--- a/shiny/iconvg/doc.go
+++ b/shiny/iconvg/doc.go
@@ -549,6 +549,8 @@
 */
 package iconvg
 
+// TODO: elliptical gradients, not just circular?
+
 // TODO: shapes (circles, rects) and strokes? Or can we assume that authoring
 // tools will convert shapes and strokes to paths?
 
diff --git a/shiny/iconvg/encode.go b/shiny/iconvg/encode.go
index ea87e88..aa52041 100644
--- a/shiny/iconvg/encode.go
+++ b/shiny/iconvg/encode.go
@@ -6,14 +6,17 @@
 
 import (
 	"errors"
+	"image/color"
 	"math"
 )
 
 var (
+	errCSELUsedAsBothGradientAndStop = errors.New("iconvg: CSEL used as both gradient and stop")
 	errDrawingOpsUsedInStylingMode   = errors.New("iconvg: drawing ops used in styling mode")
 	errInvalidSelectorAdjustment     = errors.New("iconvg: invalid selector adjustment")
 	errInvalidIncrementingAdjustment = errors.New("iconvg: invalid incrementing adjustment")
 	errStylingOpsUsedInDrawingMode   = errors.New("iconvg: styling ops used in drawing mode")
+	errTooManyGradientStops          = errors.New("iconvg: too many gradient stops")
 )
 
 type mode uint8
@@ -259,6 +262,76 @@
 	e.buf.encodeReal(lod1)
 }
 
+// SetLinearGradient sets CREG[CSEL] to encode the linear gradient whose
+// geometry is defined by x1, y1, x2, y2 and colors defined by spread and
+// stops.
+//
+// The colors of the n stops are encoded at CREG[cBase+0], CREG[cBase+1], ...,
+// CREG[cBase+n-1]. Similarly, the offsets of the n stops are encoded at
+// NREG[nBase+0], NREG[nBase+1], ..., NREG[nBase+n-1]. Additional parameters
+// are stored at NREG[nBase-4], NREG[nBase-3], NREG[nBase-2] and NREG[nBase-1].
+//
+// See the package documentation for more details on the gradient encoding
+// format.
+//
+// The CSEL and NSEL selector registers maintain the same values after the
+// method returns as they had when the method was called.
+func (e *Encoder) SetLinearGradient(cBase, nBase uint8, x1, y1, x2, y2 float32, spread GradientSpread, stops []GradientStop) {
+	e.setGradient(cBase, nBase, x1, y1, x2, y2, 0x80, spread, stops)
+}
+
+// SetRadialGradient is like SetLinearGradient except that the radial
+// gradient's geometry is defined by cx, cy and r.
+func (e *Encoder) SetRadialGradient(cBase, nBase uint8, cx, cy, r float32, spread GradientSpread, stops []GradientStop) {
+	// TODO: two radii, r1 and r2, as per doc.go.
+	e.setGradient(cBase, nBase, cx, cy, 0, r, 0xc0, spread, stops)
+}
+
+func (e *Encoder) setGradient(cBase, nBase uint8, a0, a1, a2, a3 float32, bFlags uint8, spread GradientSpread, stops []GradientStop) {
+	e.checkModeStyling()
+	if e.err != nil {
+		return
+	}
+	if len(stops) > 60 {
+		e.err = errTooManyGradientStops
+		return
+	}
+	if x, y := e.cSel, e.cSel+64; (cBase <= x && x < cBase+uint8(len(stops))) ||
+		(cBase <= y && y < cBase+uint8(len(stops))) {
+		e.err = errCSELUsedAsBothGradientAndStop
+		return
+	}
+
+	oldCSel := e.cSel
+	oldNSel := e.nSel
+	cBase &= 0x3f
+	nBase &= 0x3f
+	e.SetCReg(0, false, RGBAColor(color.RGBA{
+		R: uint8(len(stops)),
+		G: cBase | uint8(spread<<6),
+		B: nBase | bFlags,
+		A: 0x00,
+	}))
+	e.SetCSel(cBase)
+	e.SetNSel(nBase)
+	e.SetNReg(4, false, a0)
+	e.SetNReg(3, false, a1)
+	e.SetNReg(2, false, a2)
+	e.SetNReg(1, false, a3)
+	for _, s := range stops {
+		r, g, b, a := s.Color.RGBA()
+		e.SetCReg(0, true, RGBAColor(color.RGBA{
+			R: uint8(r >> 8),
+			G: uint8(g >> 8),
+			B: uint8(b >> 8),
+			A: uint8(a >> 8),
+		}))
+		e.SetNReg(0, true, s.Offset)
+	}
+	e.SetCSel(oldCSel)
+	e.SetNSel(oldNSel)
+}
+
 func (e *Encoder) StartPath(adj uint8, x, y float32) {
 	e.checkModeStyling()
 	if e.err != nil {
diff --git a/shiny/iconvg/encode_test.go b/shiny/iconvg/encode_test.go
index 2a06891..8766d21 100644
--- a/shiny/iconvg/encode_test.go
+++ b/shiny/iconvg/encode_test.go
@@ -324,6 +324,54 @@
 	testEncode(t, &e, "testdata/favicon.ivg")
 }
 
+func TestEncodeGradient(t *testing.T) {
+	rgb := []GradientStop{
+		{Offset: 0.00, Color: color.RGBA{0xff, 0x00, 0x00, 0xff}},
+		{Offset: 0.25, Color: color.RGBA{0x00, 0xff, 0x00, 0xff}},
+		{Offset: 0.50, Color: color.RGBA{0x00, 0x00, 0xff, 0xff}},
+		{Offset: 1.00, Color: color.RGBA{0x00, 0x00, 0x00, 0xff}},
+	}
+	cmy := []GradientStop{
+		{Offset: 0.00, Color: color.RGBA{0x00, 0xff, 0xff, 0xff}},
+		{Offset: 0.25, Color: color.RGBA{0xff, 0xff, 0xff, 0xff}},
+		{Offset: 0.50, Color: color.RGBA{0xff, 0x00, 0xff, 0xff}},
+		{Offset: 0.75, Color: color.RGBA{0x00, 0x00, 0x00, 0x00}},
+		{Offset: 1.00, Color: color.RGBA{0xff, 0xff, 0x00, 0xff}},
+	}
+
+	var e Encoder
+
+	e.SetLinearGradient(10, 10, -12, -30, +12, -18, GradientSpreadNone, rgb)
+	e.StartPath(0, -30, -30)
+	e.AbsHLineTo(+30)
+	e.AbsVLineTo(-18)
+	e.AbsHLineTo(-30)
+	e.ClosePathEndPath()
+
+	e.SetLinearGradient(10, 10, -12, -14, +12, -2, GradientSpreadPad, cmy)
+	e.StartPath(0, -30, -14)
+	e.AbsHLineTo(+30)
+	e.AbsVLineTo(-2)
+	e.AbsHLineTo(-30)
+	e.ClosePathEndPath()
+
+	e.SetRadialGradient(10, 10, -8, 8, 16, GradientSpreadReflect, rgb)
+	e.StartPath(0, -30, +2)
+	e.AbsHLineTo(+30)
+	e.AbsVLineTo(+14)
+	e.AbsHLineTo(-30)
+	e.ClosePathEndPath()
+
+	e.SetRadialGradient(10, 10, -8, 24, 16, GradientSpreadRepeat, cmy)
+	e.StartPath(0, -30, +18)
+	e.AbsHLineTo(+30)
+	e.AbsVLineTo(+30)
+	e.AbsHLineTo(-30)
+	e.ClosePathEndPath()
+
+	testEncode(t, &e, "testdata/gradient.ivg")
+}
+
 var video005PrimitiveSVGData = []struct {
 	r, g, b uint32
 	x0, y0  int
diff --git a/shiny/iconvg/iconvg.go b/shiny/iconvg/iconvg.go
index 5d5fff5..753d49e 100644
--- a/shiny/iconvg/iconvg.go
+++ b/shiny/iconvg/iconvg.go
@@ -15,7 +15,10 @@
 
 var magicBytes = []byte(magic)
 
-var positiveInfinity = math.Float32frombits(0x7f800000)
+var (
+	negativeInfinity = math.Float32frombits(0xff800000)
+	positiveInfinity = math.Float32frombits(0x7f800000)
+)
 
 func isNaNOrInfinity(f float32) bool {
 	return math.Float32bits(f)&0x7f800000 == 0x7f800000
@@ -38,6 +41,23 @@
 	"repeat",
 }
 
+// GradientSpread is how to spread a gradient past its nominal bounds (from
+// offset being 0.0 to offset being 1.0).
+type GradientSpread uint8
+
+const (
+	GradientSpreadNone    GradientSpread = 0
+	GradientSpreadPad     GradientSpread = 1
+	GradientSpreadReflect GradientSpread = 2
+	GradientSpreadRepeat  GradientSpread = 3
+)
+
+// GradientStop is a color/offset gradient stop.
+type GradientStop struct {
+	Offset float32
+	Color  color.Color
+}
+
 // Rectangle is defined by its minimum and maximum coordinates.
 type Rectangle struct {
 	Min, Max f32.Vec2
diff --git a/shiny/iconvg/internal/gradient/gradient.go b/shiny/iconvg/internal/gradient/gradient.go
new file mode 100644
index 0000000..3baa944
--- /dev/null
+++ b/shiny/iconvg/internal/gradient/gradient.go
@@ -0,0 +1,279 @@
+// 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"
+
+	"golang.org/x/image/math/f64"
+)
+
+// TODO: gamma correction / non-linear color interpolation?
+
+// TODO: move this out of an internal directory, either under
+// golang.org/x/image or under the standard library's image, so that
+// golang.org/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.
+
+// 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
+	Ranges []Range
+
+	// First and Last are the first and last stop's colors.
+	First, Last color.RGBA64
+
+	// 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
+}
+
+func (g *Gradient) init(spread Spread, stops []Stop) {
+	g.Spread = spread
+	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
+	}
+}
+
+// InitLinear initializes g to be a linear gradient from (x1, y1) to (x2, y2),
+// in pixel space. Its colors are given by spread and stops.
+func (g *Gradient) InitLinear(x1, y1, x2, y2 float64, spread Spread, stops []Stop) {
+	g.init(spread, stops)
+	g.Shape = ShapeLinear
+	dx, dy := x2-x1, y2-y1
+	// The top row [a, b, c] of the Pix2Grad matrix satisfies the three
+	// simultaneous equations:
+	//	a*(x1   ) + b*(y1   ) + c = 0   (eq #0)
+	//	a*(x1+dy) + b*(y1-dx) + c = 0   (eq #1)
+	//	a*(x1+dx) + b*(y1+dy) + c = 1   (eq #2)
+	// Subtracting equation #0 from equations #1 and #2 give:
+	//	a*(  +dy) + b*(  -dx)     = 0   (eq #3)
+	//	a*(  +dx) + b*(  +dy)     = 1   (eq #4)
+	// So that
+	//	a*(dy*dy) - b*(dy*dx)     = 0   (eq #5)
+	//	a*(dx*dx) + b*(dx*dy)     = dx  (eq #6)
+	// And that
+	//	a = dx / (dx*dx + dy*dy)        (eq #7)
+	// Equations #3 and #7 yield:
+	//	b = dy / (dx*dx + dy*dy)        (eq #8)
+	d := dx*dx + dy*dy
+	a := dx / d
+	b := dy / d
+	g.Pix2Grad = f64.Aff3{
+		a, b, -a*x1 - b*y1,
+		0, 0, 0,
+	}
+}
+
+// InitCircular initializes g to be a circular gradient centered on (cx, cy)
+// with radius r, in pixel space. Its colors are given by spread and stops.
+func (g *Gradient) InitCircular(cx, cy, r float64, spread Spread, stops []Stop) {
+	g.init(spread, stops)
+	g.Shape = ShapeRadial
+	invR := 1 / r
+	g.Pix2Grad = f64.Aff3{
+		invR, 0, -cx * invR,
+		0, invR, -cy * invR,
+	}
+}
+
+// TODO: Gradient.InitElliptical?
+
+// 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
+}
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
index f03ac7d..da06ec0 100644
--- a/shiny/iconvg/rasterizer.go
+++ b/shiny/iconvg/rasterizer.go
@@ -10,6 +10,7 @@
 	"image/draw"
 	"math"
 
+	"golang.org/x/exp/shiny/iconvg/internal/gradient"
 	"golang.org/x/image/math/f32"
 	"golang.org/x/image/vector"
 )
@@ -57,9 +58,11 @@
 	fill      image.Image
 	flatColor color.RGBA
 	flatImage image.Uniform
+	gradient  gradient.Gradient
 
-	cReg [64]color.RGBA
-	nReg [64]float32
+	cReg  [64]color.RGBA
+	nReg  [64]float32
+	stops [64]gradient.Stop
 }
 
 // SetDstImage sets the Rasterizer to draw onto a destination image, given by
@@ -157,15 +160,74 @@
 	}
 }
 
+func (z *Rasterizer) initGradient(rgba color.RGBA) (ok bool) {
+	nStops := int(rgba.R & 0x3f)
+	cBase := int(rgba.G & 0x3f)
+	nBase := int(rgba.B & 0x3f)
+	prevN := negativeInfinity
+	for i := 0; i < nStops; i++ {
+		c := z.cReg[(cBase+i)&0x3f]
+		if !validAlphaPremulColor(c) {
+			return false
+		}
+		n := z.nReg[(nBase+i)&0x3f]
+		if !(0 <= n && n <= 1) || !(n > prevN) {
+			return false
+		}
+		prevN = n
+		z.stops[i] = gradient.Stop{
+			Offset: float64(n),
+			RGBA64: color.RGBA64{
+				R: uint16(c.R) * 0x101,
+				G: uint16(c.G) * 0x101,
+				B: uint16(c.B) * 0x101,
+				A: uint16(c.A) * 0x101,
+			},
+		}
+	}
+
+	if (rgba.B>>6)&0x01 == 0 {
+		z.gradient.InitLinear(
+			float64(z.absX(z.nReg[(nBase-4)&0x3f])),
+			float64(z.absY(z.nReg[(nBase-3)&0x3f])),
+			float64(z.absX(z.nReg[(nBase-2)&0x3f])),
+			float64(z.absY(z.nReg[(nBase-1)&0x3f])),
+			gradient.Spread(rgba.G>>6),
+			z.stops[:nStops],
+		)
+	} else {
+		// TODO: honor the r1 radius (at nBase-2), not just r2 (at nBase-1).
+		//
+		// TODO: relX can give a different scale/bias than relY. We should
+		// really use an elliptical (not circular) gradient, in gradient space
+		// (not pixel space).
+		r := z.relX(z.nReg[(nBase-1)&0x3f])
+
+		z.gradient.InitCircular(
+			float64(z.absX(z.nReg[(nBase-4)&0x3f])),
+			float64(z.absY(z.nReg[(nBase-3)&0x3f])),
+			float64(r),
+			gradient.Spread(rgba.G>>6),
+			z.stops[:nStops],
+		)
+	}
+	return true
+}
+
 func (z *Rasterizer) StartPath(adj uint8, x, y float32) {
-	// TODO: gradient fills, not just flat colors.
 	z.flatColor = z.cReg[(z.cSel-adj)&0x3f]
-	z.flatImage.C = &z.flatColor
-	z.fill = &z.flatImage
+	if validAlphaPremulColor(z.flatColor) {
+		z.flatImage.C = &z.flatColor
+		z.fill = &z.flatImage
+		z.disabled = z.flatColor.A == 0
+	} else if z.flatColor.A == 0x00 && z.flatColor.B&0x80 != 0 {
+		z.fill = &z.gradient
+		z.disabled = !z.initGradient(z.flatColor)
+	}
 
 	width, height := z.r.Dx(), z.r.Dy()
 	h := float32(height)
-	z.disabled = z.flatColor.A == 0 || !(z.lod0 <= h && h < z.lod1)
+	z.disabled = z.disabled || !(z.lod0 <= h && h < z.lod1)
 	if z.disabled {
 		return
 	}
diff --git a/shiny/iconvg/testdata/README b/shiny/iconvg/testdata/README
index 94ef89c..329454d 100644
--- a/shiny/iconvg/testdata/README
+++ b/shiny/iconvg/testdata/README
@@ -44,6 +44,14 @@
 
 
 
+gradient.ivg was created manually.
+
+gradient.ivg.disassembly is a disassembly of that IconVG file.
+
+gradient.png is a rendering of that IconVG file.
+
+
+
 lod-polygon.ivg was created manually.
 
 lod-polygon.ivg.disassembly is a disassembly of that IconVG file.
diff --git a/shiny/iconvg/testdata/gradient.ivg b/shiny/iconvg/testdata/gradient.ivg
new file mode 100644
index 0000000..9752286
--- /dev/null
+++ b/shiny/iconvg/testdata/gradient.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/gradient.ivg.disassembly b/shiny/iconvg/testdata/gradient.ivg.disassembly
new file mode 100644
index 0000000..697a91d
--- /dev/null
+++ b/shiny/iconvg/testdata/gradient.ivg.disassembly
@@ -0,0 +1,170 @@
+89 49 56 47   IconVG Magic identifier
+00            Number of metadata chunks: 0
+98            Set CREG[CSEL-0] to a 4 byte color
+04 0a 8a 00       gradient (NSTOPS=4, CBASE=10, NBASE=10, linear, none)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+b4            Set NREG[NSEL-4] to a coordinate number
+68                -12
+b3            Set NREG[NSEL-3] to a coordinate number
+44                -30
+aa            Set NREG[NSEL-2] to a real number
+18                12
+b1            Set NREG[NSEL-1] to a coordinate number
+5c                -18
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+64                RGBA ff0000ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+14                RGBA 00ff00ff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+3c                0.25
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+04                RGBA 0000ffff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+78                0.5
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+00                RGBA 000000ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+02                1
+00            Set CSEL = 0
+40            Set NSEL = 0
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+44                -30
+44                -30
+e6            H (absolute horizontal lineTo)
+bc                +30
+e8            V (absolute vertical lineTo)
+5c                -18
+e6            H (absolute horizontal lineTo)
+44                -30
+e1            z (closePath); end path
+98            Set CREG[CSEL-0] to a 4 byte color
+05 4a 8a 00       gradient (NSTOPS=5, CBASE=10, NBASE=10, linear, pad)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+b4            Set NREG[NSEL-4] to a coordinate number
+68                -12
+b3            Set NREG[NSEL-3] to a coordinate number
+64                -14
+aa            Set NREG[NSEL-2] to a real number
+18                12
+b1            Set NREG[NSEL-1] to a coordinate number
+7c                -2
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+18                RGBA 00ffffff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+7c                RGBA ffffffff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+3c                0.25
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+68                RGBA ff00ffff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+78                0.5
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+7f                RGBA 00000000
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+b4                0.75
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+78                RGBA ffff00ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+02                1
+00            Set CSEL = 0
+40            Set NSEL = 0
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+44                -30
+64                -14
+e6            H (absolute horizontal lineTo)
+bc                +30
+e8            V (absolute vertical lineTo)
+7c                -2
+e6            H (absolute horizontal lineTo)
+44                -30
+e1            z (closePath); end path
+98            Set CREG[CSEL-0] to a 4 byte color
+04 8a ca 00       gradient (NSTOPS=4, CBASE=10, NBASE=10, radial, reflect)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+b4            Set NREG[NSEL-4] to a coordinate number
+70                -8
+ab            Set NREG[NSEL-3] to a real number
+10                8
+aa            Set NREG[NSEL-2] to a real number
+00                0
+a9            Set NREG[NSEL-1] to a real number
+20                16
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+64                RGBA ff0000ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+14                RGBA 00ff00ff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+3c                0.25
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+04                RGBA 0000ffff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+78                0.5
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+00                RGBA 000000ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+02                1
+00            Set CSEL = 0
+40            Set NSEL = 0
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+44                -30
+84                +2
+e6            H (absolute horizontal lineTo)
+bc                +30
+e8            V (absolute vertical lineTo)
+9c                +14
+e6            H (absolute horizontal lineTo)
+44                -30
+e1            z (closePath); end path
+98            Set CREG[CSEL-0] to a 4 byte color
+05 ca ca 00       gradient (NSTOPS=5, CBASE=10, NBASE=10, radial, repeat)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+b4            Set NREG[NSEL-4] to a coordinate number
+70                -8
+ab            Set NREG[NSEL-3] to a real number
+30                24
+aa            Set NREG[NSEL-2] to a real number
+00                0
+a9            Set NREG[NSEL-1] to a real number
+20                16
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+18                RGBA 00ffffff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+7c                RGBA ffffffff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+3c                0.25
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+68                RGBA ff00ffff
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+78                0.5
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+7f                RGBA 00000000
+bf            Set NREG[NSEL-0] to a zero-to-one number; NSEL++
+b4                0.75
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+78                RGBA ffff00ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+02                1
+00            Set CSEL = 0
+40            Set NSEL = 0
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+44                -30
+a4                +18
+e6            H (absolute horizontal lineTo)
+bc                +30
+e8            V (absolute vertical lineTo)
+bc                +30
+e6            H (absolute horizontal lineTo)
+44                -30
+e1            z (closePath); end path
diff --git a/shiny/iconvg/testdata/gradient.png b/shiny/iconvg/testdata/gradient.png
new file mode 100644
index 0000000..7b4f028
--- /dev/null
+++ b/shiny/iconvg/testdata/gradient.png
Binary files differ