shiny/iconvg: implement arcs.
Change-Id: Id2948c1244afcf0f76410d981ebf2ad341168fa3
Reviewed-on: https://go-review.googlesource.com/31115
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/buffer.go b/shiny/iconvg/buffer.go
index fe5f44c..5bdad2a 100644
--- a/shiny/iconvg/buffer.go
+++ b/shiny/iconvg/buffer.go
@@ -191,6 +191,13 @@
return 4
}
+func (b *buffer) encodeAngle(f float32) int {
+ // Normalize f to the range [0, 1).
+ g := float64(f)
+ g -= math.Floor(g)
+ return b.encodeZeroToOne(float32(g))
+}
+
func (b *buffer) encodeZeroToOne(f float32) int {
if u := uint32(f * 15120); float32(u) == f*15120 && u < 15120 {
if u%126 == 0 {
diff --git a/shiny/iconvg/decode.go b/shiny/iconvg/decode.go
index e245e63..b5bb381 100644
--- a/shiny/iconvg/decode.go
+++ b/shiny/iconvg/decode.go
@@ -485,9 +485,30 @@
if p != nil && i != 0 {
p(nil, "%s, implicit\n", op)
}
- src, err = decodeCoordinates(coords[:nCoords], p, src)
- if err != nil {
- return nil, nil, err
+ var largeArc, sweep bool
+ if op[0] != 'A' && op[0] != 'a' {
+ src, err = decodeCoordinates(coords[:nCoords], p, src)
+ if err != nil {
+ return nil, nil, err
+ }
+ } else {
+ // We have an absolute or relative arcTo.
+ src, err = decodeCoordinates(coords[:2], p, src)
+ if err != nil {
+ return nil, nil, err
+ }
+ coords[2], src, err = decodeAngle(p, src)
+ if err != nil {
+ return nil, nil, err
+ }
+ largeArc, sweep, src, err = decodeArcToFlags(p, src)
+ if err != nil {
+ return nil, nil, err
+ }
+ src, err = decodeCoordinates(coords[4:6], p, src)
+ if err != nil {
+ return nil, nil, err
+ }
}
if dst == nil {
@@ -496,54 +517,27 @@
switch op[0] {
case 'L':
dst.AbsLineTo(coords[0], coords[1])
- continue
case 'l':
dst.RelLineTo(coords[0], coords[1])
- continue
case 'T':
dst.AbsSmoothQuadTo(coords[0], coords[1])
- continue
case 't':
dst.RelSmoothQuadTo(coords[0], coords[1])
- continue
case 'Q':
dst.AbsQuadTo(coords[0], coords[1], coords[2], coords[3])
- continue
case 'q':
dst.RelQuadTo(coords[0], coords[1], coords[2], coords[3])
- continue
case 'S':
dst.AbsSmoothCubeTo(coords[0], coords[1], coords[2], coords[3])
- continue
case 's':
dst.RelSmoothCubeTo(coords[0], coords[1], coords[2], coords[3])
- continue
case 'C':
dst.AbsCubeTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5])
- continue
case 'c':
dst.RelCubeTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5])
- continue
- }
-
- // We have an absolute or relative arcTo.
- src, err = decodeCoordinates(coords[:3], p, src)
- if err != nil {
- return nil, nil, err
- }
- var largeArc, sweep bool
- largeArc, sweep, src, err = decodeArcToFlags(p, src)
- if err != nil {
- return nil, nil, err
- }
- src, err = decodeCoordinates(coords[4:6], p, src)
- if err != nil {
- return nil, nil, err
- }
-
- if op[0] == 'A' {
+ case 'A':
dst.AbsArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
- } else {
+ case 'a':
dst.RelArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
}
}
@@ -665,6 +659,17 @@
return src, nil
}
+func decodeAngle(p printer, src buffer) (float32, buffer, error) {
+ x, n := src.decodeZeroToOne()
+ if n == 0 {
+ return 0, nil, errInvalidNumber
+ }
+ if p != nil {
+ p(src[:n], " %v × 360 degrees (%v degrees)\n", x, x*360)
+ }
+ return x, src[n:], nil
+}
+
func decodeArcToFlags(p printer, src buffer) (bool, bool, buffer, error) {
x, n := src.decodeNatural()
if n == 0 {
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
index 71ee314..26b780b 100644
--- a/shiny/iconvg/decode_test.go
+++ b/shiny/iconvg/decode_test.go
@@ -132,6 +132,7 @@
}{
{"testdata/action-info.lores", ""},
{"testdata/action-info.hires", ""},
+ {"testdata/arcs", ""},
{"testdata/blank", ""},
{"testdata/lod-polygon", ";64"},
{"testdata/video-005.primitive", ""},
diff --git a/shiny/iconvg/encode.go b/shiny/iconvg/encode.go
index 9b1bb88..9e00bfd 100644
--- a/shiny/iconvg/encode.go
+++ b/shiny/iconvg/encode.go
@@ -145,39 +145,37 @@
return e.lod0, e.lod1
}
-func (e *Encoder) SetCSel(cSel uint8) {
- if e.err != nil {
+func (e *Encoder) checkModeStyling() {
+ if e.mode == modeStyling {
return
}
- if e.mode != modeStyling {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- } else {
- e.err = errStylingOpsUsedInDrawingMode
- return
- }
+ if e.mode == modeInitial {
+ e.appendDefaultMetadata()
+ return
+ }
+ e.err = errStylingOpsUsedInDrawingMode
+}
+
+func (e *Encoder) SetCSel(cSel uint8) {
+ e.checkModeStyling()
+ if e.err != nil {
+ return
}
e.cSel = cSel & 0x3f
e.buf = append(e.buf, e.cSel)
}
func (e *Encoder) SetNSel(nSel uint8) {
+ e.checkModeStyling()
if e.err != nil {
return
}
- if e.mode != modeStyling {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- } else {
- e.err = errStylingOpsUsedInDrawingMode
- return
- }
- }
e.nSel = nSel & 0x3f
e.buf = append(e.buf, e.nSel|0x40)
}
func (e *Encoder) SetCReg(adj uint8, incr bool, c Color) {
+ e.checkModeStyling()
if e.err != nil {
return
}
@@ -216,6 +214,7 @@
}
func (e *Encoder) SetNReg(adj uint8, incr bool, f float32) {
+ e.checkModeStyling()
if e.err != nil {
return
}
@@ -249,17 +248,10 @@
}
func (e *Encoder) SetLOD(lod0, lod1 float32) {
+ e.checkModeStyling()
if e.err != nil {
return
}
- if e.mode != modeStyling {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- } else {
- e.err = errStylingOpsUsedInDrawingMode
- return
- }
- }
e.lod0 = lod0
e.lod1 = lod1
e.buf = append(e.buf, 0xc7)
@@ -268,17 +260,10 @@
}
func (e *Encoder) StartPath(adj uint8, x, y float32) {
+ e.checkModeStyling()
if e.err != nil {
return
}
- if e.mode != modeStyling {
- if e.mode == modeInitial {
- e.appendDefaultMetadata()
- } else {
- e.err = errStylingOpsUsedInDrawingMode
- return
- }
- }
if adj < 0 || 6 < adj {
e.err = errInvalidSelectorAdjustment
return
@@ -389,7 +374,7 @@
for j := m; j > 0; j-- {
e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+0]))
e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+1]))
- e.buf.encodeZeroToOne(e.drawArgs[i+2])
+ e.buf.encodeAngle(e.drawArgs[i+2])
e.buf.encodeNatural(uint32(e.drawArgs[i+3]))
e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+4]))
e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+5]))
diff --git a/shiny/iconvg/encode_test.go b/shiny/iconvg/encode_test.go
index 7d28cdd..6b1bdf9 100644
--- a/shiny/iconvg/encode_test.go
+++ b/shiny/iconvg/encode_test.go
@@ -86,6 +86,49 @@
}
}
+func TestEncodeArcs(t *testing.T) {
+ var e Encoder
+
+ e.SetCReg(1, false, RGBAColor(color.RGBA{0xff, 0x00, 0x00, 0xff}))
+ e.SetCReg(2, false, RGBAColor(color.RGBA{0xff, 0xff, 0x00, 0xff}))
+ e.SetCReg(3, false, RGBAColor(color.RGBA{0x00, 0x00, 0x00, 0xff}))
+ e.SetCReg(4, false, RGBAColor(color.RGBA{0x00, 0x00, 0x80, 0xff}))
+
+ e.StartPath(1, -10, 0)
+ e.RelHLineTo(-15)
+ e.RelArcTo(15, 15, 0, true, false, 15, -15)
+ e.ClosePathEndPath()
+
+ e.StartPath(2, -14, -4)
+ e.RelVLineTo(-15)
+ e.RelArcTo(15, 15, 0, false, false, -15, 15)
+ e.ClosePathEndPath()
+
+ const thirtyDegrees = 30.0 / 360
+ e.StartPath(3, -15, 30)
+ e.RelLineTo(5.0, -2.5)
+ e.RelArcTo(2.5, 2.5, -thirtyDegrees, false, true, 5.0, -2.5)
+ e.RelLineTo(5.0, -2.5)
+ e.RelArcTo(2.5, 5.0, -thirtyDegrees, false, true, 5.0, -2.5)
+ e.RelLineTo(5.0, -2.5)
+ e.RelArcTo(2.5, 7.5, -thirtyDegrees, false, true, 5.0, -2.5)
+ e.RelLineTo(5.0, -2.5)
+ e.RelArcTo(2.5, 10.0, -thirtyDegrees, false, true, 5.0, -2.5)
+ e.RelLineTo(5.0, -2.5)
+ e.AbsVLineTo(30)
+ e.ClosePathEndPath()
+
+ for largeArc := 0; largeArc <= 1; largeArc++ {
+ for sweep := 0; sweep <= 1; sweep++ {
+ e.StartPath(4, 10+8*float32(sweep), -28+8*float32(largeArc))
+ e.RelArcTo(6, 3, 0, largeArc != 0, sweep != 0, 6, 3)
+ e.ClosePathEndPath()
+ }
+ }
+
+ testEncode(t, &e, "testdata/arcs.ivg")
+}
+
var video005PrimitiveSVGData = []struct {
r, g, b uint32
x0, y0 int
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
index ce3c95d..30f79c0 100644
--- a/shiny/iconvg/rasterizer.go
+++ b/shiny/iconvg/rasterizer.go
@@ -8,6 +8,7 @@
"image"
"image/color"
"image/draw"
+ "math"
"golang.org/x/image/math/f32"
"golang.org/x/image/vector"
@@ -120,6 +121,9 @@
z.lod0, z.lod1 = lod0, lod1
}
+func (z *Rasterizer) unabsX(x float32) float32 { return x/z.scaleX - z.biasX }
+func (z *Rasterizer) unabsY(y float32) float32 { return y/z.scaleY - z.biasY }
+
func (z *Rasterizer) absX(x float32) float32 { return z.scaleX * (x + z.biasX) }
func (z *Rasterizer) absY(y float32) float32 { return z.scaleY * (y + z.biasY) }
func (z *Rasterizer) relX(x float32) float32 { return z.scaleX * x }
@@ -335,13 +339,165 @@
return
}
z.prevSmoothType = smoothTypeNone
- // TODO: implement.
+
+ // We follow the "Conversion from endpoint to center parameterization"
+ // algorithm as per
+ // https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+
+ // There seems to be a bug in the spec's "implementation notes".
+ //
+ // Actual implementations, such as
+ // - https://git.gnome.org/browse/librsvg/tree/rsvg-path.c
+ // - http://svn.apache.org/repos/asf/xmlgraphics/batik/branches/svg11/sources/org/apache/batik/ext/awt/geom/ExtendedGeneralPath.java
+ // - https://java.net/projects/svgsalamander/sources/svn/content/trunk/svg-core/src/main/java/com/kitfox/svg/pathcmd/Arc.java
+ // - https://github.com/millermedeiros/SVGParser/blob/master/com/millermedeiros/geom/SVGArc.as
+ // do something slightly different (marked with a †).
+
+ // (†) The Abs isn't part of the spec. Neither is checking that Rx and Ry
+ // are non-zero (and non-NaN).
+ Rx := math.Abs(float64(rx))
+ Ry := math.Abs(float64(ry))
+ if !(Rx > 0 && Ry > 0) {
+ z.z.LineTo(f32.Vec2{x, y})
+ return
+ }
+
+ // We work in IconVG coordinates (e.g. from -32 to +32 by default), rather
+ // than destination image coordinates (e.g. the width of the dst image),
+ // since the rx and ry radii also need to be scaled, but their scaling
+ // factors can be different, and aren't trivial to calculate due to
+ // xAxisRotation.
+ //
+ // We convert back to destination image coordinates via absX and absY calls
+ // later, during arcSegmentTo.
+ pen := z.z.Pen()
+ x1 := float64(z.unabsX(pen[0]))
+ y1 := float64(z.unabsY(pen[1]))
+ x2 := float64(z.unabsX(x))
+ y2 := float64(z.unabsY(y))
+
+ phi := 2 * math.Pi * float64(xAxisRotation)
+
+ // Step 1: Compute (x1′, y1′)
+ halfDx := (x1 - x2) / 2
+ halfDy := (y1 - y2) / 2
+ cosPhi := math.Cos(phi)
+ sinPhi := math.Sin(phi)
+ x1Prime := +cosPhi*halfDx + sinPhi*halfDy
+ y1Prime := -sinPhi*halfDx + cosPhi*halfDy
+
+ // Step 2: Compute (cx′, cy′)
+ rxSq := Rx * Rx
+ rySq := Ry * Ry
+ x1PrimeSq := x1Prime * x1Prime
+ y1PrimeSq := y1Prime * y1Prime
+
+ // (†) Check that the radii are large enough.
+ radiiCheck := x1PrimeSq/rxSq + y1PrimeSq/rySq
+ if radiiCheck > 1 {
+ c := math.Sqrt(radiiCheck)
+ Rx *= c
+ Ry *= c
+ rxSq = Rx * Rx
+ rySq = Ry * Ry
+ }
+
+ denom := rxSq*y1PrimeSq + rySq*x1PrimeSq
+ step2 := 0.0
+ if a := rxSq*rySq/denom - 1; a > 0 {
+ step2 = math.Sqrt(a)
+ }
+ if largeArc == sweep {
+ step2 = -step2
+ }
+ cxPrime := +step2 * Rx * y1Prime / Ry
+ cyPrime := -step2 * Ry * x1Prime / Rx
+
+ // Step 3: Compute (cx, cy) from (cx′, cy′)
+ cx := +cosPhi*cxPrime - sinPhi*cyPrime + (x1+x2)/2
+ cy := +sinPhi*cxPrime + cosPhi*cyPrime + (y1+y2)/2
+
+ // Step 4: Compute θ1 and Δθ
+ ax := (+x1Prime - cxPrime) / Rx
+ ay := (+y1Prime - cyPrime) / Ry
+ bx := (-x1Prime - cxPrime) / Rx
+ by := (-y1Prime - cyPrime) / Ry
+ theta1 := angle(1, 0, ax, ay)
+ deltaTheta := angle(ax, ay, bx, by)
+ if sweep {
+ if deltaTheta < 0 {
+ deltaTheta += 2 * math.Pi
+ }
+ } else {
+ if deltaTheta > 0 {
+ deltaTheta -= 2 * math.Pi
+ }
+ }
+
+ // This ends the
+ // https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+ // algorithm. What follows below is specific to this implementation.
+
+ // We approximate an arc by one or more cubic Bézier curves.
+ n := int(math.Ceil(math.Abs(deltaTheta) / (math.Pi/2 + 0.001)))
+ for i := 0; i < n; i++ {
+ z.arcSegmentTo(cx, cy,
+ theta1+deltaTheta*float64(i+0)/float64(n),
+ theta1+deltaTheta*float64(i+1)/float64(n),
+ Rx, Ry, cosPhi, sinPhi,
+ )
+ }
+}
+
+// arcSegmentTo approximates an arc by a cubic Bézier curve. The mathematical
+// formulae for the control points are the same as that used by librsvg.
+func (z *Rasterizer) arcSegmentTo(cx, cy, theta1, theta2, rx, ry, cosPhi, sinPhi float64) {
+ halfDeltaTheta := (theta2 - theta1) * 0.5
+ q := math.Sin(halfDeltaTheta * 0.5)
+ t := (8 * q * q) / (3 * math.Sin(halfDeltaTheta))
+ cos1 := math.Cos(theta1)
+ sin1 := math.Sin(theta1)
+ cos2 := math.Cos(theta2)
+ sin2 := math.Sin(theta2)
+ x1 := rx * (+cos1 - t*sin1)
+ y1 := ry * (+sin1 + t*cos1)
+ x2 := rx * (+cos2 + t*sin2)
+ y2 := ry * (+sin2 - t*cos2)
+ x3 := rx * (+cos2)
+ y3 := ry * (+sin2)
+ z.z.CubeTo(f32.Vec2{
+ z.absX(float32(cx + cosPhi*x1 - sinPhi*y1)),
+ z.absY(float32(cy + sinPhi*x1 + cosPhi*y1)),
+ }, f32.Vec2{
+ z.absX(float32(cx + cosPhi*x2 - sinPhi*y2)),
+ z.absY(float32(cy + sinPhi*x2 + cosPhi*y2)),
+ }, f32.Vec2{
+ z.absX(float32(cx + cosPhi*x3 - sinPhi*y3)),
+ z.absY(float32(cy + sinPhi*x3 + cosPhi*y3)),
+ })
}
func (z *Rasterizer) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
- if z.disabled {
- return
+ a := z.relVec2(x, y)
+ z.AbsArcTo(rx, ry, xAxisRotation, largeArc, sweep, a[0], a[1])
+}
+
+// angle returns the angle between the u and v vectors.
+func angle(ux, uy, vx, vy float64) float64 {
+ uNorm := math.Sqrt(ux*ux + uy*uy)
+ vNorm := math.Sqrt(vx*vx + vy*vy)
+ norm := uNorm * vNorm
+ cos := (ux*vx + uy*vy) / norm
+ ret := 0.0
+ if cos <= -1 {
+ ret = math.Pi
+ } else if cos >= +1 {
+ ret = 0
+ } else {
+ ret = math.Acos(cos)
}
- z.prevSmoothType = smoothTypeNone
- // TODO: implement.
+ if ux*vy < uy*vx {
+ return -ret
+ }
+ return +ret
}
diff --git a/shiny/iconvg/testdata/README b/shiny/iconvg/testdata/README
index 67eb33c..c883fe3 100644
--- a/shiny/iconvg/testdata/README
+++ b/shiny/iconvg/testdata/README
@@ -15,6 +15,15 @@
+arcs.ivg is inspired by the two examples at
+https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
+
+arcs.ivg.disassembly is a disassembly of that IconVG file.
+
+arcs.png is a rendering of that IconVG file.
+
+
+
blank.ivg is a blank, square graphic.
blank.ivg.disassembly is a disassembly of that IconVG file.
diff --git a/shiny/iconvg/testdata/arcs.ivg b/shiny/iconvg/testdata/arcs.ivg
new file mode 100644
index 0000000..f03a704
--- /dev/null
+++ b/shiny/iconvg/testdata/arcs.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/arcs.ivg.disassembly b/shiny/iconvg/testdata/arcs.ivg.disassembly
new file mode 100644
index 0000000..1c2eb05
--- /dev/null
+++ b/shiny/iconvg/testdata/arcs.ivg.disassembly
@@ -0,0 +1,129 @@
+89 49 56 47 IconVG Magic identifier
+00 Number of metadata chunks: 0
+81 Set CREG[CSEL-1] to a 1 byte color
+64 RGBA ff0000ff
+82 Set CREG[CSEL-2] to a 1 byte color
+78 RGBA ffff00ff
+83 Set CREG[CSEL-3] to a 1 byte color
+00 RGBA 000000ff
+84 Set CREG[CSEL-4] to a 1 byte color
+02 RGBA 000080ff
+c1 Start path, filled with CREG[CSEL-1]; M (absolute moveTo)
+6c -10
+80 +0
+e7 h (relative horizontal lineTo)
+62 -15
+d0 a (relative arcTo), 1 reps
+9e +15
+9e +15
+00 0 × 360 degrees (0 degrees)
+02 0x1 (largeArc=1, sweep=0)
+9e +15
+62 -15
+e1 z (closePath); end path
+c2 Start path, filled with CREG[CSEL-2]; M (absolute moveTo)
+64 -14
+78 -4
+e9 v (relative vertical lineTo)
+62 -15
+d0 a (relative arcTo), 1 reps
+9e +15
+9e +15
+00 0 × 360 degrees (0 degrees)
+00 0x0 (largeArc=0, sweep=0)
+62 -15
+9e +15
+e1 z (closePath); end path
+c3 Start path, filled with CREG[CSEL-3]; M (absolute moveTo)
+62 -15
+bc +30
+20 l (relative lineTo), 1 reps
+8a +5
+81 7d -2.5
+d0 a (relative arcTo), 1 reps
+81 82 +2.5
+81 82 +2.5
+dc 0.9166667 × 360 degrees (330 degrees)
+04 0x2 (largeArc=0, sweep=1)
+8a +5
+81 7d -2.5
+20 l (relative lineTo), 1 reps
+8a +5
+81 7d -2.5
+d0 a (relative arcTo), 1 reps
+81 82 +2.5
+8a +5
+dc 0.9166667 × 360 degrees (330 degrees)
+04 0x2 (largeArc=0, sweep=1)
+8a +5
+81 7d -2.5
+20 l (relative lineTo), 1 reps
+8a +5
+81 7d -2.5
+d0 a (relative arcTo), 1 reps
+81 82 +2.5
+81 87 +7.5
+dc 0.9166667 × 360 degrees (330 degrees)
+04 0x2 (largeArc=0, sweep=1)
+8a +5
+81 7d -2.5
+20 l (relative lineTo), 1 reps
+8a +5
+81 7d -2.5
+d0 a (relative arcTo), 1 reps
+81 82 +2.5
+94 +10
+dc 0.9166667 × 360 degrees (330 degrees)
+04 0x2 (largeArc=0, sweep=1)
+8a +5
+81 7d -2.5
+20 l (relative lineTo), 1 reps
+8a +5
+81 7d -2.5
+e8 V (absolute vertical lineTo)
+bc +30
+e1 z (closePath); end path
+c4 Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+94 +10
+48 -28
+d0 a (relative arcTo), 1 reps
+8c +6
+86 +3
+00 0 × 360 degrees (0 degrees)
+00 0x0 (largeArc=0, sweep=0)
+8c +6
+86 +3
+e1 z (closePath); end path
+c4 Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+a4 +18
+48 -28
+d0 a (relative arcTo), 1 reps
+8c +6
+86 +3
+00 0 × 360 degrees (0 degrees)
+04 0x2 (largeArc=0, sweep=1)
+8c +6
+86 +3
+e1 z (closePath); end path
+c4 Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+94 +10
+58 -20
+d0 a (relative arcTo), 1 reps
+8c +6
+86 +3
+00 0 × 360 degrees (0 degrees)
+02 0x1 (largeArc=1, sweep=0)
+8c +6
+86 +3
+e1 z (closePath); end path
+c4 Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+a4 +18
+58 -20
+d0 a (relative arcTo), 1 reps
+8c +6
+86 +3
+00 0 × 360 degrees (0 degrees)
+06 0x3 (largeArc=1, sweep=1)
+8c +6
+86 +3
+e1 z (closePath); end path
diff --git a/shiny/iconvg/testdata/arcs.png b/shiny/iconvg/testdata/arcs.png
new file mode 100644
index 0000000..acebcaa
--- /dev/null
+++ b/shiny/iconvg/testdata/arcs.png
Binary files differ