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