HOME/Articles/

matplotlib example snowfall (snippet)

Article Outline

Python matplotlib example 'snowfall'

Modules used in program:

  • import matplotlib.patches as mpatches
  • import matplotlib.path as mpath
  • import matplotlib.animation as animation
  • import matplotlib.patheffects as path_effects
  • import numpy as np
  • import matplotlib.pyplot as plt
  • import matplotlib

python snowfall

Python matplotlib example: snowfall

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