|
29 | 29 | import itertools
|
30 | 30 | import base64
|
31 | 31 | import contextlib
|
| 32 | +import random |
| 33 | +import string |
32 | 34 | import tempfile
|
| 35 | +if sys.version_info < (3, 0): |
| 36 | + from cStringIO import StringIO as InMemory |
| 37 | +else: |
| 38 | + from io import BytesIO as InMemory |
33 | 39 | from matplotlib.cbook import iterable, is_string_like
|
34 | 40 | from matplotlib.compat import subprocess
|
35 | 41 | from matplotlib import verbose
|
@@ -561,6 +567,340 @@ def _args(self):
|
561 | 567 | + self.output_args)
|
562 | 568 |
|
563 | 569 |
|
| 570 | +JS_INCLUDE = """ |
| 571 | +<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"> |
| 572 | +<script language="javascript"> |
| 573 | + /* Define the Animation class */ |
| 574 | + function Animation(frames, img_id, slider_id, interval, loop_select_id){ |
| 575 | + this.img_id = img_id; |
| 576 | + this.slider_id = slider_id; |
| 577 | + this.loop_select_id = loop_select_id; |
| 578 | + this.interval = interval; |
| 579 | + this.current_frame = 0; |
| 580 | + this.direction = 0; |
| 581 | + this.timer = null; |
| 582 | + this.frames = new Array(frames.length); |
| 583 | +
|
| 584 | + for (var i=0; i<frames.length; i++) |
| 585 | + { |
| 586 | + this.frames[i] = new Image(); |
| 587 | + this.frames[i].src = frames[i]; |
| 588 | + } |
| 589 | + document.getElementById(this.slider_id).max = this.frames.length - 1; |
| 590 | + this.set_frame(this.current_frame); |
| 591 | + } |
| 592 | +
|
| 593 | + Animation.prototype.get_loop_state = function(){ |
| 594 | + var button_group = document[this.loop_select_id].state; |
| 595 | + for (var i = 0; i < button_group.length; i++) { |
| 596 | + var button = button_group[i]; |
| 597 | + if (button.checked) { |
| 598 | + return button.value; |
| 599 | + } |
| 600 | + } |
| 601 | + return undefined; |
| 602 | + } |
| 603 | +
|
| 604 | + Animation.prototype.set_frame = function(frame){ |
| 605 | + this.current_frame = frame; |
| 606 | + document.getElementById(this.img_id).src = this.frames[this.current_frame].src; |
| 607 | + document.getElementById(this.slider_id).value = this.current_frame; |
| 608 | + } |
| 609 | +
|
| 610 | + Animation.prototype.next_frame = function() |
| 611 | + { |
| 612 | + this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1)); |
| 613 | + } |
| 614 | +
|
| 615 | + Animation.prototype.previous_frame = function() |
| 616 | + { |
| 617 | + this.set_frame(Math.max(0, this.current_frame - 1)); |
| 618 | + } |
| 619 | +
|
| 620 | + Animation.prototype.first_frame = function() |
| 621 | + { |
| 622 | + this.set_frame(0); |
| 623 | + } |
| 624 | +
|
| 625 | + Animation.prototype.last_frame = function() |
| 626 | + { |
| 627 | + this.set_frame(this.frames.length - 1); |
| 628 | + } |
| 629 | +
|
| 630 | + Animation.prototype.slower = function() |
| 631 | + { |
| 632 | + this.interval /= 0.7; |
| 633 | + if(this.direction > 0){this.play_animation();} |
| 634 | + else if(this.direction < 0){this.reverse_animation();} |
| 635 | + } |
| 636 | +
|
| 637 | + Animation.prototype.faster = function() |
| 638 | + { |
| 639 | + this.interval *= 0.7; |
| 640 | + if(this.direction > 0){this.play_animation();} |
| 641 | + else if(this.direction < 0){this.reverse_animation();} |
| 642 | + } |
| 643 | +
|
| 644 | + Animation.prototype.anim_step_forward = function() |
| 645 | + { |
| 646 | + this.current_frame += 1; |
| 647 | + if(this.current_frame < this.frames.length){ |
| 648 | + this.set_frame(this.current_frame); |
| 649 | + }else{ |
| 650 | + var loop_state = this.get_loop_state(); |
| 651 | + if(loop_state == "loop"){ |
| 652 | + this.first_frame(); |
| 653 | + }else if(loop_state == "reflect"){ |
| 654 | + this.last_frame(); |
| 655 | + this.reverse_animation(); |
| 656 | + }else{ |
| 657 | + this.pause_animation(); |
658 | + this.last_frame(); |
| 659 | + } |
| 660 | + } |
| 661 | + } |
| 662 | +
|
| 663 | + Animation.prototype.anim_step_reverse = function() |
| 664 | + { |
| 665 | + this.current_frame -= 1; |
| 666 | + if(this.current_frame >= 0){ |
| 667 | + this.set_frame(this.current_frame); |
| 668 | + }else{ |
| 669 | + var loop_state = this.get_loop_state(); |
| 670 | + if(loop_state == "loop"){ |
| 671 | + this.last_frame(); |
| 672 | + }else if(loop_state == "reflect"){ |
| 673 | + this.first_frame(); |
| 674 | + this.play_animation(); |
| 675 | + }else{ |
| 676 | + this.pause_animation(); |
| 677 | + this.first_frame(); |
| 678 | + } |
| 679 | + } |
| 680 | + } |
| 681 | +
|
| 682 | + Animation.prototype.pause_animation = function() |
| 683 | + { |
| 684 | + this.direction = 0; |
| 685 | + if (this.timer){ |
| 686 | + clearInterval(this.timer); |
| 687 | + this.timer = null; |
| 688 | + } |
| 689 | + } |
| 690 | +
|
| 691 | + Animation.prototype.play_animation = function() |
| 692 | + { |
| 693 | + this.pause_animation(); |
| 694 | + this.direction = 1; |
| 695 | + var t = this; |
| 696 | + if (!this.timer) this.timer = setInterval(function(){t.anim_step_forward();}, this.interval); |
| 697 | + } |
| 698 | +
|
| 699 | + Animation.prototype.reverse_animation = function() |
| 700 | + { |
| 701 | + this.pause_animation(); |
| 702 | + this.direction = -1; |
| 703 | + var t = this; |
| 704 | + if (!this.timer) this.timer = setInterval(function(){t.anim_step_reverse();}, this.interval); |
| 705 | + } |
| 706 | +</script> |
| 707 | +""" |
| 708 | + |
| 709 | + |
| 710 | +DISPLAY_TEMPLATE = """ |
| 711 | +<div class="animation" align="center"> |
| 712 | + <img id="_anim_img{id}"> |
| 713 | + <br> |
| 714 | + <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> |
| 715 | + <br> |
| 716 | + <button onclick="anim{id}.slower()"><i class="fa fa-minus"></i></button> |
| 717 | + <button onclick="anim{id}.first_frame()"><i class="fa fa-fast-backward"></i></button> |
| 718 | + <button onclick="anim{id}.previous_frame()"><i class="fa fa-step-backward"></i></button> |
| 719 | + <button onclick="anim{id}.reverse_animation()"><i class="fa fa-play fa-flip-horizontal"></i></button> |
| 720 | + <button onclick="anim{id}.pause_animation()"><i class="fa fa-pause"></i></button> |
| 721 | + <button onclick="anim{id}.play_animation()"><i class="fa fa-play"></i></button> |
| 722 | + <button onclick="anim{id}.next_frame()"><i class="fa fa-step-forward"></i></button> |
| 723 | + <button onclick="anim{id}.last_frame()"><i class="fa fa-fast-forward"></i></button> |
| 724 | + <button onclick="anim{id}.faster()"><i class="fa fa-plus"></i></button> |
| 725 | + <form action="#n" name="_anim_loop_select{id}" class="anim_control"> |
| 726 | + <input type="radio" name="state" value="once" {once_checked}> Once </input> |
| 727 | + <input type="radio" name="state" value="loop" {loop_checked}> Loop </input> |
| 728 | + <input type="radio" name="state" value="reflect" {reflect_checked}> Reflect </input> |
| 729 | + </form> |
| 730 | +</div> |
| 731 | +
|
| 732 | +
|
| 733 | +<script language="javascript"> |
| 734 | + /* Instantiate the Animation class. */ |
| 735 | + /* The IDs given should match those used in the template above. */ |
| 736 | + (function() {{ |
| 737 | + var img_id = "_anim_img{id}"; |
| 738 | + var slider_id = "_anim_slider{id}"; |
| 739 | + var loop_select_id = "_anim_loop_select{id}"; |
| 740 | + var frames = new Array({Nframes}); |
| 741 | + {fill_frames} |
| 742 | +
|
| 743 | + /* set a timeout to make sure all the above elements are created before |
| 744 | + the object is initialized. */ |
| 745 | + setTimeout(function() {{ |
| 746 | + anim{id} = new Animation(frames, img_id, slider_id, {interval}, loop_select_id); |
| 747 | + }}, 0); |
| 748 | + }})() |
| 749 | +</script> |
| 750 | +""" |
| 751 | + |
| 752 | +INCLUDED_FRAMES = """ |
| 753 | + for (var i=0; i<{Nframes}; i++){{ |
| 754 | + frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) + ".{frame_format}"; |
| 755 | + }} |
| 756 | +""" |
| 757 | + |
| 758 | + |
| 759 | +def _included_frames(frame_list, frame_format): |
| 760 | + """frame_list should be a list of filenames""" |
| 761 | + return INCLUDED_FRAMES.format(Nframes=len(frame_list), |
| 762 | + frame_dir=os.path.dirname(frame_list[0]), |
| 763 | + frame_format=frame_format) |
| 764 | + |
| 765 | + |
| 766 | +def _embedded_frames(frame_list, frame_format): |
| 767 | + """frame_list should be a list of base64-encoded png files""" |
| 768 | + template = ' frames[{0}] = "data:image/{1};base64,{2}"\n' |
| 769 | + embedded = "\n" |
| 770 | + for i, frame_data in enumerate(frame_list): |
| 771 | + embedded += template.format(i, frame_format, |
| 772 | + frame_data.replace('\n', '\\\n')) |
| 773 | + return embedded |
| 774 | + |
| 775 | + |
| 776 | +# Taken directly from jakevdp's JSAnimation package at |
| 777 | +# http://github.com/jakevdp/JSAnimation |
| 778 | +@writers.register('html') |
| 779 | +class HTMLWriter(FileMovieWriter): |
| 780 | + # we start the animation id count at a random number: this way, if two |
| 781 | + # animations are meant to be included on one HTML page, there is a |
| 782 | + # very small chance of conflict. |
| 783 | + rng = random.Random() |
| 784 | + supported_formats = ['png', 'jpeg', 'tiff', 'svg'] |
| 785 | + args_key = 'animation.html_args' |
| 786 | + |
| 787 | + @classmethod |
| 788 | + def isAvailable(cls): |
| 789 | + return True |
| 790 | + |
| 791 | + @classmethod |
| 792 | + def new_id(cls): |
| 793 | + #return '%16x' % cls.rng.getrandbits(64) |
| 794 | + return ''.join(cls.rng.choice(string.ascii_uppercase) |
| 795 | + for x in range(16)) |
| 796 | + |
| 797 | + def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None, |
| 798 | + metadata=None, embed_frames=False, default_mode='loop'): |
| 799 | + self.embed_frames = embed_frames |
| 800 | + self.default_mode = default_mode.lower() |
| 801 | + |
| 802 | + if self.default_mode not in ['loop', 'once', 'reflect']: |
| 803 | + self.default_mode = 'loop' |
| 804 | + import warnings |
| 805 | + warnings.warn("unrecognized default_mode: using 'loop'") |
| 806 | + |
| 807 | + self._saved_frames = list() |
| 808 | + super(HTMLWriter, self).__init__(fps, codec, bitrate, |
| 809 | + extra_args, metadata) |
| 810 | + |
| 811 | + def setup(self, fig, outfile, dpi, frame_dir=None): |
| 812 | + if os.path.splitext(outfile)[-1] not in ['.html', '.htm']: |
| 813 | + raise ValueError("outfile must be *.htm or *.html") |
| 814 | + |
| 815 | + if not self.embed_frames: |
| 816 | + if frame_dir is None: |
| 817 | + frame_dir = outfile.rstrip('.html') + '_frames' |
| 818 | + if not os.path.exists(frame_dir): |
| 819 | + os.makedirs(frame_dir) |
| 820 | + frame_prefix = os.path.join(frame_dir, 'frame') |
| 821 | + else: |
| 822 | + frame_prefix = None |
| 823 | + |
| 824 | + super(HTMLWriter, self).setup(fig, outfile, dpi, |
| 825 | + frame_prefix, clear_temp=False) |
| 826 | + |
| 827 | + def grab_frame(self, **savefig_kwargs): |
| 828 | + if self.embed_frames: |
| 829 | + suffix = '.' + self.frame_format |
| 830 | + f = InMemory() |
| 831 | + self.fig.savefig(f, format=self.frame_format, |
| 832 | +
10000
dpi=self.dpi, **savefig_kwargs) |
| 833 | + f.seek(0) |
| 834 | + imgdata64 = base64.b64encode(f.read()).decode('ascii') |
| 835 | + self._saved_frames.append(imgdata64) |
| 836 | + else: |
| 837 | + return super(HTMLWriter, self).grab_frame(**savefig_kwargs) |
| 838 | + |
| 839 | + def _run(self): |
| 840 | + # make a ducktyped subprocess standin |
| 841 | + # this is called by the MovieWriter base class, but not used here. |
| 842 | + class ProcessStandin(object): |
| 843 | + returncode = 0 |
| 844 | + def communicate(self): |
| 845 | + return ('', '') |
| 846 | + self._proc = ProcessStandin() |
| 847 | + |
| 848 | + # save the frames to an html file |
| 849 | + if self.embed_frames: |
| 850 | + fill_frames = _embedded_frames(self._saved_frames, |
| 851 | + self.frame_format) |
| 852 | + else: |
| 853 | + # temp names is filled by FileMovieWriter |
| 854 | + fill_frames = _included_frames(self._temp_names, |
| 855 | + self.frame_format) |
| 856 | + |
| 857 | + mode_dict = dict(once_checked='', |
| 858 | + loop_checked='', |
| 859 | + reflect_checked='') |
| 860 | + mode_dict[self.default_mode + '_checked'] = 'checked' |
| 861 | + |
| 862 | + interval = int(1000. / self.fps) |
| 863 | + |
| 864 | + with open(self.outfile, 'w') as of: |
| 865 | + of.write(JS_INCLUDE) |
| 866 | + of.write(DISPLAY_TEMPLATE.format(id=self.new_id(), |
| 867 | + Nframes=len(self._temp_names), |
| 868 | + fill_frames=fill_frames, |
| 869 | + interval=interval, |
| 870 | + **mode_dict)) |
| 871 | + |
| 872 | + |
| 873 | +def anim_to_jshtml(anim, fps=None, embed_frames=True, default_mode=None): |
| 874 | + """Generate HTML representation of the animation""" |
| 875 | + if fps is None and hasattr(anim, '_interval'): |
| 876 | + # Convert interval in ms to frames per second |
| 877 | + fps = 1000. / anim._interval |
| 878 | + |
| 879 | + # If we're not given a default mode, choose one base on the value of |
| 880 | + # the repeat attribute |
| 881 | + if default_mode is None: |
| 882 | + default_mode = 'loop' if anim.repeat else 'once' |
| 883 | + |
| 884 | + if hasattr(anim, "_html_representation"): |
| 885 | + return anim._html_representation |
| 886 | + else: |
| 887 | + # Can't open a second time while opened on windows. So we avoid |
| 888 | + # deleting when closed, and delete manually later. |
| 889 | + with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as f: |
| 890 | + anim.save(f.name, writer=HTMLWriter(fps=fps, |
| 891 | + embed_frames=embed_frames, |
| 892 | + default_mode=default_mode)) |
| 893 | + # Re-open and get content |
| 894 | + with open(f.name) as fobj: |
| 895 | + html = fobj.read() |
| 896 | + |
| 897 | + # Now we can delete |
| 898 | + os.remove(f.name) |
| 899 | + |
| 900 | + anim._html_representation = html |
| 901 | + return html |
| 902 | + |
| 903 | + |
564 | 904 | class Animation(object):
|
565 | 905 | '''
|
566 | 906 | This class wraps the creation of an animation using matplotlib. It is
|
@@ -939,6 +1279,8 @@ def _repr_html_(self):
|
939 | 1279 | fmt = rcParams['animation.html']
|
940 | 1280 | if fmt == 'html5':
|
941 | 1281 | return self.to_html5_video()
|
| 1282 | + elif fmt == 'jshtml': |
| 1283 | + return anim_to_jshtml(self) |
942 | 1284 |
|
943 | 1285 |
|
944 | 1286 | class TimedAnimation(Animation):
|
|
0 commit comments