-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from 1 commit
34b6ea3
11f0464
c0c8fe3
ace6700
ffb2ccd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
There are no files selected for viewing
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}"; | ||
}} | ||
""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
@@ -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() | ||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f.getvalue(), then you don't need to seek first There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too much template processing with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1000, no dot (in a py3 world "/" definitely means float div :-)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tends to happen in 2 year old PRs... 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually should probably just use |
||
|
||
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. | ||
|
||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"= []"