8000 Import JSAnimation into the animation module. (Fixes #4703) by dopplershift · Pull Request #4821 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Import JSAnimation into the animation module. (Fixes #4703) #4821

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

Merged
merged 5 commits into from
Sep 1, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Import JSAnimation into the animation module.
This pulls http://github.com/jakevdp/JSAnimation into the code. Most of
this is in the HTMLWriter class. This also adds the `jshtml` option for
the animation.html setting.
  • Loading branch information
dopplershift committed Aug 31, 2017
commit 34b6ea32e842a14896fcab610b1021f041208532
210 changes: 210 additions & 0 deletions lib/matplotlib/_animation_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Javascript template for HTMLWriter
JS_INCLUDE = """
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/
css/font-awesome.min.css">
<script language="javascript">
/* Define the Animation class */
function Animation(frames, img_id, slider_id, interval, loop_select_id){
this.img_id = img_id;
this.slider_id = slider_id;
this.loop_select_id = loop_select_id;
this.interval = interval;
this.current_frame = 0;
this.direction = 0;
this.timer = null;
this.frames = new Array(frames.length);

for (var i=0; i<frames.length; i++)
{
this.frames[i] = new Image();
this.frames[i].src = frames[i];
}
document.getElementById(this.slider_id).max = this.frames.length - 1;
this.set_frame(this.current_frame);
}

Animation.prototype.get_loop_state = function(){
var button_group = document[this.loop_select_id].state;
for (var i = 0; i < button_group.length; i++) {
var button = button_group[i];
if (button.checked) {
return button.value;
}
}
return undefined;
}

Animation.prototype.set_frame = function(frame){
this.current_frame = frame;
document.getElementById(this.img_id).src =
this.frames[this.current_frame].src;
document.getElementById(this.slider_id).value = this.current_frame;
}

Animation.prototype.next_frame = function()
{
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
}

Animation.prototype.previous_frame = function()
{
this.set_frame(Math.max(0, this.current_frame - 1));
}

Animation.prototype.first_frame = function()
{
this.set_frame(0);
}

Animation.prototype.last_frame = function()
{
this.set_frame(this.frames.length - 1);
}

Animation.prototype.slower = function()
{
this.interval /= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}

Animation.prototype.faster = function()
{
this.interval *= 0.7;
if(this.direction > 0){this.play_animation();}
else if(this.direction < 0){this.reverse_animation();}
}

Animation.prototype.anim_step_forward = function()
{
this.current_frame += 1;
if(this.current_frame < this.frames.length){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.first_frame();
}else if(loop_state == "reflect"){
this.last_frame();
this.reverse_animation();
}else{
this.pause_animation();
this.last_frame();
}
}
}

Animation.prototype.anim_step_reverse = function()
{
this.current_frame -= 1;
if(this.current_frame >= 0){
this.set_frame(this.current_frame);
}else{
var loop_state = this.get_loop_state();
if(loop_state == "loop"){
this.last_frame();
}else if(loop_state == "reflect"){
this.first_frame();
this.play_animation();
}else{
this.pause_animation();
this.first_frame();
}
}
}

Animation.prototype.pause_animation = function()
{
this.direction = 0;
if (this.timer){
clearInterval(this.timer);
this.timer = null;
}
}

Animation.prototype.play_animation = function()
{
this.pause_animation();
this.direction = 1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_forward();
}, this.interval);
}

Animation.prototype.reverse_animation = function()
{
this.pause_animation();
this.direction = -1;
var t = this;
if (!this.timer) this.timer = setInterval(function() {
t.anim_step_reverse();
}, this.interval);
}
</script>
"""


# HTML template for HTMLWriter
DISPLAY_TEMPLATE = """
<div class="animation" align="center">
<img id="_anim_img{id}">
<br>
<input id="_anim_slider{id}" type="range" style="width:350px"
name="points" min="0" max="1" step="1" value="0"
onchange="anim{id}.set_frame(parseInt(this.value));"></input>
<br>
<button onclick="anim{id}.slower()"><i class="fa fa-minus"></i></button>
<button onclick="anim{id}.first_frame()"><i class="fa fa-fast-backward">
</i></button>
<button onclick="anim{id}.previous_frame()">
<i class="fa fa-step-backward"></i></button>
<button onclick="anim{id}.reverse_animation()">
<i class="fa fa-play fa-flip-horizontal"></i></button>
<button onclick="anim{id}.pause_animation()"><i class="fa fa-pause">
</i></button>
<button onclick="anim{id}.play_animation()"><i class="fa fa-play"></i>
</button>
<button onclick="anim{id}.next_frame()"><i class="fa fa-step-forward">
</i></button>
<button onclick="anim{id}.last_frame()"><i class="fa fa-fast-forward">
</i></button>
<button onclick="anim{id}.faster()"><i class="fa fa-plus"></i></button>
<form action="#n" name="_anim_loop_select{id}" class="anim_control">
<input type="radio" name="state"
value="once" {once_checked}> Once </input>
<input type="radio" name="state"
value="loop" {loop_checked}> Loop </input>
<input type="radio" name="state"
value="reflect" {reflect_checked}> Reflect </input>
</form>
</div>


<script language="javascript">
/* Instantiate the Animation class. */
/* The IDs given should match those used in the template above. */
(function() {{
var img_id = "_anim_img{id}";
var slider_id = "_anim_slider{id}";
var loop_select_id = "_anim_loop_select{id}";
var frames = new Array({Nframes});
{fill_frames}

/* set a timeout to make sure all the above elements are created before
the object is initialized. */
setTimeout(function() {{
anim{id} = new Animation(frames, img_id, slider_id, {interval},
loop_select_id);
}}, 0);
}})()
</script>
"""

INCLUDED_FRAMES = """
for (var i=0; i<{Nframes}; i++){{
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
".{frame_format}";
}}
"""
146 changes: 146 additions & 0 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@
import abc
import contextlib
import tempfile
import uuid
import warnings
from matplotlib._animation_data import (DISPLAY_TEMPLATE, INCLUDED_FRAMES,
JS_INCLUDE)
from matplotlib.cbook import iterable, deprecated
from matplotlib.compat import subprocess
from matplotlib import verbose
from matplotlib import rcParams, rcParamsDefault, rc_context
if sys.version_info < (3, 0):
from cStringIO import StringIO as InMemory
else:
from io import BytesIO as InMemory

# Process creation flag for subprocess to prevent it raising a terminal
# window. See for example:
Expand Down Expand Up @@ -876,6 +883,112 @@ def _args(self):
+ self.output_args)


# Taken directly from jakevdp's JSAnimation package at
# http://github.com/jakevdp/JSAnimation
def _included_frames(frame_list, frame_format):
"""frame_list should be a list of filenames"""
return INCLUDED_FRAMES.format(Nframes=len(frame_list),
frame_dir=os.path.dirname(frame_list[0]),
frame_format=frame_format)


def _embedded_frames(frame_list, frame_format):
"""frame_list should be a list of base64-encoded png files"""
template = ' frames[{0}] = "data:image/{1};base64,{2}"\n'
embedded = "\n"
for i, frame_data in enumerate(frame_list):
embedded += template.format(i, frame_format,
frame_data.replace('\n', '\\\n'))
return embedded


@writers.register('html')
class HTMLWriter(FileMovieWriter):
supported_formats = ['png', 'jpeg', 'tiff', 'svg']
args_key = 'animation.html_args'

@classmethod
def isAvailable(cls):
return True

def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None,
metadata=None, embed_frames=False, default_mode='loop'):
self.embed_frames = embed_frames
self.default_mode = default_mode.lower()

if self.default_mode not in ['loop', 'once', 'reflect']:
self.default_mode = 'loop'
import warnings
warnings.warn("unrecognized default_mode: using 'loop'")

self._saved_frames = list()
8000
Copy link
Contributor

Choose a reason for hiding this comment

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

"= []"

super(HTMLWriter, self).__init__(fps, codec, bitrate,
extra_args, metadata)

def setup(self, fig, outfile, dpi, frame_dir=None):
if os.path.splitext(outfile)[-1] not in ['.html', '.htm']:
raise ValueError("outfile must be *.htm or *.html")

if not self.embed_frames:
if frame_dir is None:
frame_dir = outfile.rstrip('.html') + '_frames'
if not os.path.exists(frame_dir):
os.makedirs(frame_dir)
frame_prefix = os.path.join(frame_dir, 'frame')
else:
frame_prefix = None

super(HTMLWriter, self).setup(fig, outfile, dpi,
frame_prefix, clear_temp=False)

def grab_frame(self, **savefig_kwargs):
if self.embed_frames:
suffix = '.' + self.frame_format
f = InMemory()
self.fig.savefig(f, format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
f.seek(0)
imgdata64 = encodebytes(f.read()).decode('ascii')
Copy link
Contributor

Choose a reason for hiding this comment

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

f.getvalue(), then you don't need to seek first
shouldn't you just use bytes (and open the final file in binary mode) throughout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Too much template processing with format to use bytes. Then I'd just have to be using encode everywhere, which doesn't feel like a win.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used getvalue.

Copy link
Contributor

Choose a reason for hiding this comment

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

The argument regard 93CD ing format sounds reasonable.

self._saved_frames.append(imgdata64)
else:
return super(HTMLWriter, self).grab_frame(**savefig_kwargs)

def _run(self):
# make a duck-typed subprocess stand in
# this is called by the MovieWriter base class, but not used here.
class ProcessStandin(object):
returncode = 0

def communicate(self):
return '', ''

self._proc = ProcessStandin()

# save the frames to an html file
if self.embed_frames:
fill_frames = _embedded_frames(self._saved_frames,
self.frame_format)
else:
# temp names is filled by FileMovieWriter
fill_frames = _included_frames(self._temp_names,
self.frame_format)

mode_dict = dict(once_checked='',
loop_checked='',
reflect_checked='')
mode_dict[self.default_mode + '_checked'] = 'checked'

interval = int(1000. / self.fps)
Copy link
Contributor

Choose a reason for hiding this comment

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

1000, no dot (in a py3 world "/" definitely means float div :-))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tends to happen in 2 year old PRs... 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually should probably just use // since it's casting to int anyway.


with open(self.outfile, 'w') as of:
of.write(JS_INCLUDE)
of.write(DISPLAY_TEMPLATE.format(id=uuid.uuid4().hex,
Nframes=len(self._temp_names),
fill_frames=fill_frames,
interval=interval,
**mode_dict))


class Animation(object):
'''This class wraps the creation of an animation using matplotlib.

Expand Down Expand Up @@ -1288,11 +1401,44 @@ def to_html5_video(self):
size=self._video_size,
options=' '.join(options))

def to_jshtml(self, fps=None, embed_frames=True, default_mode=None):
"""Generate HTML representation of the animation"""
if fps is None and hasattr(self, '_interval'):
# Convert interval in ms to frames per second
fps = 1000. / self._interval
Copy link
Contributor

Choose a reason for hiding this comment

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

"1000"


# If we're not given a default mode, choose one base on the value of
# the repeat attribute
if default_mode is None:
default_mode = 'loop' if self.repeat else 'once'

if hasattr(self, "_html_representation"):
return self._html_representation
else:
# Can't open a second time while opened on windows. So we avoid
# deleting when closed, and delete manually later.
with tempfile.NamedTemporaryFile(suffix='.html',
delete=False) as f:
self.save(f.name, writer=HTMLWriter(fps=fps,
embed_frames=embed_frames,
default_mode=default_mode))
# Re-open and get content
with open(f.name) as fobj:
html = fobj.read()

# Now we can delete
os.remove(f.name)

self._html_representation = html
return html

def _repr_html_(self):
'''IPython display hook for rendering.'''
fmt = rcParams['animation.html']
if fmt == 'html5':
return self.to_html5_video()
elif fmt == 'jshtml':
return self.to_jshtml()


class TimedAnimation(Animation):
Expand Down
Loading
0