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