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

Add animation and GIF/video generation

Changed files
+168
drawSvg
+10
drawSvg/__init__.py
···
from .raster import Raster
from .drawing import Drawing
from .elements import *
+
from .video import (
+
render_svg_frames,
+
save_video,
+
)
+
from .animation import (
+
Animation,
+
animate_video,
+
animate_jupyter,
+
)
+
# Make all elements available in the elements module
from . import defs
+104
drawSvg/animation.py
···
+
import time
+
+
from . import video
+
+
+
class Animation:
+
def __init__(self, draw_func=None, callback=None):
+
self.frames = []
+
if draw_func is None:
+
draw_func = lambda d:d
+
self.draw_func = draw_func
+
if callback is None:
+
callback = lambda d:None
+
self.callback = callback
+
+
def append_frame(self, frame):
+
self.frames.append(frame)
+
self.callback(frame)
+
+
def draw_frame(self, *args, **kwargs):
+
frame = self.draw_func(*args, **kwargs)
+
self.append_frame(frame)
+
return frame
+
+
def save_video(self, file, **kwargs):
+
video.save_video(self.frames, file, **kwargs)
+
+
+
class AnimationContext:
+
def __init__(self, draw_func=None, out_file=None,
+
jupyter=False, pause=False, clear=True, delay=0, disable=False,
+
video_args=None, _patch_delay=0.05):
+
self.jupyter = jupyter
+
self.disable = disable
+
if self.jupyter and not self.disable:
+
from IPython import display
+
self._jupyter_clear_output = display.clear_output
+
self._jupyter_display = display.display
+
callback = self.draw_jupyter_frame
+
else:
+
callback = None
+
self.anim = Animation(draw_func, callback=callback)
+
self.out_file = out_file
+
self.pause = pause
+
self.clear = clear
+
self.delay = delay
+
if video_args is None:
+
video_args = {}
+
self.video_args = video_args
+
self._patch_delay = _patch_delay
+
+
def draw_jupyter_frame(self, frame):
+
if self.clear:
+
self._jupyter_clear_output(wait=True)
+
self._jupyter_display(frame)
+
if self.pause:
+
# Patch. Jupyter sometimes clears the input field otherwise.
+
time.sleep(self._patch_delay)
+
input('Next?')
+
elif self.delay != 0:
+
time.sleep(self.delay)
+
+
def __enter__(self):
+
return self.anim
+
+
def __exit__(self, exc_type, exc_value, exc_traceback):
+
if exc_value is None:
+
# No error
+
if self.out_file is not None and not self.disable:
+
self.anim.save_video(self.out_file, **self.video_args)
+
+
+
def animate_video(out_file, draw_func=None, jupyter=False, **video_args):
+
'''
+
Returns a context manager that stores frames and saves a video when the
+
context exits.
+
+
Example:
+
```
+
with animate_video('video.mp4') as draw_frame:
+
while True:
+
...
+
draw_frame(...)
+
```
+
'''
+
return AnimationContext(draw_func=draw_func, out_file=out_file,
+
jupyter=jupyter, video_args=video_args)
+
+
+
def animate_jupyter(draw_func=None, pause=False, clear=True, delay=0.1,
+
**kwargs):
+
'''
+
Returns a context manager that displays frames in a Jupyter notebook.
+
+
Example:
+
```
+
with animate_jupyter(delay=0.5) as draw_frame:
+
while True:
+
...
+
draw_frame(...)
+
```
+
'''
+
return AnimationContext(draw_func=draw_func, jupyter=True, pause=pause,
+
clear=clear, delay=delay, **kwargs)
+54
drawSvg/video.py
···
+
import numpy as np
+
import imageio
+
+
from .drawing import Drawing
+
+
+
def render_svg_frames(frames, align_bottom=False, align_right=False,
+
bg=(255,)*4, **kwargs):
+
arr_frames = [imageio.imread(d.rasterize().pngData)
+
for d in frames]
+
max_width = max(map(lambda arr:arr.shape[1], arr_frames))
+
max_height = max(map(lambda arr:arr.shape[0], arr_frames))
+
+
def mod_frame(arr):
+
new_arr = np.zeros((max_height, max_width) + arr.shape[2:],
+
dtype=arr.dtype)
+
new_arr[:,:] = bg[:new_arr.shape[-1]]
+
if align_bottom:
+
slice0 = slice(-arr.shape[0], None)
+
else:
+
slice0 = slice(None, arr.shape[0])
+
if align_right:
+
slice1 = slice(-arr.shape[1], None)
+
else:
+
slice1 = slice(None, arr.shape[1])
+
new_arr[slice0, slice1] = arr
+
return new_arr
+
return list(map(mod_frame, arr_frames))
+
+
def save_video(frames, file, **kwargs):
+
'''
+
Save a series of drawings as a GIF or video.
+
+
Arguments:
+
frames: A list of `Drawing`s or a list of `numpy.array`s.
+
file: File name or file like object to write the video to. The
+
extension determines the output format.
+
align_bottom: If frames are different sizes, align the bottoms of each
+
frame in the video.
+
align_right: If frames are different sizes, align the right edge of each
+
frame in the video.
+
bg: If frames are different sizes, fill the background with this color.
+
(default is white: (255, 255, 255, 255))
+
duration: If writing a GIF, sets the duration of each frame.
+
fps: If writing a video, sets the frame rate in FPS.
+
**kwargs: Other arguments to imageio.mimsave().
+
+
'''
+
if isinstance(frames[0], Drawing):
+
frames = render_svg_frames(frames, **kwargs)
+
kwargs.pop('align_bottom', None)
+
kwargs.pop('align_right', None)
+
kwargs.pop('bg', None)
+
imageio.mimsave(file, frames, **kwargs)