Skip to content

Instantly share code, notes, and snippets.

@pelson
Last active August 29, 2015 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pelson/ca795a02a420a1b9bfbc to your computer and use it in GitHub Desktop.
Save pelson/ca795a02a420a1b9bfbc to your computer and use it in GitHub Desktop.
Season's greetings from matplotlib (2014). Very simple animated snow scene. (run the source, or see the result at https://www.youtube.com/watch?v=POnAkPpe770)

If working on XKCD style plotting for matplotlib taught me anything, it is that playing with software in a way that it was not originally designed to do can lead to some excellent discoveries (bugs) and generate new ideas and generalisations - not to mention it being a lot of fun!

So, in that vein, I wanted to put together a simple Christmas e-card using matplotlib. My main aim was to re-purpose some of the familiar matplotlib functionality to generate a simple festive animation.

I decided to go for a snowy scene, with a snow-capped greeting and sprig of holly. The snow is simply a scatter plot scaled by flake size and animated to fall in a pleasing way. The text is making use of the path effects functionality extended in v1.4 to add randomised "snow" around the text (the same effect employed by XKCD as it happens). And the holly is a nice demonstration of the power of Paths and vector rendering in matplotlib.

The source can be found at https://gist.github.com/pelson/ca795a02a420a1b9bfbc, and it requires matplotlib >= v1.4.

If you're impatient and don't want to run the code (don't do it), the animation is available on YouTube at https://www.youtube.com/watch?v=POnAkPpe770.

Finally, to all those taking some time off this festive season, I wish you a very happy holiday and wish you all the best for the new year.

Phil Elson

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patheffects as path_effects
import matplotlib.animation as animation
import matplotlib.path as mpath
import matplotlib.patches as mpatches
from matplotlib.transforms import BboxTransform, Bbox
class SeasonsGreetings(object):
def __init__(self, axes, message, n_flakes=1200, dt=1/25.):
self.ax = axes
self._message = message
self._n_flakes = n_flakes
self._full_descent = 1.0001
self._low_cutoff = -0.0001
self._x_accn_max = 0.55
self._dt_factor = (dt ** 2) * 0.15
self.setup_scene()
def setup_scene(self):
self.ax.format_coord = lambda *args, **kwargs: "'Tis the season to be jolly, Fa la la la la, la la la la."
self.create_flakes()
self.create_message()
self.create_holly()
self.create_groundcover()
fig.canvas.mpl_connect('resize_event', self.update_holy_position)
def create_flakes(self):
"""Create the flake data, and initialise random flake locations."""
flake_dtype = [('x', np.float), ('y', np.float), ('x_orig', np.float),
('x_velocity', np.float), ('mass', np.float)]
# Initialise the snow randomly.
n_flakes = self._n_flakes
flakes = np.empty(n_flakes, dtype=flake_dtype).view(np.recarray)
flakes.x = flakes.x_orig = np.random.uniform(-0.05, 1.05, n_flakes)
flakes.y = np.zeros(n_flakes) + np.random.uniform(self._low_cutoff, self._low_cutoff + self._full_descent, n_flakes)
flakes.x_velocity = np.random.normal(0.3, self._x_accn_max, n_flakes)
flakes.mass = np.clip(np.abs(np.random.normal(0, 0.12, n_flakes)) + 0.55, 0, 1)
self.flake_data = flakes
colors = np.ones((flakes.x.size, 4), dtype=np.float)
colors[:, 3] = flakes.mass ** 2
# Draw the flakes for the first time. The scatter will be updated in the animate loop.
self.flakes = self.ax.scatter(flakes.x, flakes.y, s=flakes.mass**2 * 30, marker='*',
c=colors, edgecolor='none', transform=self.ax.get_figure().transFigure)
def advance_flakes(self):
"""Update self.flake_data by one timestep."""
flakes = self.flake_data
# Move the flakes according to their mass and horizontal acceleration.
# This is not real physics - it just looks good.
flakes.y -= 9.8 * self._dt_factor * (flakes.mass + 0.4) ** 2
flakes.x -= flakes.x_velocity * self._dt_factor * (flakes.mass + 0.4) ** 2
restart = flakes.y < self._low_cutoff
flakes.y[restart] = self._low_cutoff + self._full_descent
flakes.x[restart] = flakes.x_orig[restart]
flakes.x_velocity[restart] = np.random.normal(0.3, self._x_accn_max, np.count_nonzero(restart))
return flakes
def create_message(self):
fig = self.ax.get_figure()
t1 = fig.text(0.5, 0.5, self._message, fontsize=50, weight=1000, ha='center', va='center')
wavy_text = path_effects.PathPatchEffect(offset=(0, 1), facecolor='white', edgecolor='white', linewidth=1.5)
wavy_text.patch.set_sketch_params(scale=2, length=30, randomness=1)
effects = [wavy_text, path_effects.PathPatchEffect(edgecolor='white', linewidth=1.1,
facecolor='black')]
t1.set_path_effects(effects)
self.text = t1
def create_holly(self):
# Holly and berries using cubic Bezier curves, adapted from www.createcricutdesigns.com
holly_verts = np.array([[334, 317, 291, 260, 264, 285, 251, 266, 278, 288, 281, 277, 277, 277, 277, 278, 260, 246, 234, 209, 203, 127, 150, 122, 100, 156, 178, 190, 210, 239, 299, 297, 299, 303, 305, 307, 310, 316, 322, 327, 327, 320, 311, 345, 376, 396, 437, 473, 508, 478, 481, 495, 469, 441, 412, 401, 387, 373, 373, 373, 373, 373, 366, 356, 369, 382, 397, 381, 375, 405, 363, 347, 334, 0, 338, 339, 339, 307, 324, 323, 306, 338, 338, 0, 356, 419, 455, 455, 454, 454, 418, 356, 356, 0, 282, 282, 282, 282, 267, 198, 153, 152, 198, 267, 282, 0],
[92, 145, 164, 172, 203, 221, 289, 288, 293, 300, 305, 313, 321, 324, 326, 329, 324, 314, 302, 320, 348, 350, 388, 418, 450, 441, 458, 482, 454, 431, 432, 416, 401, 388, 388, 388, 388, 388, 386, 382, 398, 410, 421, 430, 448, 505, 484, 494, 506, 459, 427, 398, 380, 385, 315, 330, 336, 337, 336, 335, 335, 324, 314, 310, 300, 297, 298, 266, 230, 178, 160, 127, 92, 0, 157, 158, 158, 233, 300, 300, 233, 157, 157, 0, 363, 394, 467, 467, 468, 468, 395, 364, 363, 0, 364, 365, 365, 365, 367, 378, 420, 419, 377, 366, 364, 0]]).T
holly_codes = np.array([1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79, 1, 2, 4, 4, 4, 2, 4, 4, 4, 79, 1, 4, 4, 4, 2, 4, 4, 4, 2, 79, 1, 4, 4, 4, 4, 4, 4, 2, 4, 4, 4, 79])
berries_verts = [np.array([[315, 300, 287, 287, 287, 300, 315, 331, 343, 343, 343, 331, 315, 0],
[339, 339, 351, 366, 381, 393, 393, 393, 381, 366, 351, 339, 339, 0]]).T,
np.array([[300, 285, 274, 274, 274, 285, 300, 314, 325, 325, 325, 314, 300, 0],
[297, 297, 308, 322, 336, 347, 347, 347, 336, 322, 308, 297, 297, 0]]).T,
np.array([[348, 334, 322, 322, 322, 334, 348, 362, 374, 374, 374, 362, 348, 0],
[306, 306, 318, 331, 345, 356, 356, 356, 345, 331, 318, 306, 306, 0]]).T]
berries_codes = [np.array([1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79]),
np.array([1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79]),
np.array([1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 79])]
# Turn the verts and codes into a Path for the holly.
holly_path = mpath.Path(holly_verts, holly_codes)
# Create a bounding box for the desired location of the holly.
self._holly_target_bbox = Bbox.unit()
# Now update the desired location based on the location of the text.
self.update_holy_position()
# Construct the transform which takes us from holly coordinates to pixels.
holly_to_pixel_trans = BboxTransform(holly_path.get_extents(), self._holly_target_bbox)
# Construct a path effect which has a 2px snow border at the top.
simple_snow_effect = [path_effects.withStroke(foreground='white', offset=(0, 1), linewidth=2)]
# Construct a patch for the holly leaves, and attach the path effect.
holly = mpatches.PathPatch(holly_path, facecolor='#008000', edgecolor='black',
transform=holly_to_pixel_trans, path_effects=simple_snow_effect)
# Add the patch to the axes.
self.ax.add_patch(holly)
# For each of the berries, create the appropriate path, and patch, and add to the axes.
for berry_verts, berry_codes in zip(berries_verts, berries_codes):
berry_path = mpath.Path(berry_verts, berry_codes)
berry = mpatches.PathPatch(berry_path, facecolor='red', edgecolor='black',
transform=holly_to_pixel_trans, path_effects=simple_snow_effect)
self.ax.add_patch(berry)
def update_holy_position(self, event=None):
"""Compute the desired bounding box for the holly in figure (pixel) coordinates."""
# Get the bounding box of the text.
text_extent = self.text.get_window_extent(self.ax.get_renderer_cache())
x_min, y_min = text_extent.xmax - 20, text_extent.ymin - 10
x_max, y_max = x_min + 100, y_min + 100
self._holly_target_bbox.set_points(np.array([[x_min, y_min], [x_max, y_max]]))
def create_groundcover(self):
def smooth(y, window_size):
box = np.ones(window_size) / float(window_size)
return np.convolve(y, box, mode='same')
n_pts, smooth_size = 60, 20
x = np.linspace(-0.5, 1.5, n_pts)
y = smooth(np.clip(np.random.poisson(1, n_pts) * 0.2, 0, 0.15), window_size=smooth_size)
self.ax.fill_between(x, y, 0, color='white', transform=self.ax.transAxes)
def animate_initial_step(self):
# Setup the figure without snow, the snow will be added with blitting later on.
self.flakes.set_visible(False)
return [self.flakes]
def animate_step(self, i):
# This is where the magic happens.
self.flakes.set_visible(True)
flake_data = self.advance_flakes()
self.flakes.set_offsets(np.vstack([flake_data.x, flake_data.y]).T)
return [self.flakes]
if __name__ == '__main__':
if plt.get_backend().lower() in ['macosx', 'agg']:
raise RuntimeError('Sorry, the greeting does not work on the {} backend.'.format(plt.get_backend()))
if matplotlib.__version__ < '1.4':
raise RuntimeError('Sorry, the greeting does not work on matplotlib < 1.4.')
fig = plt.figure(facecolor='black', dpi=100, figsize=(9.5, 6))
ax = plt.axes([0, 0, 1, 1], xlim=(0, 1), ylim=(0, 1), axis_bgcolor='black')
ax.set_axis_off()
plt.draw()
greetings_card = SeasonsGreetings(ax, message="Season's greetings\nfrom matplotlib")
ani = animation.FuncAnimation(fig, greetings_card.animate_step,
init_func=greetings_card.animate_initial_step,
interval=25, blit=True)
plt.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment