Draw cycloid animation using matplotlib.animation.FuncAnimation


The result is:

Draw cycloid animation using matplotlib.animation.FuncAnimation

This page shows an example of the animation.FuncAnimation function. The theme is the animation of the cycloid.
This code is based on the following web sites:



In [1]:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
Documents of the animation.FuncAnimation
In [2]:
animation.FuncAnimation?
Init signature: animation.FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, **kwargs)
Docstring:     
Makes an animation by repeatedly calling a function ``func``.

Parameters
----------
fig : matplotlib.figure.Figure
   The figure object that is used to get draw, resize, and any
   other needed events.

func : callable
   The function to call at each frame.  The first argument will
   be the next value in ``frames``.   Any additional positional
   arguments can be supplied via the ``fargs`` parameter.

   The required signature is::

      def func(frame, *fargs) -> iterable_of_artists:

frames : iterable, int, generator function, or None, optional
    Source of data to pass ``func`` and each frame of the animation

    If an iterable, then simply use the values provided.  If the
    iterable has a length, it will override the ``save_count`` kwarg.

    If an integer, then equivalent to passing ``range(frames)``

    If a generator function, then must have the signature::

       def gen_function() -> obj:

    If ``None``, then equivalent to passing ``itertools.count``.

    In all of these cases, the values in *frames* is simply passed through
    to the user-supplied *func* and thus can be of any type.

init_func : callable, optional
   A function used to draw a clear frame. If not given, the
   results of drawing from the first item in the frames sequence
   will be used. This function will be called once before the
   first frame.

   If ``blit == True``, ``init_func`` must return an iterable of artists
   to be re-drawn.

   The required signature is::

      def init_func() -> iterable_of_artists:

fargs : tuple or None, optional
   Additional arguments to pass to each call to *func*.

save_count : int, optional
   The number of values from *frames* to cache.

interval : number, optional
   Delay between frames in milliseconds.  Defaults to 200.

repeat_delay : number, optional
   If the animation in repeated, adds a delay in milliseconds
   before repeating the animation.  Defaults to ``None``.

repeat : bool, optional
   Controls whether the animation should repeat when the sequence
   of frames is completed.  Defaults to ``True``.

blit : bool, optional
   Controls whether blitting is used to optimize drawing.  Defaults
   to ``False``.
File:           ****lib/python3.6/site-packages/matplotlib/animation.py
Type:           type
In [3]:
animation.FuncAnimation??
Init signature: animation.FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, **kwargs)
Source:        
class FuncAnimation(TimedAnimation):
    '''
    Makes an animation by repeatedly calling a function ``func``.

    Parameters
    ----------
    fig : matplotlib.figure.Figure
       The figure object that is used to get draw, resize, and any
       other needed events.

    func : callable
       The function to call at each frame.  The first argument will
       be the next value in ``frames``.   Any additional positional
       arguments can be supplied via the ``fargs`` parameter.

       The required signature is::

          def func(frame, *fargs) -> iterable_of_artists:

    frames : iterable, int, generator function, or None, optional
        Source of data to pass ``func`` and each frame of the animation

        If an iterable, then simply use the values provided.  If the
        iterable has a length, it will override the ``save_count`` kwarg.

        If an integer, then equivalent to passing ``range(frames)``

        If a generator function, then must have the signature::

           def gen_function() -> obj:

        If ``None``, then equivalent to passing ``itertools.count``.

        In all of these cases, the values in *frames* is simply passed through
        to the user-supplied *func* and thus can be of any type.

    init_func : callable, optional
       A function used to draw a clear frame. If not given, the
       results of drawing from the first item in the frames sequence
       will be used. This function will be called once before the
       first frame.

       If ``blit == True``, ``init_func`` must return an iterable of artists
       to be re-drawn.

       The required signature is::

          def init_func() -> iterable_of_artists:

    fargs : tuple or None, optional
       Additional arguments to pass to each call to *func*.

    save_count : int, optional
       The number of values from *frames* to cache.

    interval : number, optional
       Delay between frames in milliseconds.  Defaults to 200.

    repeat_delay : number, optional
       If the animation in repeated, adds a delay in milliseconds
       before repeating the animation.  Defaults to ``None``.

    repeat : bool, optional
       Controls whether the animation should repeat when the sequence
       of frames is completed.  Defaults to ``True``.

    blit : bool, optional
       Controls whether blitting is used to optimize drawing.  Defaults
       to ``False``.

    '''
    def __init__(self, fig, func, frames=None, init_func=None, fargs=None,
                 save_count=None, **kwargs):
        if fargs:
            self._args = fargs
        else:
            self._args = ()
        self._func = func

        # Amount of framedata to keep around for saving movies. This is only
        # used if we don't know how many frames there will be: in the case
        # of no generator or in the case of a callable.
        self.save_count = save_count
        # Set up a function that creates a new iterable when needed. If nothing
        # is passed in for frames, just use itertools.count, which will just
        # keep counting from 0. A callable passed in for frames is assumed to
        # be a generator. An iterable will be used as is, and anything else
        # will be treated as a number of frames.
        if frames is None:
            self._iter_gen = itertools.count
        elif callable(frames):
            self._iter_gen = frames
        elif iterable(frames):
            self._iter_gen = lambda: iter(frames)
            if hasattr(frames, '__len__'):
                self.save_count = len(frames)
        else:
            self._iter_gen = lambda: iter(xrange(frames))
            self.save_count = frames

        if self.save_count is None:
            # If we're passed in and using the default, set save_count to 100.
            self.save_count = 100
        else:
            # itertools.islice returns an error when passed a numpy int instead
            # of a native python int (http://bugs.python.org/issue30537).
            # As a workaround, convert save_count to a native python int.
            self.save_count = int(self.save_count)

        self._init_func = init_func

        # Needs to be initialized so the draw functions work without checking
        self._save_seq = []

        TimedAnimation.__init__(self, fig, **kwargs)

        # Need to reset the saved seq, since right now it will contain data
        # for a single frame from init, which is not what we want.
        self._save_seq = []

    def new_frame_seq(self):
        # Use the generating function to generate a new frame sequence
        return self._iter_gen()

    def new_saved_frame_seq(self):
        # Generate an iterator for the sequence of saved data. If there are
        # no saved frames, generate a new frame sequence and take the first
        # save_count entries in it.
        if self._save_seq:
            # While iterating we are going to update _save_seq
            # so make a copy to safely iterate over
            self._old_saved_seq = list(self._save_seq)
            return iter(self._old_saved_seq)
        else:
            return itertools.islice(self.new_frame_seq(), self.save_count)

    def _init_draw(self):
        # Initialize the drawing either using the given init_func or by
        # calling the draw function with the first item of the frame sequence.
        # For blitting, the init_func should return a sequence of modified
        # artists.
        if self._init_func is None:
            self._draw_frame(next(self.new_frame_seq()))

        else:
            self._drawn_artists = self._init_func()
            if self._blit:
                if self._drawn_artists is None:
                    raise RuntimeError('The init_func must return a '
                                       'sequence of Artist objects.')
                for a in self._drawn_artists:
                    a.set_animated(self._blit)
        self._save_seq = []

    def _draw_frame(self, framedata):
        # Save the data for potential saving of movies.
        self._save_seq.append(framedata)

        # Make sure to respect save_count (keep only the last save_count
        # around)
        self._save_seq = self._save_seq[-self.save_count:]

        # Call the func with framedata and args. If blitting is desired,
        # func needs to return a sequence of any artists that were modified.
        self._drawn_artists = self._func(framedata, *self._args)
        if self._blit:
            if self._drawn_artists is None:
                    raise RuntimeError('The animation function must return a '
                                       'sequence of Artist objects.')
            for a in self._drawn_artists:
                a.set_animated(self._blit)
File:           ****/lib/python3.6/site-packages/matplotlib/animation.py
Type:           type
In [4]:
# radius of the circle
R = 1
Define the function used in the FuncAnimation
In [5]:
def circle(a, b, r):
    # (a,b): the center of the circle
    # r: the radius of the circle
    # T: The number of the segments
    T = 100
    x, y = [0]*T, [0]*T
    for i,theta in enumerate(np.linspace(0,2*np.pi,T)):
        x[i] = a + r*np.cos(theta)
        y[i] = b + r*np.sin(theta)
    return x, y


def gen():
    for theta in np.linspace(0,4*np.pi,100):
        yield R*(theta-np.sin(theta)), R*(1-np.cos(theta)), R*theta
Define the figure
In [6]:
fig = plt.figure(figsize=(6,3))
ax = fig.add_subplot(111)
ax.set_ylim(0, 3)
ax.set_xlim(0, 15)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_aspect('equal')
ax.grid()
time_text = ax.text(0.05, 0.8, '', transform=ax.transAxes)

cycloid, = ax.plot([], [], 'r-', lw=2)
line, = ax.plot([], [], 'y-', lw=2)
circle_line, = ax.plot([], [], 'g', lw=2)
point, = ax.plot([], [], 'bo', ms=4)

xx, yy = [], []
def func(data):
    x, y, Rt = data
    time_text.set_text(r'$\theta$ = %.2f $\pi$' % (Rt/np.pi))
    xx.append(x)
    yy.append(y)
    cx, cy = circle(Rt, R, R)
    
    cycloid.set_data(xx, yy)
    line.set_data((x,Rt), (y,R))
    circle_line.set_data(cx, cy)
    point.set_data(x, y)
Build the animation
In [7]:
ani = animation.FuncAnimation(fig, func, gen, blit=False, interval=50)
Save the animation in mp4 format
In [8]:
fn = 'cycloid_FuncAnimation'
ani.save('%s.mp4'%(fn), writer='ffmpeg', fps=1000/50)
Save the animation in gif format (define xx, yy again in order to avoid unexpected behavior)
In [9]:
xx, yy = [], []
ani.save('%s.gif'%(fn), writer='imagemagick', fps=1000/50)
Reduce the size of the GIF image using Imagemagick
In [10]:
import subprocess
cmd = 'magick convert %s.gif -fuzz 10%% -layers Optimize %s_r.gif'%(fn,fn)
subprocess.check_output(cmd)
Out[10]:
b''
Show the animation in the jupyter notebook
In [11]:
plt.rcParams['animation.html'] = 'html5'
xx, yy = [], []
ani
Out[11]: