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.
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
d.rasterize() # Display as PNG
d # Display as SVG
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example1.png)
### Gradients
```python
···
d
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example2.png)
### Duplicate geometry and clip paths
```python
···
d.rasterize()
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example3.png)
### Implementing other SVG tags
```python
···
d
```
-
![Example output image](https://raw.githubusercontent.com/cduck/drawSvg/master/examples/example4.png)
### 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.append(draw.Text(global_variable, 20, 0, 10))
-
d.append(draw.Text(str(secs), 20, 30, 10))
return d
# Jupyter cell 3:
global_variable = 'b' # Animation above now displays 'b'
```
···
# drawSvg
+
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 Jupyter notebook
d.rasterize() # Display as PNG
d # Display as SVG
```
+
[![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)](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)](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)](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(100, 40)
d.append(draw.Text(global_variable, 20, 0, 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
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):
assert float(width) == width
assert float(height) == height
self.width = width
···
self.pixelScale = 1
self.renderWidth = None
self.renderHeight = None
-
self.svgArgs = svgArgs
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'):
nonlocal idIndex
-
idStr = 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)
outputFile.write('\n')
except AttributeError:
pass
for element in self.elements:
try:
-
element.writeSvgDefs(idGen, isDuplicate, outputFile)
except AttributeError:
pass
outputFile.write('</defs>\n')
# Write normal elements
for element in self.elements:
try:
-
element.writeSvgElement(outputFile)
outputFile.write('\n')
except AttributeError:
pass
···
return Raster.fromSvg(self.asSvg())
def _repr_svg_(self):
''' Display in Jupyter notebook '''
return self.asSvg()
-
···
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), 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.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=''):
nonlocal 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(idGen, isDuplicate, outputFile, False)
outputFile.write('\n')
except AttributeError:
pass
for element in self.elements:
try:
+
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(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 k == 'xlink:href':
v = '#{}'.format(v.id)
else:
···
''' Base class for drawing elements
Subclasses must implement writeSvgElement '''
-
def writeSvgElement(self, outputFile):
raise NotImplementedError('Abstract base class')
def getSvgDefs(self):
return ()
-
def writeSvgDefs(self, idGen, isDuplicate, outputFile):
for defn in self.getSvgDefs():
if isDuplicate(defn): continue
-
defn.writeSvgDefs(idGen, isDuplicate, outputFile)
-
defn.id = idGen()
-
defn.writeSvgElement(outputFile)
-
outputFile.write('\n')
def __eq__(self, other):
return self is other
···
TAG_NAME = '_'
hasContent = False
def __init__(self, **args):
-
self.args = args
@property
def id(self):
return self.args.get('id', None)
@id.setter
def id(self, newId):
self.args['id'] = newId
-
def writeSvgElement(self, outputFile):
outputFile.write('<')
outputFile.write(self.TAG_NAME)
writeXmlNodeArgs(self.args, outputFile)
-
if not self.hasContent:
outputFile.write(' />')
else:
outputFile.write('>')
-
self.writeContent(outputFile)
outputFile.write('</')
outputFile.write(self.TAG_NAME)
outputFile.write('>')
-
def writeContent(self, outputFile):
''' 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 getSvgDefs(self):
return [v for v in self.args.values()
-
if isinstance(v, defs.DrawingElement)]
def __eq__(self, other):
if isinstance(other, type(self)):
return (self.TAG_NAME == other.TAG_NAME and
-
self.args == other.args)
return False
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)
class NoElement(DrawingElement):
''' A drawing element that has no effect '''
def __init__(self): pass
-
def writeSvgElement(self, outputFile):
pass
def __eq__(self, other):
if isinstance(other, type(self)):
···
otherElem = '#' + otherElem
super().__init__(xlink__href=otherElem, x=x, y=y, **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)
if mimeType is None:
mimeType = self.MIME_DEFAULT
-
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):
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)
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):
''' Uses A() to draw a circular arc '''
largeArc = (endDeg - startDeg) % 360 > 180
startRad, endRad = startDeg*math.pi/180, endDeg*math.pi/180
···
def writeXmlNodeArgs(args, outputFile):
for k, v in args.items():
if v is None: continue
+
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, idGen, isDuplicate, outputFile, dryRun,
+
forceDup=False):
raise NotImplementedError('Abstract base class')
def getSvgDefs(self):
return ()
+
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, 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 = {}
+
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, 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 and not self.children:
outputFile.write(' />')
else:
outputFile.write('>')
+
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, 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, 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 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 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, idGen, isDuplicate, outputFile, dryRun):
+
pass
class NoElement(DrawingElement):
''' A drawing element that has no effect '''
def __init__(self): pass
+
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)
if mimeType is None:
mimeType = self.MIME_DEFAULT
+
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, 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)
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):
''' 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.