Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Add animation elements and example, improve element deduplication

Add displayInline attribute to Drawing

+52 -8
README.md
···
# drawSvg
-
A Python 3 library for programmatically generating SVG images (vector drawings) and rendering them or displaying them in an iPython notebook.
+
A Python 3 library for programmatically generating SVG images (vector drawings) and rendering them or displaying them in a Jupyter notebook.
Most common SVG tags are supported and others can easily be added by writing a small subclass of `DrawableBasicElement` or `DrawableParentElement`.
···
d.saveSvg('example.svg')
d.savePng('example.png')
-
# Display in iPython notebook
+
# Display in Jupyter notebook
d.rasterize() # Display as PNG
d # Display as SVG
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example1.png)
+
[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example1.png)](https://github.com/cduck/drawSvg/blob/master/examples/example1.svg)
### Gradients
```python
···
d
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example2.png)
+
[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example2.png)](https://github.com/cduck/drawSvg/blob/master/examples/example2.svg)
### Duplicate geometry and clip paths
```python
···
d.rasterize()
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example3.png)
+
[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example3.png)](https://github.com/cduck/drawSvg/blob/master/examples/example3.svg)
### Implementing other SVG tags
```python
···
d
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example4.png)
+
[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example4.png)](https://github.com/cduck/drawSvg/blob/master/examples/example4.svg)
### Interactive Widget
```python
···
@widget.set_draw_frame # Animation above is automatically updated
def draw_frame(secs=0):
# Draw something...
-
d = draw.Drawing(300, 40)
+
d = draw.Drawing(100, 40)
d.append(draw.Text(global_variable, 20, 0, 10))
-
d.append(draw.Text(str(secs), 20, 30, 10))
+
d.append(draw.Text('{:0.1f}'.format(secs), 20, 30, 10))
return d
# Jupyter cell 3:
global_variable = 'b' # Animation above now displays 'b'
```
+
+
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example7.gif)
+
+
### SVG-Native Animation
+
```python
+
import drawSvg as draw
+
+
d = draw.Drawing(200, 200, origin='center')
+
+
# Animate the position and color of circle
+
c = draw.Circle(0, 0, 20, fill='red')
+
# See for supported attributes:
+
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate
+
c.appendAnim(draw.Animate('cy', '6s', '-80;80;-80',
+
repeatCount='indefinite'))
+
c.appendAnim(draw.Animate('cx', '6s', '0;80;0;-80;0',
+
repeatCount='indefinite'))
+
c.appendAnim(draw.Animate('fill', '6s', 'red;green;blue;yellow',
+
calcMode='discrete',
+
repeatCount='indefinite'))
+
d.append(c)
+
+
# Animate a black circle around an ellipse
+
ellipse = draw.Path()
+
ellipse.M(-90, 0)
+
ellipse.A(90, 40, 360, True, True, 90, 0) # Ellipse path
+
ellipse.A(90, 40, 360, True, True, -90, 0)
+
ellipse.Z()
+
c2 = draw.Circle(0, 0, 10)
+
# See for supported attributes:
+
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateMotion
+
c2.appendAnim(draw.AnimateMotion(ellipse, '3s',
+
repeatCount='indefinite'))
+
# See for supported attributes:
+
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateTransform
+
c2.appendAnim(draw.AnimateTransform('scale', '3s', '1,2;2,1;1,2;2,1;1,2',
+
repeatCount='indefinite'))
+
d.append(c2)
+
+
d.saveSvg('/Users/cduck/Downloads/animated.svg') # Save to file
+
d # Display in Jupyter notebook
+
```
+
+
[![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/animated-fix-github.svg?sanitize=true)](https://github.com/cduck/drawSvg/blob/master/examples/animated.svg)
+35 -8
drawSvg/drawing.py
···
from io import StringIO
+
import base64
from . import Raster
from . import elements as elementsModule
···
Supports iPython: If a Drawing is the last line of a cell, it will be
displayed as an SVG below. '''
-
def __init__(self, width, height, origin=(0,0), **svgArgs):
+
def __init__(self, width, height, origin=(0,0), idPrefix='d',
+
displayInline=True, **svgArgs):
assert float(width) == width
assert float(height) == height
self.width = width
···
self.pixelScale = 1
self.renderWidth = None
self.renderHeight = None
-
self.svgArgs = svgArgs
+
self.idPrefix = str(idPrefix)
+
self.displayInline = displayInline
+
self.svgArgs = {}
+
for k, v in svgArgs.items():
+
k = k.replace('__', ':')
+
k = k.replace('_', '-')
+
if k[-1] == '-':
+
k = k[:-1]
+
self.svgArgs[k] = v
def setRenderSize(self, w=None, h=None):
self.renderWidth = w
self.renderHeight = h
···
outputFile.write('>\n<defs>\n')
# Write definition elements
idIndex = 0
-
def idGen(base='d'):
+
def idGen(base=''):
nonlocal idIndex
-
idStr = base + str(idIndex)
+
idStr = self.idPrefix + base + str(idIndex)
idIndex += 1
return idStr
prevSet = set((id(defn) for defn in self.otherDefs))
···
return dup
for element in self.otherDefs:
try:
-
element.writeSvgElement(outputFile)
+
element.writeSvgElement(idGen, isDuplicate, outputFile, False)
outputFile.write('\n')
except AttributeError:
pass
for element in self.elements:
try:
-
element.writeSvgDefs(idGen, isDuplicate, outputFile)
+
element.writeSvgDefs(idGen, isDuplicate, outputFile, False)
except AttributeError:
pass
outputFile.write('</defs>\n')
+
# Generate ids for normal elements
+
prevDefSet = set(prevSet)
+
for element in self.elements:
+
try:
+
element.writeSvgElement(idGen, isDuplicate, outputFile, True)
+
except AttributeError:
+
pass
+
prevSet = prevDefSet
# Write normal elements
for element in self.elements:
try:
-
element.writeSvgElement(outputFile)
+
element.writeSvgElement(idGen, isDuplicate, outputFile, False)
outputFile.write('\n')
except AttributeError:
pass
···
return Raster.fromSvg(self.asSvg())
def _repr_svg_(self):
''' Display in Jupyter notebook '''
+
if not self.displayInline:
+
return None
return self.asSvg()
-
+
def _repr_html_(self):
+
''' Display in Jupyter notebook '''
+
if self.displayInline:
+
return None
+
prefix = b'data:image/svg+xml;base64,'
+
data = base64.b64encode(self.asSvg().encode())
+
src = (prefix+data).decode()
+
return '<img src="{}">'.format(src)
+187 -37
drawSvg/elements.py
···
def writeXmlNodeArgs(args, outputFile):
for k, v in args.items():
if v is None: continue
-
k = k.replace('__', ':')
-
k = k.replace('_', '-')
-
if k[-1]=='-':
-
k = k[:-1]
-
if isinstance(v, defs.DrawingElement):
+
if isinstance(v, DrawingElement):
+
if v.id is None:
+
continue
if k == 'xlink:href':
v = '#{}'.format(v.id)
else:
···
''' Base class for drawing elements
Subclasses must implement writeSvgElement '''
-
def writeSvgElement(self, outputFile):
+
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
+
forceDup=False):
raise NotImplementedError('Abstract base class')
def getSvgDefs(self):
return ()
-
def writeSvgDefs(self, idGen, isDuplicate, outputFile):
+
def getLinkedElems(self):
+
return ()
+
def writeSvgDefs(self, idGen, isDuplicate, outputFile, dryRun):
for defn in self.getSvgDefs():
if isDuplicate(defn): continue
-
defn.writeSvgDefs(idGen, isDuplicate, outputFile)
-
defn.id = idGen()
-
defn.writeSvgElement(outputFile)
-
outputFile.write('\n')
+
defn.writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
+
if defn.id is None:
+
defn.id = idGen()
+
defn.writeSvgElement(idGen, isDuplicate, outputFile, dryRun,
+
forceDup=True)
+
if not dryRun:
+
outputFile.write('\n')
def __eq__(self, other):
return self is other
···
TAG_NAME = '_'
hasContent = False
def __init__(self, **args):
-
self.args = args
+
self.args = {}
+
for k, v in args.items():
+
k = k.replace('__', ':')
+
k = k.replace('_', '-')
+
if k[-1] == '-':
+
k = k[:-1]
+
self.args[k] = v
+
self.children = []
+
def checkChildrenAllowed(self):
+
if not self.hasContent:
+
raise RuntimeError(
+
'{} does not support children'.format(type(self)))
@property
def id(self):
return self.args.get('id', None)
@id.setter
def id(self, newId):
self.args['id'] = newId
-
def writeSvgElement(self, outputFile):
+
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
+
forceDup=False):
+
if dryRun:
+
if isDuplicate(self) and self.id is None:
+
self.id = idGen()
+
for elem in self.getLinkedElems():
+
if elem.id is None:
+
elem.id = idGen()
+
if self.hasContent:
+
self.writeContent(idGen, isDuplicate, outputFile, dryRun)
+
if self.children:
+
self.writeChildrenContent(idGen, isDuplicate, outputFile,
+
dryRun)
+
return
+
if isDuplicate(self) and not forceDup:
+
outputFile.write('<use xlink:href="#{}" />'.format(self.id))
+
return
outputFile.write('<')
outputFile.write(self.TAG_NAME)
writeXmlNodeArgs(self.args, outputFile)
-
if not self.hasContent:
+
if not self.hasContent and not self.children:
outputFile.write(' />')
else:
outputFile.write('>')
-
self.writeContent(outputFile)
+
if self.hasContent:
+
self.writeContent(idGen, isDuplicate, outputFile, dryRun)
+
if self.children:
+
self.writeChildrenContent(idGen, isDuplicate, outputFile,
+
dryRun)
outputFile.write('</')
outputFile.write(self.TAG_NAME)
outputFile.write('>')
-
def writeContent(self, outputFile):
+
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
''' Override in a subclass to add data between the start and end
tags. This will not be called if hasContent is False. '''
raise RuntimeError('This element has no content')
+
def writeChildrenContent(self, idGen, isDuplicate, outputFile, dryRun):
+
''' Override in a subclass to add data between the start and end
+
tags. This will not be called if hasContent is False. '''
+
if dryRun:
+
for child in self.children:
+
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
+
return
+
outputFile.write('\n')
+
for child in self.children:
+
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
+
outputFile.write('\n')
def getSvgDefs(self):
return [v for v in self.args.values()
-
if isinstance(v, defs.DrawingElement)]
+
if isinstance(v, DrawingElement)]
+
def writeSvgDefs(self, idGen, isDuplicate, outputFile, dryRun):
+
super().writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
+
for child in self.children:
+
child.writeSvgDefs(idGen, isDuplicate, outputFile, dryRun)
def __eq__(self, other):
if isinstance(other, type(self)):
return (self.TAG_NAME == other.TAG_NAME and
-
self.args == other.args)
+
self.args == other.args and
+
self.children == other.children)
return False
+
def appendAnim(self, animateElement):
+
self.children.append(animateElement)
+
def extendAnim(self, animateIterable):
+
self.children.extend(animateIterable)
class DrawingParentElement(DrawingBasicElement):
''' Base class for SVG elements that can have child nodes '''
···
self.children = list(children)
if len(self.children) > 0:
self.checkChildrenAllowed()
-
def checkChildrenAllowed(self):
-
if not self.hasContent:
-
raise RuntimeError('{} does not support children'.format(type(self)))
def draw(self, obj, **kwargs):
if not hasattr(obj, 'writeSvgElement'):
elements = obj.toDrawables(elements=elementsModule, **kwargs)
···
def extend(self, iterable):
self.checkChildrenAllowed()
self.children.extend(iterable)
-
def writeContent(self, outputFile):
-
outputFile.write('\n')
-
for child in self.children:
-
child.writeSvgElement(outputFile)
-
outputFile.write('\n')
-
def writeSvgDefs(self, idGen, isDuplicate, outputFile):
-
super().writeSvgDefs(idGen, isDuplicate, outputFile)
-
for child in self.children:
-
child.writeSvgDefs(idGen, isDuplicate, outputFile)
+
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
+
pass
class NoElement(DrawingElement):
''' A drawing element that has no effect '''
def __init__(self): pass
-
def writeSvgElement(self, outputFile):
+
def writeSvgElement(self, idGen, isDuplicate, outputFile, dryRun,
+
forceDup=False):
pass
def __eq__(self, other):
if isinstance(other, type(self)):
···
otherElem = '#' + otherElem
super().__init__(xlink__href=otherElem, x=x, y=y, **kwargs)
+
class Animate(DrawingBasicElement):
+
''' Animation for a specific property of another element
+
+
This should be added as a child of the element to animate. Otherwise
+
the other element and this element must both be added to the drawing.
+
'''
+
TAG_NAME = 'animate'
+
def __init__(self, attributeName, dur, from_or_values=None, to=None,
+
begin=None, otherElem=None, **kwargs):
+
if to is None:
+
values = from_or_values
+
from_ = None
+
else:
+
values = None
+
from_ = from_or_values
+
if isinstance(otherElem, str) and not otherElem.startswith('#'):
+
otherElem = '#' + otherElem
+
kwargs.update(attributeName=attributeName, to=to, dur=dur, begin=begin)
+
kwargs.setdefault('values', values)
+
kwargs.setdefault('from_', from_)
+
super().__init__(xlink__href=otherElem, **kwargs)
+
+
def getSvgDefs(self):
+
return [v for k, v in self.args.items()
+
if isinstance(v, DrawingElement)
+
if k != 'xlink:href']
+
+
def getLinkedElems(self):
+
return (self.args['xlink:href'],)
+
+
class _Mpath(DrawingBasicElement):
+
''' Used by AnimateMotion '''
+
TAG_NAME = 'mpath'
+
def __init__(self, otherPath, **kwargs):
+
super().__init__(xlink__href=otherPath, **kwargs)
+
+
class AnimateMotion(Animate):
+
''' Animation for the motion another element along a path
+
+
This should be added as a child of the element to animate. Otherwise
+
the other element and this element must both be added to the drawing.
+
'''
+
TAG_NAME = 'animateMotion'
+
def __init__(self, path, dur, from_or_values=None, to=None, begin=None,
+
otherElem=None, **kwargs):
+
useMpath = False
+
if isinstance(path, DrawingElement):
+
useMpath = True
+
pathElem = path
+
path = None
+
kwargs.setdefault('attributeName', None)
+
super().__init__(dur=dur, from_or_values=from_or_values, to=to,
+
begin=begin, path=path, otherElem=otherElem, **kwargs)
+
if useMpath:
+
self.children.append(_Mpath(pathElem))
+
+
class AnimateTransform(Animate):
+
''' Animation for the transform property of another element
+
+
This should be added as a child of the element to animate. Otherwise
+
the other element and this element must both be added to the drawing.
+
'''
+
TAG_NAME = 'animateTransform'
+
def __init__(self, type, dur, from_or_values, to=None, begin=None,
+
attributeName='transform', otherElem=None, **kwargs):
+
super().__init__(attributeName, dur=dur, from_or_values=from_or_values,
+
to=to, begin=begin, type=type, otherElem=otherElem,
+
**kwargs)
+
+
class Set(Animate):
+
''' Animation for a specific property of another element that sets the new
+
value without a transition.
+
+
This should be added as a child of the element to animate. Otherwise
+
the other element and this element must both be added to the drawing.
+
'''
+
TAG_NAME = 'set'
+
def __init__(self, attributeName, dur, to=None, begin=None,
+
otherElem=None, **kwargs):
+
super().__init__(attributeName, dur=dur, from_or_values=None,
+
to=to, begin=begin, otherElem=otherElem, **kwargs)
+
+
class Discard(Animate):
+
''' Animation configuration specifying when it is safe to discard another
+
element. E.g. when it will no longer be visible after an animation.
+
+
This should be added as a child of the element to animate. Otherwise
+
the other element and this element must both be added to the drawing.
+
'''
+
TAG_NAME = 'discard'
+
def __init__(self, attributeName, begin=None, **kwargs):
+
kwargs.setdefault('attributeName', None)
+
kwargs.setdefault('to', None)
+
kwargs.setdefault('dur', None)
+
super().__init__(from_or_values=None, begin=begin, otherElem=None,
+
**kwargs)
+
class Image(DrawingBasicElement):
''' A linked or embedded raster image '''
TAG_NAME = 'image'
···
mimeType = self.MIME_MAP[ext]
else:
mimeType = self.MIME_DEFAULT
-
warnings.warn('Unknown image file type "{}"'.format(ext), Warning)
+
warnings.warn('Unknown image file type "{}"'.format(ext),
+
Warning)
if mimeType is None:
mimeType = self.MIME_DEFAULT
-
warnings.warn('Unspecified image type; assuming png'.format(ext), Warning)
+
warnings.warn('Unspecified image type; assuming png'.format(ext),
+
Warning)
if data is not None:
embed = True
if embed and data is None:
···
pass
super().__init__(x=x, y=-y, font_size=fontSize, **kwargs)
self.escapedText = xml.escape(text)
-
def writeContent(self, outputFile):
+
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
+
if dryRun:
+
return
outputFile.write(self.escapedText)
class Rectangle(DrawingBasicElement):
···
def T(self, ex, ey): self.append('T', ex, -ey)
def t(self, ex, ey): self.append('t', ex, -ey)
def A(self, rx, ry, rot, largeArc, sweep, ex, ey):
-
self.append('A', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey)
+
self.append('A', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex,
+
-ey)
def a(self, rx, ry, rot, largeArc, sweep, ex, ey):
-
self.append('a', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex, -ey)
-
def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True, includeL=False):
+
self.append('a', rx, ry, rot, int(bool(largeArc)), int(bool(sweep)), ex,
+
-ey)
+
def arc(self, cx, cy, r, startDeg, endDeg, cw=False, includeM=True,
+
includeL=False):
''' Uses A() to draw a circular arc '''
largeArc = (endDeg - startDeg) % 360 > 180
startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180
+37
examples/animated-fix-github.svg
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+
width="200" height="200" viewBox="-100.0 -100.0 200 200">
+
<defs>
+
<path d="M-90,0 A90,40,360,1,1,90,0 A90,40,360,1,1,-90,0 Z" id="d0" />
+
</defs>
+
<g>
+
<circle cx="0" cy="0" r="19" fill="red">
+
<animateTransform type="scale" repeatCount="indefinite" attributeName="transform" dur="6s" values="1;1;1;1;1;1;0;0;0;0;0;0" />
+
</circle>
+
<animateTransform type="translate" repeatCount="indefinite" attributeName="transform" dur="6s" values="0,-80;80,0;0,80;-80,0;0,-80" />
+
</g>
+
<g>
+
<circle cx="0" cy="0" r="19.25" fill="green" transform="scale(0)">
+
<animateTransform type="scale" repeatCount="indefinite" attributeName="transform" dur="6s" begin="1.5s" values="1;1;1;1;1;1;0;0;0;0;0;0" />
+
</circle>
+
<animateTransform type="translate" repeatCount="indefinite" attributeName="transform" dur="6s" values="0,-80;80,0;0,80;-80,0;0,-80" />
+
</g>
+
<g>
+
<circle cx="0" cy="0" r="19.5" fill="blue" transform="scale(0)">
+
<animateTransform type="scale" repeatCount="indefinite" attributeName="transform" dur="6s" begin="3s" values="1;1;1;1;1;0;0;0;0;0;0;0" />
+
</circle>
+
<animateTransform type="translate" repeatCount="indefinite" attributeName="transform" dur="6s" values="0,-80;80,0;0,80;-80,0;0,-80" />
+
</g>
+
<g>
+
<circle cx="0" cy="0" r="20.1" fill="yellow" transform="scale(0)">
+
<animateTransform type="scale" repeatCount="indefinite" attributeName="transform" dur="6s" begin="4.5s" values="1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;" />
+
</circle>
+
<animateTransform type="translate" repeatCount="indefinite" attributeName="transform" dur="6s" values="0,-80;80,0;0,80;-80,0;0,-80" />
+
</g>
+
<circle cx="0" cy="0" r="10">
+
<animateMotion repeatCount="indefinite" dur="3s">
+
<mpath xlink:href="#d0" />
+
</animateMotion>
+
<animateTransform type="scale" repeatCount="indefinite" attributeName="transform" dur="3s" values="1,2;2,1;1,2;2,1;1,2" />
+
</circle>
+
</svg>
+18
examples/animated.svg
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+
width="200" height="200" viewBox="-100.0 -100.0 200 200">
+
<defs>
+
<path d="M-90,0 A90,40,360,1,1,90,0 A90,40,360,1,1,-90,0 Z" id="d0" />
+
</defs>
+
<circle cx="0" cy="0" r="20" fill="red">
+
<animate repeatCount="indefinite" attributeName="cy" dur="6s" values="-80;80;-80" />
+
<animate repeatCount="indefinite" attributeName="cx" dur="6s" values="0;80;0;-80;0" />
+
<animate calcMode="discrete" repeatCount="indefinite" attributeName="fill" dur="6s" values="red;green;blue;yellow" />
+
</circle>
+
<circle cx="0" cy="0" r="10">
+
<animateMotion repeatCount="indefinite" dur="3s">
+
<mpath xlink:href="#d0" />
+
</animateMotion>
+
<animateTransform type="scale" repeatCount="indefinite" attributeName="transform" dur="3s" values="1,2;2,1;1,2;2,1;1,2" />
+
</circle>
+
</svg>
examples/example7.gif

This is a binary file and will not be displayed.