|  | 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() |