Skip to content

Player animation #24554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions examples/animation/animate_decay_blit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
=====
Decay
=====

This example showcases:
- using PlayerAnimation
- using blitting
- changing axes limits during an animation.
"""

import itertools

import numpy as np
import matplotlib.pyplot as plt

from matplotlib.animation import PlayerAnimation, FuncAnimation
import math

def data_gen():
for cnt in itertools.count():
yield cnt

def init(val):
global line
print(f"init with val: {val}")
if not "line" in globals():
line, = ax.plot([], [], lw=2)
ax.grid()
ax.set_ylim(-1.1, 1.1)
ax.set_xlim(0, 1 + math.floor(val / 10))
line.set_data([], [])
return [line]

fig, ax = plt.subplots()

def update_plot(i):
# update the data
xdata = np.linspace(-10, 10, 1000)
ydata = np.sin(xdata + i*0.1)
_, xmax = ax.get_xlim()
new_xmax = 1 + math.floor(i / 10)
if xmax != new_xmax:
ax.set_xlim(0, new_xmax)
ax.figure.canvas.draw_idle()
line.set_data(xdata, ydata)

return [line]

animation = PlayerAnimation(fig=fig, func=update_plot, init_func=init, interval=100, blit=True, valstep=0.5)
plt.show()
111 changes: 109 additions & 2 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
DISPLAY_TEMPLATE, INCLUDED_FRAMES, JS_INCLUDE, STYLE_INCLUDE)
from matplotlib import _api, cbook
import matplotlib.colors as mcolors
from matplotlib.widgets import Button,Slider
import mpl_toolkits.axes_grid1

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -1215,6 +1217,7 @@ def _on_resize(self, event):
# we're paused. Reset the cache and re-init. Set up an event handler
# to catch once the draw has actually taken place.
self._fig.canvas.mpl_disconnect(self._resize_id)
self._was_stopped = self.event_source._timer is None
self.event_source.stop()
self._blit_cache.clear()
self._init_draw()
Expand All @@ -1224,8 +1227,9 @@ def _on_resize(self, event):
def _end_redraw(self, event):
# Now that the redraw has happened, do the post draw flushing and
# blit handling. Then re-enable all of the original events.
self._post_draw(None, False)
self.event_source.start()
self._post_draw(None, self._blit)
if not self._was_stopped:
self.event_source.start()
self._fig.canvas.mpl_disconnect(self._resize_id)
self._resize_id = self._fig.canvas.mpl_connect('resize_event',
self._on_resize)
Expand Down Expand Up @@ -1777,3 +1781,106 @@ def _draw_frame(self, framedata):

for a in self._drawn_artists:
a.set_animated(self._blit)

class PlayerAnimation(FuncAnimation):
# inspired from https://stackoverflow.com/a/46327978/3949028
PLAY_SYMBOL = "$\u25B6$"
STOP_SYMBOL = "$\u25A0$"
PAUSE_SYMBOL = "$\u23F8$" #TODO use instead of STOP_SYMBOL, but doesn't work in Button.label
ONE_BACK_SYMBOL = "$\u29CF$"
ONE_FORWARD_SYMBOL = "$\u29D0$"

def __init__(self, func, init_func, min_value=0, max_value=100,
pos=(0.125, 0.92), valstep=1, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to need a docstring including the required signatures of the functions passed in.

self.val = min_value
self.min = min_value
self.max = max_value
self.direction = 1
self.caller_func = func
self.valstep = valstep
self._player_initiated = False
#https://github.com/matplotlib/matplotlib/issues/17685

def init_func_wrapper():
return init_func(self.val)

super().__init__(func=self._func_wrapper, frames=self._frame_generator,
init_func=init_func_wrapper, **kwargs)

self._setup_player(pos)

def _setup_player(self, pos):
if not self._player_initiated:
self._player_initiated = True
playerax = self._fig.add_axes([pos[0], pos[1], 0.64, 0.04])
divider = mpl_toolkits.axes_grid1.make_axes_locatable(playerax)
sax = divider.append_axes("right", size="80%", pad=0.05)
ofax = divider.append_axes("right", size="100%", pad=0.05)
sliderax = divider.append_axes("right", size="500%", pad=0.07)
self.button_oneback = Button(playerax, label=self.ONE_BACK_SYMBOL, useblit=self._blit)
self.play_pause_button = Button(sax, label=self.STOP_SYMBOL, useblit=self._blit)
self.button_oneforward = Button(ofax, label=self.ONE_FORWARD_SYMBOL, useblit=self._blit)
self.button_oneback.on_clicked(self.onebackward)
self.play_pause_button.on_clicked(self.play_pause)
self.button_oneforward.on_clicked(self.oneforward)
self.slider = Slider(sliderax, '', self.min, self.max, valinit=self.min, valstep=self.valstep, useblit=self._blit)
self.slider.on_changed(self.set_pos)

def _frame_generator(self):
while True:
next = self.val + self.direction*self.valstep
if next >= self.min and next <= self.max:
self.val = next
print(f"yield: {self.val}")
yield self.val
else:
self.pause()
print(f"pause, yield: {self.val}")
yield self.val

def pause(self, event=None):
super().pause()
self.direction = 0
self.play_pause_button.label.set_text(self.PLAY_SYMBOL)
self.play_pause_button._draw()

def resume(self, event=None):
self.direction = 1
self.play_pause_button.label.set_text(self.STOP_SYMBOL)
self.play_pause_button._draw()
super().resume()

def play_pause(self, event=None):
if self.direction == 0:
self.resume()
else:
self.pause()

def oneforward(self, event=None):
self.direction = 1
self.trigger_step()

def onebackward(self, event=None):
self.direction = -1
self.trigger_step()

def set_pos(self, val):
if isinstance(self.valstep, int):
val = int(val) # slider gives float event if valstep is int
if self.val != val:
print(f"slider set_pos: {val}")
self.val = val
self.direction = 0
self.trigger_step()

def trigger_step(self):
for a in self._drawn_artists:
a.set_animated(True)
self._step()
self.pause()

def _func_wrapper(self, val):
print(f"player _func_wrapper: {val}")
self.slider.set_val(val)
return self.caller_func(self.val)

Loading