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

Add FrameAnimationContext for Spritesheets (#92)

authored by

Jared Bitz and committed by
GitHub
2d31b53c 2f76b329

+85 -8
+10 -3
README.md
··· 400 400 401 401 with draw.frame_animate_jupyter(draw_frame, delay=0.05) as anim: 402 402 # Or: 403 - #with draw.animate_video('example6.gif', draw_frame, duration=0.05 404 - # ) as anim: 403 + #with draw.frame_animate_video('example6.gif', draw_frame, duration=0.05) as anim: 404 + # Or: 405 + #with draw.frame_animate_spritesheet('example6.png', draw_frame, row_length=10) as anim: 405 406 # Add each frame to the animation 406 407 for i in range(20): 407 408 anim.draw_frame(i/10) ··· 411 412 anim.draw_frame(i/10) 412 413 ``` 413 414 414 - ![Example output image](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example6.gif) 415 + GIF: 416 + 417 + ![Example output gif](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example6.gif) 418 + 419 + Spritesheet (usable in most 2D game engines): 420 + 421 + ![Example output spritesheet](https://raw.githubusercontent.com/cduck/drawsvg/master/examples/example6.png) 415 422 416 423 ### Asynchronous Frame-based Animation in Jupyter 417 424 ```python
+1
drawsvg/__init__.py
··· 85 85 FrameAnimation, 86 86 frame_animate_video, 87 87 frame_animate_jupyter, 88 + frame_animate_spritesheet, 88 89 ) 89 90 from .native_animation import ( 90 91 SyncedAnimationConfig,
+31 -5
drawsvg/frame_animation.py
··· 25 25 def save_video(self, file, **kwargs): 26 26 video.save_video(self.frames, file, **kwargs) 27 27 28 + def save_spritesheet(self, file, **kwargs): 29 + video.save_spritesheet(self.frames, file, **kwargs) 30 + 28 31 29 32 class FrameAnimationContext: 30 33 def __init__(self, draw_func=None, out_file=None, 31 - jupyter=False, pause=False, clear=True, delay=0, disable=False, 32 - video_args=None, _patch_delay=0.05): 34 + jupyter=False, spritesheet=False, pause=False, 35 + clear=True, delay=0, disable=False, video_args=None, 36 + _patch_delay=0.05): 33 37 self.jupyter = jupyter 38 + self.spritesheet = spritesheet 34 39 self.disable = disable 35 40 if self.jupyter and not self.disable: 36 41 from IPython import display ··· 67 72 if exc_value is None: 68 73 # No error 69 74 if self.out_file is not None and not self.disable: 70 - self.anim.save_video(self.out_file, **self.video_args) 75 + if self.spritesheet: 76 + self.anim.save_spritesheet(self.out_file, **self.video_args) 77 + else: 78 + self.anim.save_video(self.out_file, **self.video_args) 71 79 72 80 73 81 def frame_animate_video(out_file, draw_func=None, jupyter=False, **video_args): ··· 77 85 78 86 Example: 79 87 ``` 80 - with animate_video('video.mp4') as anim: 88 + with frame_animate_video('video.mp4') as anim: 81 89 while True: 82 90 ... 83 91 anim.draw_frame(...) ··· 86 94 return FrameAnimationContext(draw_func=draw_func, out_file=out_file, 87 95 jupyter=jupyter, video_args=video_args) 88 96 97 + def frame_animate_spritesheet(out_file, draw_func=None, jupyter=False, 98 + **video_args): 99 + ''' 100 + Returns a context manager that stores frames and saves a spritesheet when 101 + the context exits. 102 + 103 + Example: 104 + ``` 105 + with frame_animate_spritesheet('sheet.png', row_length=10) as anim: 106 + while True: 107 + ... 108 + anim.draw_frame(...) 109 + ``` 110 + ''' 111 + return FrameAnimationContext(draw_func=draw_func, out_file=out_file, 112 + jupyter=jupyter, spritesheet=True, 113 + video_args=video_args) 114 + 89 115 90 116 def frame_animate_jupyter(draw_func=None, pause=False, clear=True, delay=0.1, 91 117 **kwargs): ··· 94 120 95 121 Example: 96 122 ``` 97 - with animate_jupyter(delay=0.5) as anim: 123 + with frame_animate_jupyter(delay=0.5) as anim: 98 124 while True: 99 125 ... 100 126 anim.draw_frame(...)
+43
drawsvg/video.py
··· 148 148 print() 149 149 print(f'Converting to video') 150 150 imageio.mimsave(file, frames, **kwargs) 151 + 152 + def save_spritesheet(frames, file, row_length=None, verbose=False, **kwargs): 153 + ''' 154 + Save a series of drawings as a bitmap spritesheet 155 + 156 + Arguments: 157 + frames: A list of `Drawing`s or a list of `numpy.array`s. 158 + file: File name or file like object to write the spritesheet to. The 159 + extension determines the output format. 160 + row_length: The length (in frames) of one row in the spritesheet. 161 + If not provided, all frames go on one row. 162 + align_bottom: If frames are different sizes, align the bottoms of each 163 + frame in the video. 164 + align_right: If frames are different sizes, align the right edge of each 165 + frame in the video. 166 + bg: If frames are different sizes, fill the background with this color. 167 + (default is white: (255, 255, 255, 255)) 168 + **kwargs: Other arguments to imageio.imsave(). 169 + 170 + ''' 171 + np, imageio = delay_import_np_imageio() 172 + if not isinstance(frames[0], np.ndarray): 173 + frames = render_svg_frames(frames, verbose=verbose, **kwargs) 174 + kwargs.pop('align_bottom', None) 175 + kwargs.pop('align_right', None) 176 + kwargs.pop('bg', None) 177 + 178 + cols = row_length if row_length is not None else len(frames) 179 + rows = (len(frames) - 1) // cols + 1 180 + 181 + if rows * cols > len(frames): # Unfilled final row 182 + empty_frame = np.zeros(frames[0].shape, dtype=frames[0].dtype) 183 + frames.extend([empty_frame] * (rows * cols - len(frames))) 184 + 185 + block_arrangement = [] 186 + for row in range(rows): 187 + next_row_end = (row+1)*cols 188 + block_arrangement.append([ 189 + [frame] for frame in frames[row*cols:next_row_end] 190 + ]) 191 + 192 + spritesheet = np.block(block_arrangement) 193 + imageio.imsave(file, spritesheet, **kwargs)
examples/example6.png

This is a binary file and will not be displayed.