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