shiny/iconvg: implement elliptical gradients.

Change-Id: I8db1977187c1137823b88d4616f6dc3d6ee70968
Reviewed-on: https://go-review.googlesource.com/31542
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
index e23ccf1..520abb7 100644
--- a/shiny/iconvg/decode_test.go
+++ b/shiny/iconvg/decode_test.go
@@ -134,6 +134,8 @@
 	{"testdata/action-info.hires", ""},
 	{"testdata/arcs", ""},
 	{"testdata/blank", ""},
+	{"testdata/cowbell", ""},
+	{"testdata/elliptical", ""},
 	{"testdata/favicon", ""},
 	{"testdata/gradient", ""},
 	{"testdata/lod-polygon", ";64"},
@@ -165,7 +167,7 @@
 			continue
 		}
 		if !bytes.Equal(got, want) {
-			t.Errorf("got:\n%s\nwant:\n%s", got, want)
+			t.Errorf("%s: got:\n%s\nwant:\n%s", tc.filename, got, want)
 			diffLines(t, string(got), string(want))
 		}
 	}
diff --git a/shiny/iconvg/doc.go b/shiny/iconvg/doc.go
index 10bcb84..bfa7310 100644
--- a/shiny/iconvg/doc.go
+++ b/shiny/iconvg/doc.go
@@ -98,8 +98,11 @@
 The high 2 bits of the green value is how to spread the gradient past its
 nominal bounds (from offset being 0.0 to offset being 1.0). The high two bits
 being 0, 1, 2 or 3 mean none, pad, reflect and repeat respectively. None means
-extend with transparent black, the others are per SVG's
-https://www.w3.org/TR/SVG/pservers.html#LinearGradientElementSpreadMethodAttribute
+that offsets outside of the [0.0, 1.0] range map to transparent black. Pad
+means that offsets below 0.0 and above 1.0 map to the colors that 0.0 and 1.0
+would map to. Reflect means that the offset mapping is reflected start-to-end,
+end-to-start, start-to-end, etc. Repeat means that the offset mapping is
+repeated start-to-end, start-to-end, start-to-end, etc.
 
 The low 6 bits of the blue value is the number register base, NBASE.
 
@@ -108,15 +111,23 @@
 
 The gradient has NSTOPS color/offset stops. The first stop has color
 CREG[CBASE+0] and offset NREG[NBASE+0], the second stop has color CREG[CBASE+1]
-and offset NREG[NBASE+1], and so on. The gradient also uses the four numbers
-NREG[NBASE-4], NREG[NBASE-3], NREG[NBASE-2], NREG[NBASE-1].
+and offset NREG[NBASE+1], and so on.
 
-For a linear gradient, these are the x1, y1, x2 and y2 (in order) that define
-the gradient start and end, corresponding to stop offsets 0 and 1.
+The gradient also uses the six numbers from NREG[NBASE-6] to NREG[NBASE-1],
+which form an affine transformation matrix [a, b, c; d, e, f] such that
+a=NREG[NBASE-6], b=NREG[NBASE-5], c=NREG[NBASE-4], etc. This matrix maps from
+graphic coordinate space (defined by the metadata's viewBox) to gradient
+coordinate space. Gradient coordinate space is where a linear gradient ranges
+from x=0 to x=1, and a radial gradient has center (0, 0) and radius 1.
 
-For a radial gradient, these are the cx, cy, r1 and r2 (in order) that define
-the gradient center and a start and end radius, corresponding to stop offsets 0
-and 1.
+The graphic coordinate (px, py) maps to the gradient coordinate (dx, dy) by:
+
+	dx = a*px + b*py + c
+	dy = d*px + e*py + f
+
+The appendix below gives explicit formulae for the [a, b, c; d, e, f] affine
+transformation matrix for common gradient geometry, such as a linear gradient
+defined by two points.
 
 At the time a gradient is used to fill a path, it is invalid for any of the
 stop colors to itself be a gradient, or for any stop offset to be less than or
@@ -546,11 +557,104 @@
 	e1            z (closePath); end path
 
 There are more examples in the ./testdata directory.
+
+
+Appendix - Gradient Transformation Matrices
+
+This appendix derives the affine transformation matrices [a, b, c; d, e, f] for
+linear, circular and elliptical gradients.
+
+
+Linear Gradients
+
+For a linear gradient from (x1, y1) to (x2, y2), let dx, dy = x2-x1, y2-y1. In
+gradient coordinate space, the y-coordinate is ignored, so the transformation
+matrix simplifies to [a, b, c; 0, 0, 0]. It satisfies the three simultaneous
+equations:
+
+	a*(x1   ) + b*(y1   ) + c = 0   (eq L.0)
+	a*(x1+dy) + b*(y1-dx) + c = 0   (eq L.1)
+	a*(x1+dx) + b*(y1+dy) + c = 1   (eq L.2)
+
+Subtracting equation L.0 from equations L.1 and L.2 yields:
+
+	a*dy - b*dx = 0
+	a*dx + b*dy = 1
+
+So that
+
+	a*dy*dy - b*dx*dy = 0
+	a*dx*dx + b*dx*dy = dx
+
+Overall:
+
+	a = dx / (dx*dx + dy*dy)
+	b = dy / (dx*dx + dy*dy)
+	c = -a*x1 - b*y1
+	d = 0
+	e = 0
+	f = 0
+
+
+Circular Gradients
+
+For a circular gradient with center (cx, cy) and radius vector (rx, ry), such
+that (cx+rx, cy+ry) is on the circle, let
+
+	r = math.Sqrt(rx*rx + ry*ry)
+
+The transformation matrix maps (cx, cy) to (0, 0), maps (cx+r, cy) to (1, 0)
+and maps (cx, cy+r) to (0, 1). Solving those six simultaneous equations give:
+
+	a = +1  / r
+	b = +0  / r
+	c = -cx / r
+	d = +0  / r
+	e = +1  / r
+	f = -cy / r
+
+
+Elliptical Gradients
+
+For an elliptical gradient with center (cx, cy) and axis vectors (rx, ry) and
+(sx, sy), such that (cx+rx, cx+ry) and (cx+sx, cx+sy) are on the ellipse, the
+transformation matrix satisfies the six simultaneous equations:
+
+	a*(cx   ) + b*(cy   ) + c = 0   (eq E.0)
+	a*(cx+rx) + b*(cy+ry) + c = 1   (eq E.1)
+	a*(cx+sx) + b*(cy+sy) + c = 0   (eq E.2)
+	d*(cx   ) + e*(cy   ) + f = 0   (eq E.3)
+	d*(cx+rx) + e*(cy+ry) + f = 0   (eq E.4)
+	d*(cx+sx) + e*(cy+sy) + f = 1   (eq E.5)
+
+Subtracting equation E.0 from equations E.1 and E.2 yields:
+
+	a*rx + b*ry = 1
+	a*sx + b*sy = 0
+
+Solving these two simultaneous equations yields:
+
+	a = +sy / (rx*sy - sx*ry)
+	b = -sx / (rx*sy - sx*ry)
+
+Re-arranging E.0 yields:
+
+	c = -a*cx - b*cy
+
+Similarly for d, e and f so that, overall:
+
+	a = +sy / (rx*sy - sx*ry)
+	b = -sx / (rx*sy - sx*ry)
+	c = -a*cx - b*cy
+	d = -ry / (rx*sy - sx*ry)
+	e = +rx / (rx*sy - sx*ry)
+	f = -d*cx - e*cy
+
+Note that if rx = r, ry = 0, sx = 0 and sy = r then this simplifies to the
+circular gradient transformation matrix formula, above.
 */
 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 aa52041..ead030a 100644
--- a/shiny/iconvg/encode.go
+++ b/shiny/iconvg/encode.go
@@ -8,6 +8,8 @@
 	"errors"
 	"image/color"
 	"math"
+
+	"golang.org/x/image/math/f32"
 )
 
 var (
@@ -262,37 +264,30 @@
 	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.
+// SetGradient sets CREG[CSEL] to encode the gradient whose colors defined by
+// spread and stops. Its geometry is either linear or radial, depending on the
+// radial argument, and the given affine transformation matrix maps from
+// graphic coordinate space defined by the metadata's viewBox (e.g. from (-32,
+// -32) to (+32, +32)) to gradient coordinate space. Gradient coordinate space
+// is where a linear gradient ranges from x=0 to x=1, and a radial gradient has
+// center (0, 0) and radius 1.
 //
 // 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) {
+//
+// See the package documentation for more details on the gradient encoding
+// format and the derivation of common transformation matrices.
+func (e *Encoder) SetGradient(cBase, nBase uint8, radial bool, transform f32.Aff3, spread GradientSpread, stops []GradientStop) {
 	e.checkModeStyling()
 	if e.err != nil {
 		return
 	}
-	if len(stops) > 60 {
+	if len(stops) > 64-len(transform) {
 		e.err = errTooManyGradientStops
 		return
 	}
@@ -306,6 +301,10 @@
 	oldNSel := e.nSel
 	cBase &= 0x3f
 	nBase &= 0x3f
+	bFlags := uint8(0x80)
+	if radial {
+		bFlags = 0xc0
+	}
 	e.SetCReg(0, false, RGBAColor(color.RGBA{
 		R: uint8(len(stops)),
 		G: cBase | uint8(spread<<6),
@@ -314,10 +313,9 @@
 	}))
 	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 i, v := range transform {
+		e.SetNReg(uint8(len(transform)-i), false, v)
+	}
 	for _, s := range stops {
 		r, g, b, a := s.Color.RGBA()
 		e.SetCReg(0, true, RGBAColor(color.RGBA{
@@ -332,6 +330,57 @@
 	e.SetNSel(oldNSel)
 }
 
+// SetLinearGradient is like SetGradient with radial=false except that the
+// transformation matrix is implicitly defined by two boundary points (x1, y1)
+// and (x2, y2).
+func (e *Encoder) SetLinearGradient(cBase, nBase uint8, x1, y1, x2, y2 float32, spread GradientSpread, stops []GradientStop) {
+	// See the package documentation's appendix for a derivation of the
+	// transformation matrix.
+	dx, dy := x2-x1, y2-y1
+	d := dx*dx + dy*dy
+	ma := dx / d
+	mb := dy / d
+	e.SetGradient(cBase, nBase, false, f32.Aff3{
+		ma, mb, -ma*x1 - mb*y1,
+		0, 0, 0,
+	}, spread, stops)
+}
+
+// SetCircularGradient is like SetGradient with radial=true except that the
+// transformation matrix is implicitly defined by a center (cx, cy) and a
+// radius vector (rx, ry) such that (cx+rx, cy+ry) is on the circle.
+func (e *Encoder) SetCircularGradient(cBase, nBase uint8, cx, cy, rx, ry float32, spread GradientSpread, stops []GradientStop) {
+	// See the package documentation's appendix for a derivation of the
+	// transformation matrix.
+	invR := float32(1 / math.Sqrt(float64(rx*rx+ry*ry)))
+	e.SetGradient(cBase, nBase, true, f32.Aff3{
+		invR, 0, -cx * invR,
+		0, invR, -cy * invR,
+	}, spread, stops)
+}
+
+// SetEllipticalGradient is like SetGradient with radial=true except that the
+// transformation matrix is implicitly defined by a center (cx, cy) and two
+// axis vectors (rx, ry) and (sx, sy) such that (cx+rx, cy+ry) and (cx+sx,
+// cy+sy) are on the ellipse.
+func (e *Encoder) SetEllipticalGradient(cBase, nBase uint8, cx, cy, rx, ry, sx, sy float32, spread GradientSpread, stops []GradientStop) {
+	// See the package documentation's appendix for a derivation of the
+	// transformation matrix.
+	invRSSR := 1 / (rx*sy - sx*ry)
+
+	ma := +sy * invRSSR
+	mb := -sx * invRSSR
+	mc := -ma*cx - mb*cy
+	md := -ry * invRSSR
+	me := +rx * invRSSR
+	mf := -md*cx - me*cy
+
+	e.SetGradient(cBase, nBase, true, f32.Aff3{
+		ma, mb, mc,
+		md, me, mf,
+	}, spread, stops)
+}
+
 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 8766d21..94f3e0f 100644
--- a/shiny/iconvg/encode_test.go
+++ b/shiny/iconvg/encode_test.go
@@ -130,6 +130,171 @@
 	testEncode(t, &e, "testdata/arcs.ivg")
 }
 
+var cowbellGradients = []struct {
+	radial bool
+
+	// Linear gradient coefficients.
+	x1, y1 float32
+	x2, y2 float32
+	tx, ty float32
+
+	// Radial gradient coefficients.
+	cx, cy, r float32
+	transform f32.Aff3
+
+	stops []GradientStop
+}{{
+// The 0th element is unused.
+}, {
+	radial: true,
+	cx:     -102.14,
+	cy:     20.272,
+	r:      18.012,
+	transform: f32.Aff3{
+		.33050, -.50775, 65.204,
+		.17296, .97021, 16.495,
+	},
+	stops: []GradientStop{
+		{Offset: 0, Color: color.RGBA{0xed, 0xd4, 0x00, 0xff}},
+		{Offset: 1, Color: color.RGBA{0xfc, 0xe9, 0x4f, 0xff}},
+	},
+}, {
+	radial: true,
+	cx:     -97.856,
+	cy:     26.719,
+	r:      18.61,
+	transform: f32.Aff3{
+		.35718, -.11527, 51.072,
+		.044280, .92977, 7.6124,
+	},
+	stops: []GradientStop{
+		{Offset: 0, Color: color.RGBA{0xed, 0xd4, 0x00, 0xff}},
+		{Offset: 1, Color: color.RGBA{0xfc, 0xe9, 0x4f, 0xff}},
+	},
+}, {
+	x1: -16.183,
+	y1: 35.723,
+	x2: -18.75,
+	y2: 29.808,
+	tx: 48.438,
+	ty: -.22321,
+	stops: []GradientStop{
+		{Offset: 0, Color: color.RGBA{0x39, 0x21, 0x00, 0xff}},
+		{Offset: 1, Color: color.RGBA{0x0f, 0x08, 0x00, 0xff}},
+	},
+}}
+
+var cowbellSVGData = []struct {
+	rgba      color.RGBA
+	gradient  int
+	d         string
+	transform *f32.Aff3
+}{{
+	gradient: 2,
+	d:        "m5.6684 17.968l.265-4.407 13.453 19.78.301 8.304-14.019-23.677z",
+}, {
+	gradient: 1,
+	d:        "m19.299 33.482l-13.619-19.688 3.8435-2.684.0922-2.1237 4.7023-2.26 2.99 1.1274 4.56-1.4252 20.719 16.272-23.288 10.782z",
+}, {
+	rgba: color.RGBA{0xfd * 127 / 255, 0xee * 127 / 255, 0x74 * 127 / 255, 127},
+	d:    "m19.285 32.845l-13.593-19.079 3.995-2.833.1689-2.0377 1.9171-.8635 18.829 18.965-11.317 5.848z",
+}, {
+	rgba: color.RGBA{0xc4, 0xa0, 0x00, 0xff},
+	d:    "m19.211 40.055c-.11-.67-.203-2.301-.205-3.624l-.003-2.406-2.492-3.769c-3.334-5.044-11.448-17.211-9.6752-14.744.3211.447 1.6961 2.119 2.1874 2.656.4914.536 1.3538 1.706 1.9158 2.6 2.276 3.615 8.232 12.056 8.402 12.056.1 0 10.4-5.325 11.294-5.678.894-.354 11.25-4.542 11.45-4.342.506.506 1.27 7.466.761 8.08-.392.473-5.06 3.672-10.256 6.121-5.195 2.45-11.984 4.269-12.594 4.269-.421 0-.639-.338-.785-1.219z",
+}, {
+	gradient: 3,
+	d:        "m19.825 33.646c.422-.68 10.105-5.353 10.991-5.753s9.881-4.123 10.468-4.009c.512.099.844 6.017.545 6.703-.23.527-8.437 4.981-9.516 5.523-1.225.616-11.642 4.705-12.145 4.369-.553-.368-.707-6.245-.343-6.833z",
+}, {
+	rgba: color.RGBA{0x00, 0x00, 0x00, 0xff},
+	d:    "m21.982 5.8789-4.865 1.457-2.553-1.1914-5.3355 2.5743l-.015625.29688-.097656 1.8672-4.1855 2.7383.36719 4.5996.054687.0957s3.2427 5.8034 6.584 11.654c1.6707 2.9255 3.3645 5.861 4.6934 8.0938.66442 1.1164 1.2366 2.0575 1.6719 2.7363.21761.33942.40065.6121.54883.81641.07409.10215.13968.18665.20312.25976.06345.07312.07886.13374.27148.22461.27031.12752.38076.06954.54102.04883.16025-.02072.34015-.05724.55078-.10938.42126-.10427.95998-.26728 1.584-.4707 1.248-.40685 2.8317-.97791 4.3926-1.5586 3.1217-1.1614 6.1504-2.3633 6.1504-2.3633l.02539-.0098.02539-.01367s2.5368-1.3591 5.1211-2.8027c1.2922-.72182 2.5947-1.4635 3.6055-2.0723.50539-.30438.93732-.57459 1.2637-.79688.16318-.11114.29954-.21136.41211-.30273.11258-.09138.19778-.13521.30273-.32617.16048-.292.13843-.48235.1543-.78906s.01387-.68208.002-1.1094c-.02384-.8546-.09113-1.9133-.17188-2.9473-.161-2.067-.373-4.04-.373-4.04l-.021-.211-20.907-16.348zm-.209 1.1055 20.163 15.766c.01984.1875.19779 1.8625.34961 3.8066.08004 1.025.14889 2.0726.17188 2.8965.01149.41192.01156.76817-.002 1.0293-.01351.26113-.09532.47241-.0332.35938.05869-.10679.01987-.0289-.05664.0332s-.19445.14831-.34375.25c-.29859.20338-.72024.46851-1.2168.76758-.99311.59813-2.291 1.3376-3.5781 2.0566-2.5646 1.4327-5.0671 2.7731-5.0859 2.7832-.03276.01301-3.0063 1.1937-6.0977 2.3438-1.5542.5782-3.1304 1.1443-4.3535 1.543-.61154.19936-1.1356.35758-1.5137.45117-.18066.04472-.32333.07255-.41992.08594-.02937-.03686-.05396-.06744-.0957-.125-.128-.176-.305-.441-.517-.771-.424-.661-.993-1.594-1.655-2.705-1.323-2.223-3.016-5.158-4.685-8.08-3.3124-5.8-6.4774-11.465-6.5276-11.555l-.3008-3.787 4.1134-2.692.109-2.0777 4.373-2.1133 2.469 1.1523 4.734-1.4179z",
+}}
+
+func inv(x *f32.Aff3) f32.Aff3 {
+	invDet := 1 / (x[0]*x[4] - x[1]*x[3])
+	return f32.Aff3{
+		+x[4] * invDet,
+		-x[1] * invDet,
+		(x[1]*x[5] - x[2]*x[4]) * invDet,
+		-x[3] * invDet,
+		+x[0] * invDet,
+		(x[2]*x[3] - x[0]*x[5]) * invDet,
+	}
+}
+
+func TestEncodeCowbell(t *testing.T) {
+	var e Encoder
+	e.Reset(Metadata{
+		ViewBox: Rectangle{
+			Min: f32.Vec2{0, 0},
+			Max: f32.Vec2{+48, +48},
+		},
+		Palette: DefaultPalette,
+	})
+
+	for _, data := range cowbellSVGData {
+		if data.rgba != (color.RGBA{}) {
+			e.SetCReg(0, false, RGBAColor(data.rgba))
+		} else if data.gradient != 0 {
+			g := cowbellGradients[data.gradient]
+			if g.radial {
+				iform := inv(&g.transform)
+				iform[2] -= g.cx
+				iform[5] -= g.cy
+				for i := range iform {
+					iform[i] /= g.r
+				}
+				e.SetGradient(10, 10, true, iform, GradientSpreadPad, g.stops)
+			} else {
+				x1 := g.x1 + g.tx
+				y1 := g.y1 + g.ty
+				x2 := g.x2 + g.tx
+				y2 := g.y2 + g.ty
+				e.SetLinearGradient(10, 10, x1, y1, x2, y2, GradientSpreadPad, g.stops)
+			}
+		}
+
+		if err := encodePathData(&e, data.d, 0, false); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	testEncode(t, &e, "testdata/cowbell.ivg")
+}
+
+func TestEncodeElliptical(t *testing.T) {
+	var e Encoder
+
+	const (
+		cx, cy = -20, -10
+		rx, ry = 0, 24
+		sx, sy = 30, 15
+	)
+
+	e.SetEllipticalGradient(10, 10, cx, cy, rx, ry, sx, sy, GradientSpreadReflect, []GradientStop{
+		{Offset: 0, Color: color.RGBA{0xc0, 0x00, 0x00, 0xff}},
+		{Offset: 1, Color: color.RGBA{0x00, 0x00, 0xc0, 0xff}},
+	})
+	e.StartPath(0, -32, -32)
+	e.AbsHLineTo(+32)
+	e.AbsVLineTo(+32)
+	e.AbsHLineTo(-32)
+	e.ClosePathEndPath()
+
+	e.SetCReg(0, false, RGBAColor(color.RGBA{0xff, 0xff, 0xff, 0xff}))
+	diamond := func(x, y float32) {
+		e.StartPath(0, x-1, y)
+		e.AbsLineTo(x, y-1)
+		e.AbsLineTo(x+1, y)
+		e.AbsLineTo(x, y+1)
+		e.ClosePathEndPath()
+	}
+	diamond(cx, cy)
+	diamond(cx+rx, cy+ry)
+	diamond(cx+sx, cy+sy)
+
+	testEncode(t, &e, "testdata/elliptical.ivg")
+}
+
 var faviconColors = []color.RGBA{
 	{0x76, 0xe1, 0xfe, 0xff},
 	{0x38, 0x4e, 0x54, 0xff},
@@ -206,60 +371,77 @@
 			adj -= 2
 		}
 
-		var args [7]float32
-		prevN, prevVerb := 0, byte(0)
-		for d, first := data.d, true; d[0] != 'z'; first = false {
-			n, verb, implicit := 0, d[0], false
-			switch d[0] {
-			case 'H', 'h', 'V', 'v':
-				n = 1
-			case 'L', 'M', 'l', 'm':
-				n = 2
-			case 'S', 's':
-				n = 4
-			case 'C', 'c':
-				n = 6
-			case 'A', 'a':
-				n = 7
-			default:
-				if prevVerb == '\x00' {
-					panic("unrecognized verb")
-				}
-				n, verb, implicit = prevN, prevVerb, true
-			}
-			prevN, prevVerb = n, verb
-			if !implicit {
-				d = d[1:]
-			}
+		if err := encodePathData(&e, data.d, adj, true); err != nil {
+			t.Fatal(err)
+		}
+	}
 
-			for i := 0; i < n; i++ {
-				nDots := 0
-				if d[0] == '.' {
-					nDots = 1
-				}
-				j := 1
-				for ; ; j++ {
-					switch d[j] {
-					case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+	testEncode(t, &e, "testdata/favicon.ivg")
+}
+
+func encodePathData(e *Encoder, d string, adj uint8, normalizeTo64X64 bool) error {
+	var args [7]float32
+	prevN, prevVerb := 0, byte(0)
+	for first := true; d != "z"; first = false {
+		n, verb, implicit := 0, d[0], false
+		switch d[0] {
+		case 'H', 'h', 'V', 'v':
+			n = 1
+		case 'L', 'M', 'l', 'm':
+			n = 2
+		case 'S', 's':
+			n = 4
+		case 'C', 'c':
+			n = 6
+		case 'A', 'a':
+			n = 7
+		case 'z':
+			n = 0
+		default:
+			if prevVerb == '\x00' {
+				panic("unrecognized verb")
+			}
+			n, verb, implicit = prevN, prevVerb, true
+		}
+		prevN, prevVerb = n, verb
+		if prevVerb == 'M' {
+			prevVerb = 'L'
+		} else if prevVerb == 'm' {
+			prevVerb = 'l'
+		}
+		if !implicit {
+			d = d[1:]
+		}
+
+		for i := 0; i < n; i++ {
+			nDots := 0
+			if d[0] == '.' {
+				nDots = 1
+			}
+			j := 1
+			for ; ; j++ {
+				switch d[j] {
+				case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+					continue
+				case '.':
+					nDots++
+					if nDots == 1 {
 						continue
-					case '.':
-						nDots++
-						if nDots == 1 {
-							continue
-						}
 					}
-					break
 				}
-				f, err := strconv.ParseFloat(d[:j], 64)
-				if err != nil {
-					t.Fatal(err)
-				}
-				args[i] = float32(f)
-				for ; d[j] == ' ' || d[j] == ','; j++ {
-				}
-				d = d[j:]
+				break
 			}
+			f, err := strconv.ParseFloat(d[:j], 64)
+			if err != nil {
+				return err
+			}
+			args[i] = float32(f)
+			for ; d[j] == ' ' || d[j] == ','; j++ {
+			}
+			d = d[j:]
+		}
 
+		if normalizeTo64X64 {
 			// The original SVG is 32x32 units, with the top left being (0, 0).
 			// Normalize to 64x64 units, with the center being (0, 0).
 			if verb == 'A' {
@@ -283,45 +465,50 @@
 					args[i] = 2 * args[i]
 				}
 			}
-
-			if first {
-				first = false
-				e.StartPath(adj, args[0], args[1])
-				continue
-			}
-			switch verb {
-			case 'H':
-				e.AbsHLineTo(args[0])
-			case 'h':
-				e.RelHLineTo(args[0])
-			case 'V':
-				e.AbsVLineTo(args[0])
-			case 'v':
-				e.RelVLineTo(args[0])
-			case 'L':
-				e.AbsLineTo(args[0], args[1])
-			case 'l':
-				e.RelLineTo(args[0], args[1])
-			case 'S':
-				e.AbsSmoothCubeTo(args[0], args[1], args[2], args[3])
-			case 's':
-				e.RelSmoothCubeTo(args[0], args[1], args[2], args[3])
-			case 'C':
-				e.AbsCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
-			case 'c':
-				e.RelCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
-			case 'A':
-				e.AbsArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
-			case 'a':
-				e.RelArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
-			default:
-				panic("unrecognized verb")
-			}
+		} else if verb == 'A' || verb == 'a' {
+			args[2] /= 360
 		}
-		e.ClosePathEndPath()
-	}
 
-	testEncode(t, &e, "testdata/favicon.ivg")
+		if first {
+			first = false
+			e.StartPath(adj, args[0], args[1])
+			continue
+		}
+		switch verb {
+		case 'H':
+			e.AbsHLineTo(args[0])
+		case 'h':
+			e.RelHLineTo(args[0])
+		case 'V':
+			e.AbsVLineTo(args[0])
+		case 'v':
+			e.RelVLineTo(args[0])
+		case 'L':
+			e.AbsLineTo(args[0], args[1])
+		case 'l':
+			e.RelLineTo(args[0], args[1])
+		case 'm':
+			e.ClosePathRelMoveTo(args[0], args[1])
+		case 'S':
+			e.AbsSmoothCubeTo(args[0], args[1], args[2], args[3])
+		case 's':
+			e.RelSmoothCubeTo(args[0], args[1], args[2], args[3])
+		case 'C':
+			e.AbsCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
+		case 'c':
+			e.RelCubeTo(args[0], args[1], args[2], args[3], args[4], args[5])
+		case 'A':
+			e.AbsArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
+		case 'a':
+			e.RelArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6])
+		case 'z':
+			// No-op.
+		default:
+			panic("unrecognized verb")
+		}
+	}
+	e.ClosePathEndPath()
+	return nil
 }
 
 func TestEncodeGradient(t *testing.T) {
@@ -355,14 +542,14 @@
 	e.AbsHLineTo(-30)
 	e.ClosePathEndPath()
 
-	e.SetRadialGradient(10, 10, -8, 8, 16, GradientSpreadReflect, rgb)
+	e.SetCircularGradient(10, 10, -8, 8, 0, 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.SetCircularGradient(10, 10, -8, 24, 0, 16, GradientSpreadRepeat, cmy)
 	e.StartPath(0, -30, +18)
 	e.AbsHLineTo(+30)
 	e.AbsVLineTo(+30)
diff --git a/shiny/iconvg/internal/gradient/gradient.go b/shiny/iconvg/internal/gradient/gradient.go
index 3baa944..92446e7 100644
--- a/shiny/iconvg/internal/gradient/gradient.go
+++ b/shiny/iconvg/internal/gradient/gradient.go
@@ -21,7 +21,9 @@
 // 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.
+// 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
@@ -153,10 +155,6 @@
 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
@@ -168,10 +166,19 @@
 	//
 	// 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
 }
 
-func (g *Gradient) init(spread Spread, stops []Stop) {
+// 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{}
@@ -182,50 +189,6 @@
 	}
 }
 
-// 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
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
index da06ec0..342a9c9 100644
--- a/shiny/iconvg/rasterizer.go
+++ b/shiny/iconvg/rasterizer.go
@@ -12,6 +12,7 @@
 
 	"golang.org/x/exp/shiny/iconvg/internal/gradient"
 	"golang.org/x/image/math/f32"
+	"golang.org/x/image/math/f64"
 	"golang.org/x/image/vector"
 )
 
@@ -186,31 +187,43 @@
 		}
 	}
 
-	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])
+	// The affine transformation matrix in the IconVG graphic, stored in 6
+	// contiguous NREG registers, goes from graphic coordinate space (i.e. the
+	// metadata viewBox) to the gradient coordinate space. We need it to start
+	// in pixel space, not graphic coordinate space.
 
-		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],
-		)
+	invZSX := 1 / float64(z.scaleX)
+	invZSY := 1 / float64(z.scaleY)
+	zBX := float64(z.biasX)
+	zBY := float64(z.biasY)
+
+	a := float64(z.nReg[(nBase-6)&0x3f])
+	b := float64(z.nReg[(nBase-5)&0x3f])
+	c := float64(z.nReg[(nBase-4)&0x3f])
+	d := float64(z.nReg[(nBase-3)&0x3f])
+	e := float64(z.nReg[(nBase-2)&0x3f])
+	f := float64(z.nReg[(nBase-1)&0x3f])
+
+	pix2Grad := f64.Aff3{
+		a * invZSX,
+		b * invZSY,
+		c - a*zBX - b*zBY,
+		d * invZSX,
+		e * invZSY,
+		f - d*zBX - e*zBY,
 	}
+
+	shape := gradient.ShapeLinear
+	if (rgba.B>>6)&0x01 != 0 {
+		shape = gradient.ShapeRadial
+	}
+	z.gradient.Init(
+		shape,
+		gradient.Spread(rgba.G>>6),
+		pix2Grad,
+		z.stops[:nStops],
+	)
+
 	return true
 }
 
diff --git a/shiny/iconvg/testdata/README b/shiny/iconvg/testdata/README
index 329454d..21c5ee6 100644
--- a/shiny/iconvg/testdata/README
+++ b/shiny/iconvg/testdata/README
@@ -32,6 +32,24 @@
 
 
 
+cowbell.svg is an original artwork by nigeltao@golang.org.
+
+cowbell.ivg is an IconVG version of that SVG file.
+
+cowbell.ivg.disassembly is a disassembly of that IconVG file.
+
+cowbell.png is a rendering of that IconVG file.
+
+
+
+elliptical.ivg was created manually.
+
+elliptical.ivg.disassembly is a disassembly of that IconVG file.
+
+elliptical.png is a rendering of that IconVG file.
+
+
+
 favicon.svg is based on doc/gopher/favicon.svg from the Go 1.7 release, after
 using Inkscape to convert strokes and circles to paths, and saving it as an
 "Optimized SVG".
@@ -66,7 +84,7 @@
 video-005.primitive.svg was based on running github.com/fogleman/primitive on a
 256x192 scaled version of video-005.jpeg.
 
-video-005.primitive.ivg is an IconVG version of video-005.primitive.svg.
+video-005.primitive.ivg is an IconVG version of that SVG file.
 
 video-005.primitive.ivg.disassembly is a disassembly of that IconVG file.
 
diff --git a/shiny/iconvg/testdata/cowbell.ivg b/shiny/iconvg/testdata/cowbell.ivg
new file mode 100644
index 0000000..f98ce1f
--- /dev/null
+++ b/shiny/iconvg/testdata/cowbell.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/cowbell.ivg.disassembly b/shiny/iconvg/testdata/cowbell.ivg.disassembly
new file mode 100644
index 0000000..a97bfd6
--- /dev/null
+++ b/shiny/iconvg/testdata/cowbell.ivg.disassembly
@@ -0,0 +1,611 @@
+89 49 56 47   IconVG Magic identifier
+02            Number of metadata chunks: 1
+0a            Metadata chunk length: 5
+00            Metadata Identifier: 0 (viewBox)
+80                +0
+80                +0
+e0                +48
+e0                +48
+98            Set CREG[CSEL-0] to a 4 byte color
+02 4a ca 00       gradient (NSTOPS=2, CBASE=10, NBASE=10, radial, pad)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+ae            Set NREG[NSEL-6] to a real number
+4b b8 17 3e       0.14816391
+ad            Set NREG[NSEL-5] to a real number
+63 7a 96 3c       0.0183689
+ac            Set NREG[NSEL-4] to a real number
+03 b6 1c c0       -2.4486084
+ab            Set NREG[NSEL-3] to a real number
+33 38 e7 bb       -0.0070562586
+aa            Set NREG[NSEL-2] to a real number
+73 23 69 3d       0.05691856
+a9            Set NREG[NSEL-1] to a real number
+3b 1b c1 bf       -1.5086432
+97            Set CREG[CSEL-0] to a 3 byte (direct) color; CSEL++
+ed d4 00          RGBA edd400ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+97            Set CREG[CSEL-0] to a 3 byte (direct) color; CSEL++
+fc e9 4f          RGBA fce94fff
+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)
+ad 85             +5.671875
+f9 91             +17.96875
+23            l (relative lineTo), 4 reps
+45 80             +0.265625
+99 7b             -4.40625
+              l (relative lineTo), implicit
+75 8d             +13.453125
+c9 93             +19.78125
+              l (relative lineTo), implicit
+4d 80             +0.296875
+4d 88             +8.296875
+              l (relative lineTo), implicit
+fd 71             -14.015625
+55 68             -23.671875
+e1            z (closePath); end path
+98            Set CREG[CSEL-0] to a 4 byte color
+02 4a ca 00       gradient (NSTOPS=2, CBASE=10, NBASE=10, radial, pad)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+ae            Set NREG[NSEL-6] to a real number
+57 08 07 3e       0.1318677
+ad            Set NREG[NSEL-5] to a real number
+03 56 8d 3d       0.06901169
+ac            Set NREG[NSEL-4] to a real number
+93 1c 82 c0       -4.0659866
+ab            Set NREG[NSEL-3] to a real number
+2b 94 c0 bc       -0.023508146
+aa            Set NREG[NSEL-2] to a real number
+87 fe 37 3d       0.04492046
+a9            Set NREG[NSEL-1] to a real number
+e3 ce aa be       -0.33360958
+97            Set CREG[CSEL-0] to a 3 byte (direct) color; CSEL++
+ed d4 00          RGBA edd400ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+97            Set CREG[CSEL-0] to a 3 byte (direct) color; CSEL++
+fc e9 4f          RGBA fce94fff
+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)
+4d 93             +19.296875
+7d a1             +33.484375
+27            l (relative lineTo), 8 reps
+61 72             -13.625
+51 6c             -19.6875
+              l (relative lineTo), implicit
+d9 83             +3.84375
+51 7d             -2.6875
+              l (relative lineTo), implicit
+19 80             +0.09375
+e1 7d             -2.125
+              l (relative lineTo), implicit
+b5 84             +4.703125
+bd 7d             -2.265625
+              l (relative lineTo), implicit
+fd 82             +2.984375
+21 81             +1.125
+              l (relative lineTo), implicit
+91 84             +4.5625
+95 7e             -1.421875
+              l (relative lineTo), implicit
+b9 94             +20.71875
+45 90             +16.265625
+              l (relative lineTo), implicit
+b9 68             -23.28125
+c9 8a             +10.78125
+e1            z (closePath); end path
+98            Set CREG[CSEL-0] to a 4 byte color
+7e 76 39 7f       RGBA 7e76397f
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+49 93             +19.28125
+d9 a0             +32.84375
+25            l (relative lineTo), 6 reps
+69 72             -13.59375
+ed 6c             -19.078125
+              l (relative lineTo), implicit
+88                +4
+2d 7d             -2.828125
+              l (relative lineTo), implicit
+2d 80             +0.171875
+f9 7d             -2.03125
+              l (relative lineTo), implicit
+ed 81             +1.921875
+25 7f             -0.859375
+              l (relative lineTo), implicit
+d5 92             +18.828125
+f9 92             +18.96875
+              l (relative lineTo), implicit
+b1 74             -11.3125
+d9 85             +5.84375
+e1            z (closePath); end path
+90            Set CREG[CSEL-0] to a 3 byte (direct) color
+c4 a0 00          RGBA c4a000ff
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+39 93             +19.21875
+11 a8             +40.0625
+b0            c (relative cubeTo), 1 reps
+e5 7f             -0.109375
+55 7f             -0.671875
+cd 7f             -0.203125
+b5 7d             -2.296875
+cd 7f             -0.203125
+61 7c             -3.625
+21            l (relative lineTo), 2 reps
+80                +0
+99 7d             -2.40625
+              l (relative lineTo), implicit
+85 7d             -2.484375
+3d 7c             -3.765625
+b9            c (relative cubeTo), 10 reps
+ad 7c             -3.328125
+f5 7a             -5.046875
+8d 74             -11.453125
+c9 6e             -17.21875
+55 76             -9.671875
+41 71             -14.75
+              c (relative cubeTo), implicit
+55 80             +0.328125
+75 80             +0.453125
+b5 81             +1.703125
+21 82             +2.125
+31 82             +2.1875
+a9 82             +2.65625
+              c (relative cubeTo), implicit
+7d 80             +0.484375
+89 80             +0.53125
+5d 81             +1.359375
+b5 81             +1.703125
+ed 81             +1.921875
+99 82             +2.59375
+              c (relative cubeTo), implicit
+49 82             +2.28125
+9d 83             +3.609375
+3d 88             +8.234375
+11 8c             +12.0625
+69 88             +8.40625
+11 8c             +12.0625
+              c (relative cubeTo), implicit
+19 80             +0.09375
+80                +0
+69 8a             +10.40625
+ad 7a             -5.328125
+4d 8b             +11.296875
+55 7a             -5.671875
+              c (relative cubeTo), implicit
+e5 80             +0.890625
+a5 7f             -0.359375
+41 8b             +11.25
+75 7b             -4.546875
+75 8b             +11.453125
+a9 7b             -4.34375
+              c (relative cubeTo), implicit
+81 80             +0.5
+81 80             +0.5
+45 81             +1.265625
+79 87             +7.46875
+c5 80             +0.765625
+15 88             +8.078125
+              c (relative cubeTo), implicit
+9d 7f             -0.390625
+79 80             +0.46875
+f1 7a             -5.0625
+ad 83             +3.671875
+c1 75             -10.25
+21 86             +6.125
+              c (relative cubeTo), implicit
+d1 7a             -5.1875
+75 82             +2.453125
+05 74             -11.984375
+45 84             +4.265625
+69 73             -12.59375
+45 84             +4.265625
+              c (relative cubeTo), implicit
+95 7f             -0.421875
+80                +0
+5d 7f             -0.640625
+a9 7f             -0.34375
+39 7f             -0.78125
+c9 7e             -1.21875
+e1            z (closePath); end path
+98            Set CREG[CSEL-0] to a 4 byte color
+02 4a 8a 00       gradient (NSTOPS=2, CBASE=10, NBASE=10, linear, pad)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+ae            Set NREG[NSEL-6] to a real number
+6b e4 7c bd       -0.061741263
+ad            Set NREG[NSEL-5] to a real number
+83 ae 11 be       -0.14226723
+ac            Set NREG[NSEL-4] to a real number
+6b 57 e1 40       7.0419197
+ab            Set NREG[NSEL-3] to a real number
+00                0
+aa            Set NREG[NSEL-2] to a real number
+00                0
+a9            Set NREG[NSEL-1] to a real number
+00                0
+97            Set CREG[CSEL-0] to a 3 byte (direct) color; CSEL++
+39 21 00          RGBA 392100ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+97            Set CREG[CSEL-0] to a 3 byte (direct) color; CSEL++
+0f 08 00          RGBA 0f0800ff
+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)
+d5 93             +19.828125
+a5 a1             +33.640625
+b0            c (relative cubeTo), 1 reps
+6d 80             +0.421875
+51 7f             -0.6875
+1d 8a             +10.109375
+a5 7a             -5.359375
+fd 8a             +10.984375
+41 7a             -5.75
+90            s (relative smooth cubeTo), 1 reps
+e1 89             +9.875
+e1 7b             -4.125
+79 8a             +10.46875
+fd 7b             -4.015625
+b3            c (relative cubeTo), 4 reps
+85 80             +0.515625
+19 80             +0.09375
+d9 80             +0.84375
+05 86             +6.015625
+8d 80             +0.546875
+b5 86             +6.703125
+              c (relative cubeTo), implicit
+c5 7f             -0.234375
+89 80             +0.53125
+91 77             -8.4375
+fd 84             +4.984375
+7d 76             -9.515625
+85 85             +5.515625
+              c (relative cubeTo), implicit
+c9 7e             -1.21875
+9d 80             +0.609375
+5d 74             -11.640625
+b5 84             +4.703125
+dd 73             -12.140625
+61 84             +4.375
+              c (relative cubeTo), implicit
+75 7f             -0.546875
+a1 7f             -0.375
+4d 7f             -0.703125
+c1 79             -6.25
+a9 7f             -0.34375
+2d 79             -6.828125
+e1            z (closePath); end path
+80            Set CREG[CSEL-0] to a 1 byte color
+00                RGBA 000000ff
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+fd 95             +21.984375
+e1 85             +5.875
+27            l (relative lineTo), 8 reps
+25 7b             -4.859375
+75 81             +1.453125
+              l (relative lineTo), implicit
+75 7d             -2.546875
+d1 7e             -1.1875
+              l (relative lineTo), implicit
+ad 7a             -5.328125
+95 82             +2.578125
+              l (relative lineTo), implicit
+fd 7f             -0.015625
+4d 80             +0.296875
+              l (relative lineTo), implicit
+e9 7f             -0.09375
+e1 81             +1.875
+              l (relative lineTo), implicit
+d1 7b             -4.1875
+bd 82             +2.734375
+              l (relative lineTo), implicit
+61 80             +0.375
+99 84             +4.59375
+              l (relative lineTo), implicit
+0d 80             +0.046875
+19 80             +0.09375
+90            s (relative smooth cubeTo), 1 reps
+41 83             +3.25
+cd 85             +5.796875
+95 86             +6.578125
+a9 8b             +11.65625
+b9            c (relative cubeTo), 10 reps
+ad 81             +1.671875
+ed 82             +2.921875
+5d 83             +3.359375
+dd 85             +5.859375
+b1 84             +4.6875
+19 88             +8.09375
+              c (relative cubeTo), implicit
+ad 80             +0.671875
+1d 81             +1.109375
+3d 81             +1.234375
+11 82             +2.0625
+ad 81             +1.671875
+bd 82             +2.734375
+              c (relative cubeTo), implicit
+39 80             +0.21875
+59 80             +0.34375
+69 80             +0.40625
+9d 80             +0.609375
+8d 80             +0.546875
+d1 80             +0.8125
+              c (relative cubeTo), implicit
+15 80             +0.078125
+1d 80             +0.109375
+25 80             +0.140625
+31 80             +0.1875
+35 80             +0.203125
+45 80             +0.265625
+              c (relative cubeTo), implicit
+11 80             +0.0625
+15 80             +0.078125
+15 80             +0.078125
+25 80             +0.140625
+45 80             +0.265625
+39 80             +0.21875
+              c (relative cubeTo), implicit
+45 80             +0.265625
+21 80             +0.125
+61 80             +0.375
+11 80             +0.0625
+8d 80             +0.546875
+0d 80             +0.046875
+              c (relative cubeTo), implicit
+29 80             +0.15625
+fd 7f             -0.015625
+59 80             +0.34375
+f1 7f             -0.0625
+8d 80             +0.546875
+e5 7f             -0.109375
+              c (relative cubeTo), implicit
+6d 80             +0.421875
+e5 7f             -0.109375
+f5 80             +0.953125
+bd 7f             -0.265625
+95 81             +1.578125
+89 7f             -0.46875
+              c (relative cubeTo), implicit
+41 81             +1.25
+99 7f             -0.40625
+d5 82             +2.828125
+05 7f             -0.984375
+65 84             +4.390625
+71 7e             -1.5625
+              c (relative cubeTo), implicit
+21 83             +3.125
+d9 7e             -1.15625
+29 86             +6.15625
+a5 7d             -2.359375
+29 86             +6.15625
+a5 7d             -2.359375
+21            l (relative lineTo), 2 reps
+09 80             +0.03125
+fd 7f             -0.015625
+              l (relative lineTo), implicit
+09 80             +0.03125
+fd 7f             -0.015625
+90            s (relative smooth cubeTo), 1 reps
+89 82             +2.53125
+a5 7e             -1.359375
+21 85             +5.125
+35 7d             -2.796875
+b4            c (relative cubeTo), 5 reps
+4d 81             +1.296875
+49 7f             -0.71875
+99 82             +2.59375
+89 7e             -1.46875
+9d 83             +3.609375
+ed 7d             -2.078125
+              c (relative cubeTo), implicit
+81 80             +0.5
+b5 7f             -0.296875
+f1 80             +0.9375
+6d 7f             -0.578125
+45 81             +1.265625
+35 7f             -0.796875
+              c (relative cubeTo), implicit
+29 80             +0.15625
+e5 7f             -0.109375
+4d 80             +0.296875
+c9 7f             -0.21875
+69 80             +0.40625
+b5 7f             -0.296875
+              c (relative cubeTo), implicit
+1d 80             +0.109375
+e9 7f             -0.09375
+35 80             +0.203125
+dd 7f             -0.140625
+4d 80             +0.296875
+ad 7f             -0.328125
+              c (relative cubeTo), implicit
+29 80             +0.15625
+b5 7f             -0.296875
+25 80             +0.140625
+85 7f             -0.484375
+29 80             +0.15625
+39 7f             -0.78125
+90            s (relative smooth cubeTo), 1 reps
+05 80             +0.015625
+51 7f             -0.6875
+80                +0
+e5 7e             -1.109375
+b1            c (relative cubeTo), 2 reps
+f9 7f             -0.03125
+25 7f             -0.859375
+e9 7f             -0.09375
+19 7e             -1.90625
+d5 7f             -0.171875
+0d 7d             -2.953125
+              c (relative cubeTo), implicit
+d9 7f             -0.15625
+f1 7d             -2.0625
+a1 7f             -0.375
+f5 7b             -4.046875
+a1 7f             -0.375
+f5 7b             -4.046875
+21            l (relative lineTo), 2 reps
+fd 7f             -0.015625
+c9 7f             -0.21875
+              l (relative lineTo), implicit
+19 6b             -20.90625
+a9 6f             -16.34375
+e3            z (closePath); m (relative moveTo)
+cd 7f             -0.203125
+1d 81             +1.109375
+20            l (relative lineTo), 1 reps
+29 94             +20.15625
+c5 8f             +15.765625
+b4            c (relative cubeTo), 5 reps
+05 80             +0.015625
+31 80             +0.1875
+35 80             +0.203125
+dd 81             +1.859375
+59 80             +0.34375
+d1 83             +3.8125
+              c (relative cubeTo), implicit
+15 80             +0.078125
+09 81             +1.03125
+29 80             +0.15625
+15 82             +2.078125
+2d 80             +0.171875
+e5 82             +2.890625
+              c (relative cubeTo), implicit
+05 80             +0.015625
+69 80             +0.40625
+05 80             +0.015625
+c5 80             +0.765625
+80                +0
+09 81             +1.03125
+              c (relative cubeTo), implicit
+fd 7f             -0.015625
+45 80             +0.265625
+e9 7f             -0.09375
+79 80             +0.46875
+f9 7f             -0.03125
+5d 80             +0.359375
+              c (relative cubeTo), implicit
+11 80             +0.0625
+e5 7f             -0.109375
+05 80             +0.015625
+f9 7f             -0.03125
+f1 7f             -0.0625
+09 80             +0.03125
+90            s (relative smooth cubeTo), 1 reps
+d1 7f             -0.1875
+25 80             +0.140625
+a9 7f             -0.34375
+41 80             +0.25
+bb            c (relative cubeTo), 12 reps
+b5 7f             -0.296875
+35 80             +0.203125
+49 7f             -0.71875
+79 80             +0.46875
+c9 7e             -1.21875
+c5 80             +0.765625
+              c (relative cubeTo), implicit
+7e                -1
+99 80             +0.59375
+b5 7d             -2.296875
+59 81             +1.34375
+6d 7c             -3.578125
+11 82             +2.0625
+              c (relative cubeTo), implicit
+71 7d             -2.5625
+71 81             +1.4375
+f1 7a             -5.0625
+c5 82             +2.765625
+ed 7a             -5.078125
+c9 82             +2.78125
+              c (relative cubeTo), implicit
+f9 7f             -0.03125
+05 80             +0.015625
+7a                -3
+31 81             +1.1875
+e9 79             -6.09375
+59 82             +2.34375
+              c (relative cubeTo), implicit
+75 7e             -1.546875
+95 80             +0.578125
+e1 7c             -3.125
+25 81             +1.140625
+a5 7b             -4.359375
+8d 81             +1.546875
+              c (relative cubeTo), implicit
+65 7f             -0.609375
+35 80             +0.203125
+dd 7e             -1.140625
+5d 80             +0.359375
+7d 7e             -1.515625
+75 80             +0.453125
+              c (relative cubeTo), implicit
+d1 7f             -0.1875
+0d 80             +0.046875
+ad 7f             -0.328125
+15 80             +0.078125
+95 7f             -0.421875
+19 80             +0.09375
+              c (relative cubeTo), implicit
+f9 7f             -0.03125
+f9 7f             -0.03125
+f5 7f             -0.046875
+f1 7f             -0.0625
+e9 7f             -0.09375
+e1 7f             -0.125
+              c (relative cubeTo), implicit
+e1 7f             -0.125
+d5 7f             -0.171875
+b1 7f             -0.3125
+91 7f             -0.4375
+7d 7f             -0.515625
+3d 7f             -0.765625
+              c (relative cubeTo), implicit
+95 7f             -0.421875
+59 7f             -0.65625
+7e                -1
+69 7e             -1.59375
+59 7e             -1.65625
+4d 7d             -2.703125
+              c (relative cubeTo), implicit
+ad 7e             -1.328125
+c9 7d             -2.21875
+fd 7c             -3.015625
+d9 7a             -5.15625
+51 7b             -4.6875
+ed 77             -8.078125
+              c (relative cubeTo), implicit
+b1 7c             -3.3125
+35 7a             -5.796875
+85 79             -6.484375
+89 74             -11.46875
+79 79             -6.53125
+71 74             -11.5625
+25            l (relative lineTo), 6 reps
+b5 7f             -0.296875
+39 7c             -3.78125
+              l (relative lineTo), implicit
+1d 84             +4.109375
+51 7d             -2.6875
+              l (relative lineTo), implicit
+1d 80             +0.109375
+ed 7d             -2.078125
+              l (relative lineTo), implicit
+61 84             +4.375
+e5 7d             -2.109375
+              l (relative lineTo), implicit
+79 82             +2.46875
+29 81             +1.15625
+              l (relative lineTo), implicit
+bd 84             +4.734375
+95 7e             -1.421875
+e1            z (closePath); end path
diff --git a/shiny/iconvg/testdata/cowbell.png b/shiny/iconvg/testdata/cowbell.png
new file mode 100644
index 0000000..e962ff1
--- /dev/null
+++ b/shiny/iconvg/testdata/cowbell.png
Binary files differ
diff --git a/shiny/iconvg/testdata/cowbell.svg b/shiny/iconvg/testdata/cowbell.svg
new file mode 100644
index 0000000..34ec496
--- /dev/null
+++ b/shiny/iconvg/testdata/cowbell.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg id="svg1921" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="256" width="256" viewBox="0 0 48 48" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs id="defs3">
+  <radialGradient id="radialGradient3107" gradientUnits="userSpaceOnUse" cy="20.272" cx="-102.14" gradientTransform="matrix(.33050 .17296 -.50775 .97021 65.204 16.495)" r="18.012">
+   <stop id="stop6387" stop-color="#edd400" offset="0"/>
+   <stop id="stop6389" stop-color="#fce94f" offset="1"/>
+  </radialGradient>
+  <radialGradient id="radialGradient3991" gradientUnits="userSpaceOnUse" cy="26.719" cx="-97.856" gradientTransform="matrix(.35718 .044280 -.11527 .92977 51.072 7.6124)" r="18.61">
+   <stop id="stop3995" stop-color="#edd400" offset="0"/>
+   <stop id="stop3997" stop-color="#fce94f" offset="1"/>
+  </radialGradient>
+  <linearGradient id="linearGradient5756" x1="-16.183" gradientUnits="userSpaceOnUse" y1="35.723" gradientTransform="translate(48.438 -.22321)" x2="-18.75" y2="29.808">
+   <stop id="stop5752" stop-color="#392100" offset="0"/>
+   <stop id="stop5754" stop-color="#0f0800" offset="1"/>
+  </linearGradient>
+ </defs>
+ <g id="layer1">
+  <path id="path1463" fill="url(#radialGradient3991)" d="m5.6684 17.968l.265-4.407 13.453 19.78.301 8.304-14.019-23.677z"/>
+  <path id="path1451" fill="url(#radialGradient3107)" d="m19.299 33.482l-13.619-19.688 3.8435-2.684.0922-2.1237 4.7023-2.26 2.99 1.1274 4.56-1.4252 20.719 16.272-23.288 10.782z"/>
+  <path id="path4875" fill-opacity=".49804" fill="#fdee74" d="m19.285 32.845l-13.593-19.079 3.995-2.833.1689-2.0377 1.9171-.8635 18.829 18.965-11.317 5.848z"/>
+  <path id="path1441" fill="#c4a000" d="m19.211 40.055c-.11-.67-.203-2.301-.205-3.624l-.003-2.406-2.492-3.769c-3.334-5.044-11.448-17.211-9.6752-14.744.3211.447 1.6961 2.119 2.1874 2.656.4914.536 1.3538 1.706 1.9158 2.6 2.276 3.615 8.232 12.056 8.402 12.056.1 0 10.4-5.325 11.294-5.678.894-.354 11.25-4.542 11.45-4.342.506.506 1.27 7.466.761 8.08-.392.473-5.06 3.672-10.256 6.121-5.195 2.45-11.984 4.269-12.594 4.269-.421 0-.639-.338-.785-1.219z"/>
+  <path id="path1433" fill="url(#linearGradient5756)" d="m19.825 33.646c.422-.68 10.105-5.353 10.991-5.753s9.881-4.123 10.468-4.009c.512.099.844 6.017.545 6.703-.23.527-8.437 4.981-9.516 5.523-1.225.616-11.642 4.705-12.145 4.369-.553-.368-.707-6.245-.343-6.833z"/>
+  <path id="path1350" style="color-rendering:auto;text-decoration-color:#000000;color:#000000;isolation:auto;mix-blend-mode:normal;shape-rendering:auto;solid-color:#000000;block-progression:tb;text-decoration-line:none;image-rendering:auto;white-space:normal;text-indent:0;text-transform:none;text-decoration-style:solid" fill-rule="evenodd" d="m21.982 5.8789-4.865 1.457-2.553-1.1914-5.3355 2.5743l-.015625.29688-.097656 1.8672-4.1855 2.7383.36719 4.5996.054687.0957s3.2427 5.8034 6.584 11.654c1.6707 2.9255 3.3645 5.861 4.6934 8.0938.66442 1.1164 1.2366 2.0575 1.6719 2.7363.21761.33942.40065.6121.54883.81641.07409.10215.13968.18665.20312.25976.06345.07312.07886.13374.27148.22461.27031.12752.38076.06954.54102.04883.16025-.02072.34015-.05724.55078-.10938.42126-.10427.95998-.26728 1.584-.4707 1.248-.40685 2.8317-.97791 4.3926-1.5586 3.1217-1.1614 6.1504-2.3633 6.1504-2.3633l.02539-.0098.02539-.01367s2.5368-1.3591 5.1211-2.8027c1.2922-.72182 2.5947-1.4635 3.6055-2.0723.50539-.30438.93732-.57459 1.2637-.79688.16318-.11114.29954-.21136.41211-.30273.11258-.09138.19778-.13521.30273-.32617.16048-.292.13843-.48235.1543-.78906s.01387-.68208.002-1.1094c-.02384-.8546-.09113-1.9133-.17188-2.9473-.161-2.067-.373-4.04-.373-4.04l-.021-.211-20.907-16.348zm-.209 1.1055 20.163 15.766c.01984.1875.19779 1.8625.34961 3.8066.08004 1.025.14889 2.0726.17188 2.8965.01149.41192.01156.76817-.002 1.0293-.01351.26113-.09532.47241-.0332.35938.05869-.10679.01987-.0289-.05664.0332s-.19445.14831-.34375.25c-.29859.20338-.72024.46851-1.2168.76758-.99311.59813-2.291 1.3376-3.5781 2.0566-2.5646 1.4327-5.0671 2.7731-5.0859 2.7832-.03276.01301-3.0063 1.1937-6.0977 2.3438-1.5542.5782-3.1304 1.1443-4.3535 1.543-.61154.19936-1.1356.35758-1.5137.45117-.18066.04472-.32333.07255-.41992.08594-.02937-.03686-.05396-.06744-.0957-.125-.128-.176-.305-.441-.517-.771-.424-.661-.993-1.594-1.655-2.705-1.323-2.223-3.016-5.158-4.685-8.08-3.3124-5.8-6.4774-11.465-6.5276-11.555l-.3008-3.787 4.1134-2.692.109-2.0777 4.373-2.1133 2.469 1.1523 4.734-1.4179z"/>
+ </g>
+</svg>
diff --git a/shiny/iconvg/testdata/elliptical.ivg b/shiny/iconvg/testdata/elliptical.ivg
new file mode 100644
index 0000000..0ee0321
--- /dev/null
+++ b/shiny/iconvg/testdata/elliptical.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/elliptical.ivg.disassembly b/shiny/iconvg/testdata/elliptical.ivg.disassembly
new file mode 100644
index 0000000..504194b
--- /dev/null
+++ b/shiny/iconvg/testdata/elliptical.ivg.disassembly
@@ -0,0 +1,79 @@
+89 49 56 47   IconVG Magic identifier
+00            Number of metadata chunks: 0
+98            Set CREG[CSEL-0] to a 4 byte color
+02 8a ca 00       gradient (NSTOPS=2, CBASE=10, NBASE=10, radial, reflect)
+0a            Set CSEL = 10
+4a            Set NSEL = 10
+ae            Set NREG[NSEL-6] to a real number
+af aa aa bc       -0.020833336
+bd            Set NREG[NSEL-5] to a zero-to-one number
+0a                0.041666668
+ac            Set NREG[NSEL-4] to a real number
+00                0
+ab            Set NREG[NSEL-3] to a real number
+8b 88 08 3d       0.03333333
+aa            Set NREG[NSEL-2] to a real number
+00                0
+b9            Set NREG[NSEL-1] to a zero-to-one number
+a0                0.6666667
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+4b                RGBA c00000ff
+af            Set NREG[NSEL-0] to a real number; NSEL++
+00                0
+87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
+03                RGBA 0000c0ff
+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)
+40                -32
+40                -32
+e6            H (absolute horizontal lineTo)
+c0                +32
+e8            V (absolute vertical lineTo)
+c0                +32
+e6            H (absolute horizontal lineTo)
+40                -32
+e1            z (closePath); end path
+80            Set CREG[CSEL-0] to a 1 byte color
+7c                RGBA ffffffff
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+56                -21
+6c                -10
+02            L (absolute lineTo), 3 reps
+58                -20
+6a                -11
+              L (absolute lineTo), implicit
+5a                -19
+6c                -10
+              L (absolute lineTo), implicit
+58                -20
+6e                -9
+e1            z (closePath); end path
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+56                -21
+9c                +14
+02            L (absolute lineTo), 3 reps
+58                -20
+9a                +13
+              L (absolute lineTo), implicit
+5a                -19
+9c                +14
+              L (absolute lineTo), implicit
+58                -20
+9e                +15
+e1            z (closePath); end path
+c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+92                +9
+8a                +5
+02            L (absolute lineTo), 3 reps
+94                +10
+88                +4
+              L (absolute lineTo), implicit
+96                +11
+8a                +5
+              L (absolute lineTo), implicit
+94                +10
+8c                +6
+e1            z (closePath); end path
diff --git a/shiny/iconvg/testdata/elliptical.png b/shiny/iconvg/testdata/elliptical.png
new file mode 100644
index 0000000..f67a08f
--- /dev/null
+++ b/shiny/iconvg/testdata/elliptical.png
Binary files differ
diff --git a/shiny/iconvg/testdata/gradient.ivg b/shiny/iconvg/testdata/gradient.ivg
index 9752286..865067b 100644
--- a/shiny/iconvg/testdata/gradient.ivg
+++ 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
index 697a91d..162cc57 100644
--- a/shiny/iconvg/testdata/gradient.ivg.disassembly
+++ b/shiny/iconvg/testdata/gradient.ivg.disassembly
@@ -4,14 +4,18 @@
 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
+ae            Set NREG[NSEL-6] to a real number
+8b 88 08 3d       0.03333333
+ad            Set NREG[NSEL-5] to a real number
+8b 88 88 3c       0.016666666
+ac            Set NREG[NSEL-4] to a real number
+6b 66 66 3f       0.9000001
+ab            Set NREG[NSEL-3] to a real number
+00                0
 aa            Set NREG[NSEL-2] to a real number
-18                12
-b1            Set NREG[NSEL-1] to a coordinate number
-5c                -18
+00                0
+a9            Set NREG[NSEL-1] to a real number
+00                0
 87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
 64                RGBA ff0000ff
 af            Set NREG[NSEL-0] to a real number; NSEL++
@@ -44,14 +48,18 @@
 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
+ae            Set NREG[NSEL-6] to a real number
+8b 88 08 3d       0.03333333
+ad            Set NREG[NSEL-5] to a real number
+8b 88 88 3c       0.016666666
+ac            Set NREG[NSEL-4] to a real number
+27 22 22 3f       0.63333344
+ab            Set NREG[NSEL-3] to a real number
+00                0
 aa            Set NREG[NSEL-2] to a real number
-18                12
-b1            Set NREG[NSEL-1] to a coordinate number
-7c                -2
+00                0
+a9            Set NREG[NSEL-1] to a real number
+00                0
 87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
 18                RGBA 00ffffff
 af            Set NREG[NSEL-0] to a real number; NSEL++
@@ -88,14 +96,18 @@
 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
+b6            Set NREG[NSEL-6] to a coordinate number
+11 80             0.0625
+ad            Set NREG[NSEL-5] to a real number
 00                0
-a9            Set NREG[NSEL-1] to a real number
-20                16
+bc            Set NREG[NSEL-4] to a zero-to-one number
+78                0.5
+ab            Set NREG[NSEL-3] to a real number
+00                0
+b2            Set NREG[NSEL-2] to a coordinate number
+11 80             0.0625
+b1            Set NREG[NSEL-1] to a coordinate number
+81 7f             -0.5
 87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
 64                RGBA ff0000ff
 af            Set NREG[NSEL-0] to a real number; NSEL++
@@ -128,14 +140,18 @@
 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
+b6            Set NREG[NSEL-6] to a coordinate number
+11 80             0.0625
+ad            Set NREG[NSEL-5] to a real number
 00                0
-a9            Set NREG[NSEL-1] to a real number
-20                16
+bc            Set NREG[NSEL-4] to a zero-to-one number
+78                0.5
+ab            Set NREG[NSEL-3] to a real number
+00                0
+b2            Set NREG[NSEL-2] to a coordinate number
+11 80             0.0625
+b1            Set NREG[NSEL-1] to a coordinate number
+81 7e             -1.5
 87            Set CREG[CSEL-0] to a 1 byte color; CSEL++
 18                RGBA 00ffffff
 af            Set NREG[NSEL-0] to a real number; NSEL++