passagemath-plot 10.6.31rc3__cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of passagemath-plot might be problematic. Click here for more details.
- passagemath_plot-10.6.31rc3.dist-info/METADATA +172 -0
- passagemath_plot-10.6.31rc3.dist-info/RECORD +82 -0
- passagemath_plot-10.6.31rc3.dist-info/WHEEL +6 -0
- passagemath_plot-10.6.31rc3.dist-info/top_level.txt +2 -0
- passagemath_plot.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
- passagemath_plot.libs/libgsl-cda90e79.so.28.0.0 +0 -0
- passagemath_plot.libs/libopenblasp-r0-6dcb67f9.3.29.so +0 -0
- passagemath_plot.libs/libquadmath-2284e583.so.0.0.0 +0 -0
- sage/all__sagemath_plot.py +15 -0
- sage/ext_data/threejs/animation.css +195 -0
- sage/ext_data/threejs/animation.html +85 -0
- sage/ext_data/threejs/animation.js +273 -0
- sage/ext_data/threejs/fat_lines.js +48 -0
- sage/ext_data/threejs/threejs-version.txt +1 -0
- sage/ext_data/threejs/threejs_template.html +597 -0
- sage/interfaces/all__sagemath_plot.py +1 -0
- sage/interfaces/gnuplot.py +196 -0
- sage/interfaces/jmoldata.py +208 -0
- sage/interfaces/povray.py +56 -0
- sage/plot/all.py +42 -0
- sage/plot/animate.py +1796 -0
- sage/plot/arc.py +504 -0
- sage/plot/arrow.py +671 -0
- sage/plot/bar_chart.py +205 -0
- sage/plot/bezier_path.py +400 -0
- sage/plot/circle.py +435 -0
- sage/plot/colors.py +1606 -0
- sage/plot/complex_plot.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/complex_plot.pyx +1446 -0
- sage/plot/contour_plot.py +1792 -0
- sage/plot/density_plot.py +318 -0
- sage/plot/disk.py +373 -0
- sage/plot/ellipse.py +375 -0
- sage/plot/graphics.py +3580 -0
- sage/plot/histogram.py +354 -0
- sage/plot/hyperbolic_arc.py +404 -0
- sage/plot/hyperbolic_polygon.py +416 -0
- sage/plot/hyperbolic_regular_polygon.py +296 -0
- sage/plot/line.py +626 -0
- sage/plot/matrix_plot.py +629 -0
- sage/plot/misc.py +509 -0
- sage/plot/multigraphics.py +1294 -0
- sage/plot/plot.py +4183 -0
- sage/plot/plot3d/all.py +23 -0
- sage/plot/plot3d/base.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/plot3d/base.pxd +12 -0
- sage/plot/plot3d/base.pyx +3378 -0
- sage/plot/plot3d/implicit_plot3d.py +659 -0
- sage/plot/plot3d/implicit_surface.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/plot3d/implicit_surface.pyx +1453 -0
- sage/plot/plot3d/index_face_set.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/plot3d/index_face_set.pxd +32 -0
- sage/plot/plot3d/index_face_set.pyx +1873 -0
- sage/plot/plot3d/introduction.py +131 -0
- sage/plot/plot3d/list_plot3d.py +649 -0
- sage/plot/plot3d/parametric_plot3d.py +1130 -0
- sage/plot/plot3d/parametric_surface.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/plot3d/parametric_surface.pxd +12 -0
- sage/plot/plot3d/parametric_surface.pyx +893 -0
- sage/plot/plot3d/platonic.py +601 -0
- sage/plot/plot3d/plot3d.py +1442 -0
- sage/plot/plot3d/plot_field3d.py +162 -0
- sage/plot/plot3d/point_c.pxi +148 -0
- sage/plot/plot3d/revolution_plot3d.py +309 -0
- sage/plot/plot3d/shapes.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/plot3d/shapes.pxd +22 -0
- sage/plot/plot3d/shapes.pyx +1382 -0
- sage/plot/plot3d/shapes2.py +1512 -0
- sage/plot/plot3d/tachyon.py +1779 -0
- sage/plot/plot3d/texture.py +453 -0
- sage/plot/plot3d/transform.cpython-314-x86_64-linux-gnu.so +0 -0
- sage/plot/plot3d/transform.pxd +21 -0
- sage/plot/plot3d/transform.pyx +268 -0
- sage/plot/plot3d/tri_plot.py +589 -0
- sage/plot/plot_field.py +362 -0
- sage/plot/point.py +624 -0
- sage/plot/polygon.py +562 -0
- sage/plot/primitive.py +249 -0
- sage/plot/scatter_plot.py +199 -0
- sage/plot/step.py +85 -0
- sage/plot/streamline_plot.py +328 -0
- sage/plot/text.py +432 -0
sage/plot/animate.py
ADDED
|
@@ -0,0 +1,1796 @@
|
|
|
1
|
+
# sage_setup: distribution = sagemath-plot
|
|
2
|
+
# sage.doctest: needs sage.symbolic
|
|
3
|
+
r"""
|
|
4
|
+
Animated plots
|
|
5
|
+
|
|
6
|
+
Animations are generated from a list (or other iterable) of graphics
|
|
7
|
+
objects.
|
|
8
|
+
Images are produced by calling the ``save_image`` method on each input
|
|
9
|
+
object, creating a sequence of PNG files.
|
|
10
|
+
These are then assembled to various target formats using different
|
|
11
|
+
tools.
|
|
12
|
+
In particular, the ``magick/convert`` program from ImageMagick_ can be used to
|
|
13
|
+
generate an animated GIF file.
|
|
14
|
+
FFmpeg_ (with the command line program ``ffmpeg``) provides support for
|
|
15
|
+
various video formats, but also an alternative method of generating
|
|
16
|
+
animated GIFs.
|
|
17
|
+
For `browsers which support it`_, APNG_ can be used as another
|
|
18
|
+
alternative which works without any extra dependencies.
|
|
19
|
+
|
|
20
|
+
.. WARNING::
|
|
21
|
+
|
|
22
|
+
Note that ``ImageMagick`` and ``FFmpeg`` are not included with Sage, and
|
|
23
|
+
must be installed by the user. On unix systems, type ``which
|
|
24
|
+
magick`` at a command prompt to see if ``magick`` (part of the
|
|
25
|
+
``ImageMagick`` suite) is installed. If it is, you will be given its
|
|
26
|
+
location. Similarly, you can check for ``ffmpeg`` with ``which
|
|
27
|
+
ffmpeg``. See the websites of ImageMagick_ or FFmpeg_ for
|
|
28
|
+
installation instructions.
|
|
29
|
+
|
|
30
|
+
EXAMPLES:
|
|
31
|
+
|
|
32
|
+
The sine function::
|
|
33
|
+
|
|
34
|
+
sage: x = SR.var("x")
|
|
35
|
+
sage: sines = [plot(c*sin(x), (-2*pi,2*pi), color=Color(c,0,0), ymin=-1, ymax=1)
|
|
36
|
+
....: for c in sxrange(0,1,.2)]
|
|
37
|
+
sage: a = animate(sines)
|
|
38
|
+
sage: print(a)
|
|
39
|
+
Animation with 5 frames
|
|
40
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
41
|
+
|
|
42
|
+
Animate using FFmpeg_ instead of ImageMagick::
|
|
43
|
+
|
|
44
|
+
sage: a.show(use_ffmpeg=True) # long time # optional -- FFmpeg
|
|
45
|
+
|
|
46
|
+
Animate as an APNG_::
|
|
47
|
+
|
|
48
|
+
sage: a.apng(show_path=True) # long time
|
|
49
|
+
Animation saved to ....png.
|
|
50
|
+
|
|
51
|
+
An animated :class:`sage.plot.multigraphics.GraphicsArray` of rotating ellipses::
|
|
52
|
+
|
|
53
|
+
sage: E = animate((graphics_array([[ellipse((0,0), a, b, angle=t, xmin=-3, xmax=3)
|
|
54
|
+
....: + circle((0,0), 3, color='blue')
|
|
55
|
+
....: for a in range(1,3)]
|
|
56
|
+
....: for b in range(2,4)])
|
|
57
|
+
....: for t in sxrange(0, pi/4, .15)))
|
|
58
|
+
sage: str(E) # animations produced from a generator do not have a known length
|
|
59
|
+
'Animation with unknown number of frames'
|
|
60
|
+
sage: E.show() # long time # optional -- ImageMagick
|
|
61
|
+
|
|
62
|
+
A simple animation of a circle shooting up to the right::
|
|
63
|
+
|
|
64
|
+
sage: c = animate([circle((i,i), 1 - 1/(i+1), hue=i/10)
|
|
65
|
+
....: for i in srange(0, 2, 0.2)],
|
|
66
|
+
....: xmin=0, ymin=0, xmax=2, ymax=2, figsize=[2,2])
|
|
67
|
+
sage: c.show() # long time # optional -- ImageMagick
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
Animations of 3d objects::
|
|
71
|
+
|
|
72
|
+
sage: s,t = SR.var("s,t")
|
|
73
|
+
sage: def sphere_and_plane(x):
|
|
74
|
+
....: return (sphere((0,0,0), 1, color='red', opacity=.5)
|
|
75
|
+
....: + parametric_plot3d([t,x,s], (s,-1,1), (t,-1,1),
|
|
76
|
+
....: color='green', opacity=.7))
|
|
77
|
+
sage: sp = animate([sphere_and_plane(x)
|
|
78
|
+
....: for x in sxrange(-1, 1, .3)])
|
|
79
|
+
sage: sp[0] # first frame
|
|
80
|
+
Graphics3d Object
|
|
81
|
+
sage: sp[-1] # last frame
|
|
82
|
+
Graphics3d Object
|
|
83
|
+
sage: sp.show() # long time # optional -- ImageMagick
|
|
84
|
+
|
|
85
|
+
sage: (x,y,z) = SR.var("x,y,z")
|
|
86
|
+
sage: def frame(t):
|
|
87
|
+
....: return implicit_plot3d((x^2 + y^2 + z^2),
|
|
88
|
+
....: (x, -2, 2), (y, -2, 2), (z, -2, 2),
|
|
89
|
+
....: plot_points=60, contour=[1,3,5],
|
|
90
|
+
....: region=lambda x,y,z: x<=t or y>=t or z<=t)
|
|
91
|
+
sage: a = animate([frame(t) for t in srange(.01, 1.5, .2)])
|
|
92
|
+
sage: a[0] # long time
|
|
93
|
+
Graphics3d Object
|
|
94
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
95
|
+
|
|
96
|
+
If the input objects do not have a ``save_image`` method, then the
|
|
97
|
+
animation object attempts to make an image by calling its internal
|
|
98
|
+
method :meth:`sage.plot.animate.Animation.make_image`. This is
|
|
99
|
+
illustrated by the following example::
|
|
100
|
+
|
|
101
|
+
sage: t = SR.var("t")
|
|
102
|
+
sage: a = animate((sin(c*pi*t) for c in sxrange(1, 2, .2)))
|
|
103
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
AUTHORS:
|
|
107
|
+
|
|
108
|
+
- William Stein
|
|
109
|
+
- John Palmieri
|
|
110
|
+
- Niles Johnson (2013-12): Expand to animate more graphics objects
|
|
111
|
+
- Martin von Gagern (2014-12): Added APNG support
|
|
112
|
+
- Joshua Campbell (2020): interactive animation via Three.js viewer
|
|
113
|
+
|
|
114
|
+
REFERENCES:
|
|
115
|
+
|
|
116
|
+
- `ImageMagick <https://www.imagemagick.org>`_
|
|
117
|
+
- `FFmpeg <https://www.ffmpeg.org>`_
|
|
118
|
+
- `APNG <https://wiki.mozilla.org/APNG_Specification>`_
|
|
119
|
+
- `browsers which support it <https://caniuse.com/#feat=apng>`_
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
############################################################################
|
|
123
|
+
# Copyright (C) 2007 William Stein <wstein@gmail.com>
|
|
124
|
+
# Distributed under the terms of the GNU General Public License (GPL)
|
|
125
|
+
# https://www.gnu.org/licenses/
|
|
126
|
+
############################################################################
|
|
127
|
+
|
|
128
|
+
import builtins
|
|
129
|
+
import os
|
|
130
|
+
import shlex
|
|
131
|
+
import struct
|
|
132
|
+
import zlib
|
|
133
|
+
|
|
134
|
+
from sage.misc.fast_methods import WithEqualityById
|
|
135
|
+
from sage.structure.sage_object import SageObject
|
|
136
|
+
from sage.misc.temporary_file import tmp_dir, tmp_filename
|
|
137
|
+
from . import plot
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def animate(frames, **kwds):
|
|
141
|
+
r"""
|
|
142
|
+
Animate a list of frames by creating a
|
|
143
|
+
:class:`sage.plot.animate.Animation` object.
|
|
144
|
+
|
|
145
|
+
EXAMPLES::
|
|
146
|
+
|
|
147
|
+
sage: t = SR.var("t")
|
|
148
|
+
sage: a = animate((cos(c*pi*t) for c in sxrange(1, 2, .2)))
|
|
149
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
150
|
+
|
|
151
|
+
See also :mod:`sage.plot.animate` for more examples.
|
|
152
|
+
"""
|
|
153
|
+
return Animation(frames, **kwds)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Animation(WithEqualityById, SageObject):
|
|
157
|
+
r"""
|
|
158
|
+
Return an animation of a sequence of plots of objects.
|
|
159
|
+
|
|
160
|
+
INPUT:
|
|
161
|
+
|
|
162
|
+
- ``v`` -- iterable of Sage objects; these should preferably be
|
|
163
|
+
graphics objects, but if they aren't, then :meth:`make_image` is
|
|
164
|
+
called on them.
|
|
165
|
+
|
|
166
|
+
- ``xmin``, ``xmax``, ``ymin``, ``ymax`` -- the ranges of the x and y axes
|
|
167
|
+
|
|
168
|
+
- ``**kwds`` -- all additional inputs are passed onto the rendering
|
|
169
|
+
command. E.g., use ``figsize`` to adjust the resolution and aspect
|
|
170
|
+
ratio.
|
|
171
|
+
|
|
172
|
+
EXAMPLES::
|
|
173
|
+
|
|
174
|
+
sage: x = SR.var("x")
|
|
175
|
+
sage: a = animate([sin(x + float(k)) for k in srange(0, 2*pi, 0.3)],
|
|
176
|
+
....: xmin=0, xmax=2*pi, figsize=[2,1])
|
|
177
|
+
sage: print(a)
|
|
178
|
+
Animation with 21 frames
|
|
179
|
+
sage: print(a[:5])
|
|
180
|
+
Animation with 5 frames
|
|
181
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
182
|
+
sage: a[:5].show() # long time # optional -- ImageMagick
|
|
183
|
+
|
|
184
|
+
The :meth:`show` method takes arguments to specify the
|
|
185
|
+
delay between frames (measured in hundredths of a second, default
|
|
186
|
+
value 20) and the number of iterations (default: 0, which
|
|
187
|
+
means to iterate forever). To iterate 4 times with half a second
|
|
188
|
+
between each frame::
|
|
189
|
+
|
|
190
|
+
sage: a.show(delay=50, iterations=4) # long time # optional -- ImageMagick
|
|
191
|
+
|
|
192
|
+
An animation of drawing a parabola::
|
|
193
|
+
|
|
194
|
+
sage: step = 0.1
|
|
195
|
+
sage: L = Graphics()
|
|
196
|
+
sage: v = []
|
|
197
|
+
sage: for i in srange(0, 1, step):
|
|
198
|
+
....: L += line([(i,i^2),(i+step,(i+step)^2)], rgbcolor=(1,0,0), thickness=2)
|
|
199
|
+
....: v.append(L)
|
|
200
|
+
sage: a = animate(v, xmin=0, ymin=0)
|
|
201
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
202
|
+
sage: show(L)
|
|
203
|
+
|
|
204
|
+
TESTS:
|
|
205
|
+
|
|
206
|
+
This illustrates that :issue:`2066` is fixed (setting axes
|
|
207
|
+
ranges when an endpoint is 0)::
|
|
208
|
+
|
|
209
|
+
sage: animate([plot(sin, -1,1)], xmin=0, ymin=0)._kwds['xmin']
|
|
210
|
+
0
|
|
211
|
+
|
|
212
|
+
We check that :issue:`7981` is fixed::
|
|
213
|
+
|
|
214
|
+
sage: x = SR.var("x")
|
|
215
|
+
sage: a = animate([plot(sin(x + float(k)), (0, 2*pi), ymin=-5, ymax=5)
|
|
216
|
+
....: for k in srange(0,2*pi,0.3)])
|
|
217
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
218
|
+
|
|
219
|
+
Do not convert input iterator to a list, but ensure that
|
|
220
|
+
the frame count is known after rendering the frames::
|
|
221
|
+
|
|
222
|
+
sage: x = SR.var("x")
|
|
223
|
+
sage: a = animate((plot(x^p, (x,0,2)) for p in sxrange(1,2,.1)))
|
|
224
|
+
sage: str(a)
|
|
225
|
+
'Animation with unknown number of frames'
|
|
226
|
+
sage: a.png() # long time
|
|
227
|
+
'.../'
|
|
228
|
+
sage: len(a) # long time
|
|
229
|
+
10
|
|
230
|
+
sage: a._frames
|
|
231
|
+
<generator object ...
|
|
232
|
+
|
|
233
|
+
sage: from sage.plot.animate import Animation
|
|
234
|
+
sage: hash(Animation()) # random
|
|
235
|
+
140658972348064
|
|
236
|
+
"""
|
|
237
|
+
def __init__(self, v=None, **kwds):
|
|
238
|
+
r"""
|
|
239
|
+
Return an animation of a sequence of plots of objects. See
|
|
240
|
+
documentation of :func:`animate` for more details and
|
|
241
|
+
examples.
|
|
242
|
+
|
|
243
|
+
EXAMPLES::
|
|
244
|
+
|
|
245
|
+
sage: x = SR.var("x")
|
|
246
|
+
sage: a = animate([sin(x + float(k)) for k in srange(0,2*pi,0.3)], # indirect doctest
|
|
247
|
+
....: xmin=0, xmax=2*pi, figsize=[2,1])
|
|
248
|
+
sage: print(a)
|
|
249
|
+
Animation with 21 frames
|
|
250
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
251
|
+
"""
|
|
252
|
+
self._frames = v
|
|
253
|
+
self._kwds = kwds
|
|
254
|
+
|
|
255
|
+
def _combine_kwds(self, *kwds_tuple):
|
|
256
|
+
"""
|
|
257
|
+
Return a dictionary which is a combination of the all the
|
|
258
|
+
dictionaries in kwds_tuple. This also does the appropriate thing
|
|
259
|
+
for taking the mins and maxes of all of the x/y mins/maxes.
|
|
260
|
+
|
|
261
|
+
EXAMPLES::
|
|
262
|
+
|
|
263
|
+
sage: a = animate([plot(sin, -1,1)], xmin=0, ymin=0)
|
|
264
|
+
sage: kwds1 = {'a':1, 'b':2, 'xmin':2, 'xmax':5}
|
|
265
|
+
sage: kwds2 = {'b':3, 'xmin':0, 'xmax':4}
|
|
266
|
+
sage: kwds = a._combine_kwds(kwds1, kwds2)
|
|
267
|
+
sage: list(sorted(kwds.items()))
|
|
268
|
+
[('a', 1), ('b', 3), ('xmax', 5), ('xmin', 0)]
|
|
269
|
+
|
|
270
|
+
Test that the bug reported in :issue:`12107` has been fixed::
|
|
271
|
+
|
|
272
|
+
sage: kwds3 = {}
|
|
273
|
+
sage: kwds4 = {'b':3, 'xmin':0, 'xmax':4}
|
|
274
|
+
sage: a._combine_kwds(kwds3, kwds4)['xmin']
|
|
275
|
+
0
|
|
276
|
+
"""
|
|
277
|
+
new_kwds = {}
|
|
278
|
+
|
|
279
|
+
for kwds in kwds_tuple:
|
|
280
|
+
new_kwds.update(kwds)
|
|
281
|
+
|
|
282
|
+
for name in ['xmin', 'xmax', 'ymin', 'ymax']:
|
|
283
|
+
values = [v for v in [kwds.get(name, None) for kwds in kwds_tuple] if v is not None]
|
|
284
|
+
if values:
|
|
285
|
+
new_kwds[name] = getattr(builtins, name[1:])(values)
|
|
286
|
+
return new_kwds
|
|
287
|
+
|
|
288
|
+
def __getitem__(self, i):
|
|
289
|
+
"""
|
|
290
|
+
Get a frame from an animation or
|
|
291
|
+
slice this animation returning a subanimation.
|
|
292
|
+
|
|
293
|
+
EXAMPLES::
|
|
294
|
+
|
|
295
|
+
sage: a = animate([circle((i,-i), 1-1/(i+1), hue=i/10) for i in srange(0,2,0.2)],
|
|
296
|
+
....: xmin=0,ymin=-2,xmax=2,ymax=0,figsize=[2,2])
|
|
297
|
+
sage: print(a)
|
|
298
|
+
Animation with 10 frames
|
|
299
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
300
|
+
sage: frame2 = a[2] # indirect doctest
|
|
301
|
+
sage: frame2.show()
|
|
302
|
+
sage: print(a[3:7]) # indirect doctest
|
|
303
|
+
Animation with 4 frames
|
|
304
|
+
sage: a[3:7].show() # long time # optional -- ImageMagick
|
|
305
|
+
"""
|
|
306
|
+
if isinstance(i, slice):
|
|
307
|
+
return Animation(self._frames[i], **self._kwds)
|
|
308
|
+
else:
|
|
309
|
+
return self._frames[i]
|
|
310
|
+
|
|
311
|
+
def _repr_(self):
|
|
312
|
+
"""
|
|
313
|
+
Print representation for an animation.
|
|
314
|
+
|
|
315
|
+
EXAMPLES::
|
|
316
|
+
|
|
317
|
+
sage: a = animate([circle((i,-i), 1-1/(i+1), hue=i/10) for i in srange(0,2,0.2)],
|
|
318
|
+
....: xmin=0,ymin=-2,xmax=2,ymax=0,figsize=[2,2])
|
|
319
|
+
sage: print(a)
|
|
320
|
+
Animation with 10 frames
|
|
321
|
+
sage: a._repr_()
|
|
322
|
+
'Animation with 10 frames'
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
num = len(self)
|
|
326
|
+
except TypeError:
|
|
327
|
+
num = "unknown number of"
|
|
328
|
+
return "Animation with %s frames" % num
|
|
329
|
+
|
|
330
|
+
def __add__(self, other):
|
|
331
|
+
"""
|
|
332
|
+
Add two animations. This has the effect of superimposing the two
|
|
333
|
+
animations frame-by-frame.
|
|
334
|
+
|
|
335
|
+
EXAMPLES::
|
|
336
|
+
|
|
337
|
+
sage: a = animate([line([(0,0),(1,i)],hue=0/3) for i in range(2)],
|
|
338
|
+
....: xmin=0, ymin=0, xmax=1, ymax=1,
|
|
339
|
+
....: figsize=[1,1], axes=False)
|
|
340
|
+
sage: print(a)
|
|
341
|
+
Animation with 2 frames
|
|
342
|
+
sage: a.show() # optional -- ImageMagick
|
|
343
|
+
sage: b = animate([line([(0,0),(i,1)],hue=2/3) for i in range(2)],
|
|
344
|
+
....: xmin=0, ymin=0, xmax=1, ymax=1,
|
|
345
|
+
....: figsize=[1,1], axes=False)
|
|
346
|
+
sage: print(b)
|
|
347
|
+
Animation with 2 frames
|
|
348
|
+
sage: b.show() # optional -- ImageMagick
|
|
349
|
+
sage: s = a+b # indirect doctest
|
|
350
|
+
sage: print(s)
|
|
351
|
+
Animation with 2 frames
|
|
352
|
+
sage: len(a), len(b)
|
|
353
|
+
(2, 2)
|
|
354
|
+
sage: len(s)
|
|
355
|
+
2
|
|
356
|
+
sage: s.show() # optional -- ImageMagick
|
|
357
|
+
"""
|
|
358
|
+
if not isinstance(other, Animation):
|
|
359
|
+
other = Animation(other)
|
|
360
|
+
|
|
361
|
+
kwds = self._combine_kwds(self._kwds, other._kwds)
|
|
362
|
+
|
|
363
|
+
#Combine the frames
|
|
364
|
+
m = max(len(self), len(other))
|
|
365
|
+
frames = [a+b for a,b in zip(self._frames, other._frames)]
|
|
366
|
+
frames += self._frames[m:] + other._frames[m:]
|
|
367
|
+
|
|
368
|
+
return Animation(frames, **kwds)
|
|
369
|
+
|
|
370
|
+
def __mul__(self, other):
|
|
371
|
+
"""
|
|
372
|
+
Multiply two animations. This has the effect of appending the two
|
|
373
|
+
animations (the second comes after the first).
|
|
374
|
+
|
|
375
|
+
EXAMPLES::
|
|
376
|
+
|
|
377
|
+
sage: a = animate([line([(0,0),(1,i)],hue=0/3) for i in range(2)],
|
|
378
|
+
....: xmin=0, ymin=0, xmax=1, ymax=1,
|
|
379
|
+
....: figsize=[1,1], axes=False)
|
|
380
|
+
sage: print(a)
|
|
381
|
+
Animation with 2 frames
|
|
382
|
+
sage: a.show() # optional -- ImageMagick
|
|
383
|
+
sage: b = animate([line([(0,0),(i,1)],hue=2/3) for i in range(2)],
|
|
384
|
+
....: xmin=0, ymin=0, xmax=1, ymax=1,
|
|
385
|
+
....: figsize=[1,1], axes=False)
|
|
386
|
+
sage: print(b)
|
|
387
|
+
Animation with 2 frames
|
|
388
|
+
sage: b.show() # optional -- ImageMagick
|
|
389
|
+
sage: p = a*b # indirect doctest
|
|
390
|
+
sage: len(a), len(b)
|
|
391
|
+
(2, 2)
|
|
392
|
+
sage: len(p)
|
|
393
|
+
4
|
|
394
|
+
sage: print(p)
|
|
395
|
+
Animation with 4 frames
|
|
396
|
+
sage: p.show() # optional -- ImageMagick
|
|
397
|
+
"""
|
|
398
|
+
if not isinstance(other, Animation):
|
|
399
|
+
other = Animation(other)
|
|
400
|
+
|
|
401
|
+
kwds = self._combine_kwds(self._kwds, other._kwds)
|
|
402
|
+
|
|
403
|
+
return Animation(self._frames + other._frames, **kwds)
|
|
404
|
+
|
|
405
|
+
def __len__(self):
|
|
406
|
+
"""
|
|
407
|
+
Length of ``self``.
|
|
408
|
+
|
|
409
|
+
EXAMPLES::
|
|
410
|
+
|
|
411
|
+
sage: a = animate([circle((i,0),1,thickness=20*i) for i in srange(0,2,0.4)],
|
|
412
|
+
....: xmin=0, ymin=-1, xmax=3, ymax=1, figsize=[2,1], axes=False)
|
|
413
|
+
sage: len(a)
|
|
414
|
+
5
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
return self._num_frames
|
|
418
|
+
except AttributeError:
|
|
419
|
+
return len(self._frames)
|
|
420
|
+
|
|
421
|
+
def make_image(self, frame, filename, **kwds):
|
|
422
|
+
r"""
|
|
423
|
+
Given a frame which has no ``save_image()`` method, make a graphics
|
|
424
|
+
object and save it as an image with the given filename. By default, this is
|
|
425
|
+
:meth:`sage.plot.plot.plot`. To make animations of other objects,
|
|
426
|
+
override this method in a subclass.
|
|
427
|
+
|
|
428
|
+
EXAMPLES::
|
|
429
|
+
|
|
430
|
+
sage: from sage.plot.animate import Animation
|
|
431
|
+
sage: class MyAnimation(Animation):
|
|
432
|
+
....: def make_image(self, frame, filename, **kwds):
|
|
433
|
+
....: P = parametric_plot(frame[0], frame[1], **frame[2])
|
|
434
|
+
....: P.save_image(filename, **kwds)
|
|
435
|
+
|
|
436
|
+
sage: t = SR.var("t")
|
|
437
|
+
sage: x = lambda t: cos(t)
|
|
438
|
+
sage: y = lambda n,t: sin(t)/n
|
|
439
|
+
sage: B = MyAnimation([([x(t), y(i+1,t)], (t,0,1),
|
|
440
|
+
....: {'color':Color((1,0,i/4)), 'aspect_ratio':1, 'ymax':1})
|
|
441
|
+
....: for i in range(4)])
|
|
442
|
+
|
|
443
|
+
sage: d = B.png(); v = os.listdir(d); v.sort(); v # long time
|
|
444
|
+
['00000000.png', '00000001.png', '00000002.png', '00000003.png']
|
|
445
|
+
sage: B.show() # not tested
|
|
446
|
+
|
|
447
|
+
sage: class MyAnimation(Animation):
|
|
448
|
+
....: def make_image(self, frame, filename, **kwds):
|
|
449
|
+
....: G = frame.plot()
|
|
450
|
+
....: G.set_axes_range(floor(G.xmin()), ceil(G.xmax()),
|
|
451
|
+
....: floor(G.ymin()), ceil(G.ymax()))
|
|
452
|
+
....: G.save_image(filename, **kwds)
|
|
453
|
+
|
|
454
|
+
sage: B = MyAnimation([graphs.CompleteGraph(n)
|
|
455
|
+
....: for n in range(7,11)], figsize=5)
|
|
456
|
+
sage: d = B.png()
|
|
457
|
+
sage: v = os.listdir(d); v.sort(); v
|
|
458
|
+
['00000000.png', '00000001.png', '00000002.png', '00000003.png']
|
|
459
|
+
sage: B.show() # not tested
|
|
460
|
+
"""
|
|
461
|
+
p = plot.plot(frame, **kwds)
|
|
462
|
+
p.save_image(filename)
|
|
463
|
+
|
|
464
|
+
def png(self, dir=None):
|
|
465
|
+
r"""
|
|
466
|
+
Render PNG images of the frames in this animation, saving them
|
|
467
|
+
in ``dir``. Return the absolute path to that directory. If
|
|
468
|
+
the frames have been previously rendered and ``dir`` is
|
|
469
|
+
``None``, just return the directory in which they are stored.
|
|
470
|
+
|
|
471
|
+
When ``dir`` is other than ``None``, force re-rendering of
|
|
472
|
+
frames.
|
|
473
|
+
|
|
474
|
+
INPUT:
|
|
475
|
+
|
|
476
|
+
- ``dir`` -- (default: ``None``) directory in which to store frames; in
|
|
477
|
+
this case, a temporary directory will be created for storing the frames
|
|
478
|
+
|
|
479
|
+
OUTPUT: absolute path to the directory containing the PNG images
|
|
480
|
+
|
|
481
|
+
EXAMPLES::
|
|
482
|
+
|
|
483
|
+
sage: x = SR.var("x")
|
|
484
|
+
sage: a = animate([plot(x^2 + n) for n in range(4)], ymin=0, ymax=4)
|
|
485
|
+
sage: d = a.png(); v = os.listdir(d); v.sort(); v # long time
|
|
486
|
+
['00000000.png', '00000001.png', '00000002.png', '00000003.png']
|
|
487
|
+
"""
|
|
488
|
+
if dir is None:
|
|
489
|
+
try:
|
|
490
|
+
return self._png_dir
|
|
491
|
+
except AttributeError:
|
|
492
|
+
pass
|
|
493
|
+
dir = tmp_dir()
|
|
494
|
+
i = 0
|
|
495
|
+
for frame in self._frames:
|
|
496
|
+
filename = '%s/%08d.png' % (dir,i)
|
|
497
|
+
try:
|
|
498
|
+
save_image = frame.save_image
|
|
499
|
+
except AttributeError:
|
|
500
|
+
self.make_image(frame, filename, **self._kwds)
|
|
501
|
+
else:
|
|
502
|
+
save_image(filename, **self._kwds)
|
|
503
|
+
i += 1
|
|
504
|
+
self._num_frames = i
|
|
505
|
+
self._png_dir = dir
|
|
506
|
+
return dir
|
|
507
|
+
|
|
508
|
+
def graphics_array(self, ncols=3):
|
|
509
|
+
r"""
|
|
510
|
+
Return a :class:`sage.plot.multigraphics.GraphicsArray` with plots of the
|
|
511
|
+
frames of this animation, using the given number of columns.
|
|
512
|
+
The frames must be acceptable inputs for
|
|
513
|
+
:class:`sage.plot.multigraphics.GraphicsArray`.
|
|
514
|
+
|
|
515
|
+
EXAMPLES::
|
|
516
|
+
|
|
517
|
+
sage: # needs sage.schemes
|
|
518
|
+
sage: E = EllipticCurve('37a')
|
|
519
|
+
sage: v = [E.change_ring(GF(p)).plot(pointsize=30)
|
|
520
|
+
....: for p in [97, 101, 103]]
|
|
521
|
+
sage: a = animate(v, xmin=0, ymin=0, axes=False)
|
|
522
|
+
sage: print(a)
|
|
523
|
+
Animation with 3 frames
|
|
524
|
+
sage: a.show() # optional -- ImageMagick
|
|
525
|
+
|
|
526
|
+
Modify the default arrangement of array::
|
|
527
|
+
|
|
528
|
+
sage: g = a.graphics_array(); print(g) # needs sage.schemes
|
|
529
|
+
Graphics Array of size 1 x 3
|
|
530
|
+
sage: g.show(figsize=[6,3]) # needs sage.schemes
|
|
531
|
+
|
|
532
|
+
Specify different arrangement of array and save it with a given file name::
|
|
533
|
+
|
|
534
|
+
sage: g = a.graphics_array(ncols=2); print(g) # needs sage.schemes
|
|
535
|
+
Graphics Array of size 2 x 2
|
|
536
|
+
sage: f = tmp_filename(ext='.png'); print(f) # needs sage.schemes
|
|
537
|
+
...png
|
|
538
|
+
sage: g.save(f) # needs sage.schemes
|
|
539
|
+
|
|
540
|
+
Frames can be specified as a generator too; it is internally converted to a list::
|
|
541
|
+
|
|
542
|
+
sage: t = SR.var("t")
|
|
543
|
+
sage: b = animate((plot(sin(c*pi*t)) for c in sxrange(1,2,.2)))
|
|
544
|
+
sage: g = b.graphics_array()
|
|
545
|
+
sage: print(g)
|
|
546
|
+
Graphics Array of size 2 x 3
|
|
547
|
+
"""
|
|
548
|
+
ncols = int(ncols)
|
|
549
|
+
frame_list = list(self._frames)
|
|
550
|
+
n = len(frame_list)
|
|
551
|
+
nrows, rem = divmod(n,ncols)
|
|
552
|
+
if rem > 0:
|
|
553
|
+
nrows += 1
|
|
554
|
+
return plot.graphics_array(frame_list, nrows, ncols)
|
|
555
|
+
|
|
556
|
+
def gif(self, delay=20, savefile=None, iterations=0, show_path=False,
|
|
557
|
+
use_ffmpeg=False):
|
|
558
|
+
r"""
|
|
559
|
+
Return an animated gif composed from rendering the graphics
|
|
560
|
+
objects in ``self``.
|
|
561
|
+
|
|
562
|
+
This method will only work if either (a) the ImageMagick
|
|
563
|
+
software suite is installed, i.e., you have the ``magick/convert``
|
|
564
|
+
command or (b) ``ffmpeg`` is installed. See the web sites of
|
|
565
|
+
ImageMagick_ and FFmpeg_ for more details. By default, this
|
|
566
|
+
produces the gif using Imagemagick if it is present. If this
|
|
567
|
+
can't find ImageMagick or if ``use_ffmpeg`` is True, then it
|
|
568
|
+
uses ``ffmpeg`` instead.
|
|
569
|
+
|
|
570
|
+
INPUT:
|
|
571
|
+
|
|
572
|
+
- ``delay`` -- (default: 20) delay in hundredths of a
|
|
573
|
+
second between frames
|
|
574
|
+
|
|
575
|
+
- ``savefile`` -- file that the animated gif gets saved
|
|
576
|
+
to
|
|
577
|
+
|
|
578
|
+
- ``iterations`` -- integer (default: 0); number of
|
|
579
|
+
iterations of animation. If 0, loop forever.
|
|
580
|
+
|
|
581
|
+
- ``show_path`` -- boolean (default: ``False``); if True,
|
|
582
|
+
print the path to the saved file
|
|
583
|
+
|
|
584
|
+
- ``use_ffmpeg`` -- boolean (default: ``False``); if ``True``, use
|
|
585
|
+
'ffmpeg' by default instead of ImageMagick
|
|
586
|
+
|
|
587
|
+
If ``savefile`` is not specified: in notebook mode, display the
|
|
588
|
+
animation; otherwise, save it to a default file name.
|
|
589
|
+
|
|
590
|
+
EXAMPLES::
|
|
591
|
+
|
|
592
|
+
sage: x = SR.var("x")
|
|
593
|
+
sage: a = animate([sin(x + float(k))
|
|
594
|
+
....: for k in srange(0,2*pi,0.7)],
|
|
595
|
+
....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
|
|
596
|
+
sage: td = tmp_dir()
|
|
597
|
+
sage: a.gif() # not tested
|
|
598
|
+
sage: a.gif(savefile=td + 'my_animation.gif', # long time # optional -- ImageMagick
|
|
599
|
+
....: delay=35, iterations=3)
|
|
600
|
+
sage: with open(td + 'my_animation.gif', 'rb') as f: # long time # optional -- ImageMagick
|
|
601
|
+
....: print(b'GIF8' in f.read())
|
|
602
|
+
True
|
|
603
|
+
sage: a.gif(savefile=td + 'my_animation.gif', # long time # optional -- ImageMagick
|
|
604
|
+
....: show_path=True)
|
|
605
|
+
Animation saved to .../my_animation.gif.
|
|
606
|
+
sage: a.gif(savefile=td + 'my_animation_2.gif', # long time # optional -- FFmpeg
|
|
607
|
+
....: show_path=True, use_ffmpeg=True)
|
|
608
|
+
Animation saved to .../my_animation_2.gif.
|
|
609
|
+
|
|
610
|
+
.. NOTE::
|
|
611
|
+
|
|
612
|
+
If neither ffmpeg nor ImageMagick is installed, you will
|
|
613
|
+
get an error message like this::
|
|
614
|
+
|
|
615
|
+
Error: Neither ImageMagick nor ffmpeg appears to be installed. Saving an
|
|
616
|
+
animation to a GIF file or displaying an animation requires one of these
|
|
617
|
+
packages, so please install one of them and try again.
|
|
618
|
+
|
|
619
|
+
See www.imagemagick.org and www.ffmpeg.org for more information.
|
|
620
|
+
"""
|
|
621
|
+
from sage.features.imagemagick import ImageMagick
|
|
622
|
+
from sage.features.ffmpeg import FFmpeg
|
|
623
|
+
|
|
624
|
+
if not ImageMagick().is_present() and not FFmpeg().is_present():
|
|
625
|
+
raise OSError("Error: Neither ImageMagick nor ffmpeg appear to "
|
|
626
|
+
"be installed. Saving an animation to a GIF file or "
|
|
627
|
+
"displaying an animation requires one of these "
|
|
628
|
+
"packages, so please install one of them and try "
|
|
629
|
+
"again. See www.imagemagick.org and www.ffmpeg.org "
|
|
630
|
+
"for more information.")
|
|
631
|
+
|
|
632
|
+
if use_ffmpeg or not ImageMagick().is_present():
|
|
633
|
+
self.ffmpeg(savefile=savefile, show_path=show_path,
|
|
634
|
+
output_format='.gif', delay=delay,
|
|
635
|
+
iterations=iterations)
|
|
636
|
+
else:
|
|
637
|
+
self._gif_from_imagemagick(savefile=savefile, show_path=show_path,
|
|
638
|
+
delay=delay, iterations=iterations)
|
|
639
|
+
|
|
640
|
+
def _gif_from_imagemagick(self, savefile=None, show_path=False,
|
|
641
|
+
delay=20, iterations=0):
|
|
642
|
+
r"""
|
|
643
|
+
Return a movie showing an animation composed from rendering
|
|
644
|
+
the frames in ``self``.
|
|
645
|
+
|
|
646
|
+
This method will only work if ``imagemagick`` is installed (command
|
|
647
|
+
``magick`` or ``convert``). See https://www.imagemagick.org for information
|
|
648
|
+
about ``imagemagick``.
|
|
649
|
+
|
|
650
|
+
INPUT:
|
|
651
|
+
|
|
652
|
+
- ``savefile`` -- file that the mpeg gets saved to
|
|
653
|
+
|
|
654
|
+
.. warning::
|
|
655
|
+
|
|
656
|
+
This will overwrite ``savefile`` if it already exists.
|
|
657
|
+
|
|
658
|
+
- ``show_path`` -- boolean (default: ``False``); if ``True``,
|
|
659
|
+
print the path to the saved file
|
|
660
|
+
|
|
661
|
+
- ``delay`` -- (default: 20) delay in hundredths of a
|
|
662
|
+
second between frames
|
|
663
|
+
|
|
664
|
+
- ``iterations`` -- integer (default: 0); number of iterations
|
|
665
|
+
of animation. If 0, loop forever.
|
|
666
|
+
|
|
667
|
+
If ``savefile`` is not specified: in notebook mode, display
|
|
668
|
+
the animation; otherwise, save it to a default file name. Use
|
|
669
|
+
:func:`sage.misc.verbose.set_verbose` with ``level=1`` to see
|
|
670
|
+
additional output.
|
|
671
|
+
|
|
672
|
+
EXAMPLES::
|
|
673
|
+
|
|
674
|
+
sage: x = SR.var("x")
|
|
675
|
+
sage: a = animate([sin(x + float(k)) for k in srange(0,2*pi,0.7)],
|
|
676
|
+
....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
|
|
677
|
+
sage: td = tmp_dir()
|
|
678
|
+
sage: a._gif_from_imagemagick(savefile=td + 'new.gif') # long time # optional -- ImageMagick
|
|
679
|
+
|
|
680
|
+
.. NOTE::
|
|
681
|
+
|
|
682
|
+
If imagemagick is not installed, you will get an error message
|
|
683
|
+
like this::
|
|
684
|
+
|
|
685
|
+
FeatureNotPresentError: imagemagick is not available.
|
|
686
|
+
Executable 'magick' not found on PATH.
|
|
687
|
+
Further installation instructions might be available at
|
|
688
|
+
https://www.imagemagick.org/.
|
|
689
|
+
"""
|
|
690
|
+
from sage.features.imagemagick import ImageMagick, Magick
|
|
691
|
+
ImageMagick().require()
|
|
692
|
+
|
|
693
|
+
if not savefile:
|
|
694
|
+
savefile = tmp_filename(ext='.gif')
|
|
695
|
+
if not savefile.endswith('.gif'):
|
|
696
|
+
savefile += '.gif'
|
|
697
|
+
savefile = os.path.abspath(savefile)
|
|
698
|
+
|
|
699
|
+
# running the command
|
|
700
|
+
directory = self.png()
|
|
701
|
+
cmd = [Magick().executable, '-dispose', 'Background',
|
|
702
|
+
'-delay', '%s' % int(delay), '-loop', '%s' % int(iterations),
|
|
703
|
+
'*.png', savefile]
|
|
704
|
+
from subprocess import run
|
|
705
|
+
result = run(cmd, cwd=directory, capture_output=True, text=True)
|
|
706
|
+
|
|
707
|
+
# If a problem with the command occurs, print the log before
|
|
708
|
+
# raising an error (more verbose than result.check_returncode())
|
|
709
|
+
if result.returncode:
|
|
710
|
+
print('Command "{}" returned nonzero exit status "{}" '
|
|
711
|
+
'(with stderr "{}" and stdout "{}").'.format(result.args,
|
|
712
|
+
result.returncode,
|
|
713
|
+
result.stderr.strip(),
|
|
714
|
+
result.stdout.strip()))
|
|
715
|
+
raise OSError("Error: Cannot generate GIF animation. "
|
|
716
|
+
"The magick/convert command (ImageMagick) is present but does "
|
|
717
|
+
"not seem to be functional. Verify that the objects "
|
|
718
|
+
"passed to the animate command can be saved in PNG "
|
|
719
|
+
"image format. "
|
|
720
|
+
"See www.imagemagick.org more information.")
|
|
721
|
+
|
|
722
|
+
if show_path:
|
|
723
|
+
print("Animation saved to file %s." % savefile)
|
|
724
|
+
|
|
725
|
+
def _rich_repr_(self, display_manager, **kwds):
|
|
726
|
+
"""
|
|
727
|
+
Rich Output Magic Method.
|
|
728
|
+
|
|
729
|
+
See :mod:`sage.repl.rich_output` for details.
|
|
730
|
+
|
|
731
|
+
EXAMPLES::
|
|
732
|
+
|
|
733
|
+
sage: x = SR.var("x")
|
|
734
|
+
sage: a = animate([plot(x^2 + n) for n in range(2)], ymin=0, ymax=4)
|
|
735
|
+
sage: from sage.repl.rich_output import get_display_manager
|
|
736
|
+
sage: dm = get_display_manager()
|
|
737
|
+
sage: a._rich_repr_(dm) # optional -- ImageMagick
|
|
738
|
+
OutputImageGif container
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
iterations = kwds.get('iterations', 0)
|
|
742
|
+
loop = (iterations == 0)
|
|
743
|
+
|
|
744
|
+
t = display_manager.types
|
|
745
|
+
supported = display_manager.supported_output()
|
|
746
|
+
format = kwds.pop("format", None)
|
|
747
|
+
if format is None:
|
|
748
|
+
if t.OutputImageGif in supported:
|
|
749
|
+
format = "gif"
|
|
750
|
+
else:
|
|
751
|
+
return # No supported format could be guessed
|
|
752
|
+
suffix = None
|
|
753
|
+
outputType = None
|
|
754
|
+
if format == "gif":
|
|
755
|
+
outputType = t.OutputImageGif
|
|
756
|
+
suffix = ".gif"
|
|
757
|
+
if format == "ogg":
|
|
758
|
+
outputType = t.OutputVideoOgg
|
|
759
|
+
if format == "webm":
|
|
760
|
+
outputType = t.OutputVideoWebM
|
|
761
|
+
if format == "mp4":
|
|
762
|
+
outputType = t.OutputVideoMp4
|
|
763
|
+
if format == "flash":
|
|
764
|
+
outputType = t.OutputVideoFlash
|
|
765
|
+
if format == "matroska":
|
|
766
|
+
outputType = t.OutputVideoMatroska
|
|
767
|
+
if format == "avi":
|
|
768
|
+
outputType = t.OutputVideoAvi
|
|
769
|
+
if format == "wmv":
|
|
770
|
+
outputType = t.OutputVideoWmv
|
|
771
|
+
if format == "quicktime":
|
|
772
|
+
outputType = t.OutputVideoQuicktime
|
|
773
|
+
if format is None:
|
|
774
|
+
raise ValueError("Unknown video format")
|
|
775
|
+
if outputType not in supported:
|
|
776
|
+
return # Sorry, requested format is not supported
|
|
777
|
+
if suffix is not None:
|
|
778
|
+
return display_manager.graphics_from_save(
|
|
779
|
+
self.save, kwds, suffix, outputType)
|
|
780
|
+
|
|
781
|
+
# Now we save for OutputVideoBase
|
|
782
|
+
filename = tmp_filename(ext=outputType.ext)
|
|
783
|
+
self.save(filename, **kwds)
|
|
784
|
+
from sage.repl.rich_output.buffer import OutputBuffer
|
|
785
|
+
buf = OutputBuffer.from_file(filename)
|
|
786
|
+
return outputType(buf, loop=loop)
|
|
787
|
+
|
|
788
|
+
def show(self, delay=None, iterations=None, **kwds):
|
|
789
|
+
r"""
|
|
790
|
+
Show this animation immediately.
|
|
791
|
+
|
|
792
|
+
This method attempts to display the graphics immediately,
|
|
793
|
+
without waiting for the currently running code (if any) to
|
|
794
|
+
return to the command line. Be careful, calling it from within
|
|
795
|
+
a loop will potentially launch a large number of external
|
|
796
|
+
viewer programs.
|
|
797
|
+
|
|
798
|
+
INPUT:
|
|
799
|
+
|
|
800
|
+
- ``delay`` -- (default: 20) delay in hundredths of a
|
|
801
|
+
second between frames
|
|
802
|
+
|
|
803
|
+
- ``iterations`` -- integer (default: 0); number of
|
|
804
|
+
iterations of animation. If 0, loop forever.
|
|
805
|
+
|
|
806
|
+
- ``format`` -- (default: gif) format to use for output.
|
|
807
|
+
Currently supported formats are: gif,
|
|
808
|
+
ogg, webm, mp4, flash, matroska, avi, wmv, quicktime.
|
|
809
|
+
|
|
810
|
+
OUTPUT:
|
|
811
|
+
|
|
812
|
+
This method does not return anything. Use :meth:`save` if you
|
|
813
|
+
want to save the figure as an image.
|
|
814
|
+
|
|
815
|
+
.. NOTE::
|
|
816
|
+
|
|
817
|
+
Currently this is done using an animated gif, though this
|
|
818
|
+
could change in the future. This requires that either
|
|
819
|
+
ffmpeg or the ImageMagick suite (in particular, the
|
|
820
|
+
``magick/convert`` command) is installed.
|
|
821
|
+
|
|
822
|
+
See also the :meth:`ffmpeg` method.
|
|
823
|
+
|
|
824
|
+
EXAMPLES::
|
|
825
|
+
|
|
826
|
+
sage: x = SR.var("x")
|
|
827
|
+
sage: a = animate([sin(x + float(k))
|
|
828
|
+
....: for k in srange(0,2*pi,0.7)],
|
|
829
|
+
....: xmin=0, xmax=2*pi, figsize=[2,1])
|
|
830
|
+
sage: a.show() # long time # optional -- ImageMagick
|
|
831
|
+
|
|
832
|
+
The preceding will loop the animation forever. If you want to show
|
|
833
|
+
only three iterations instead::
|
|
834
|
+
|
|
835
|
+
sage: a.show(iterations=3) # long time # optional -- ImageMagick
|
|
836
|
+
|
|
837
|
+
To put a half-second delay between frames::
|
|
838
|
+
|
|
839
|
+
sage: a.show(delay=50) # long time # optional -- ImageMagick
|
|
840
|
+
|
|
841
|
+
You can also make use of the HTML5 video element in the Sage notebook::
|
|
842
|
+
|
|
843
|
+
sage: # long time, optional -- FFmpeg
|
|
844
|
+
sage: a.show(format='ogg')
|
|
845
|
+
sage: a.show(format='webm')
|
|
846
|
+
sage: a.show(format='mp4')
|
|
847
|
+
sage: a.show(format='webm', iterations=1)
|
|
848
|
+
|
|
849
|
+
Other backends may support other file formats as well::
|
|
850
|
+
|
|
851
|
+
sage: # long time, optional -- FFmpeg
|
|
852
|
+
sage: a.show(format='flash')
|
|
853
|
+
sage: a.show(format='matroska')
|
|
854
|
+
sage: a.show(format='avi')
|
|
855
|
+
sage: a.show(format='wmv')
|
|
856
|
+
sage: a.show(format='quicktime')
|
|
857
|
+
|
|
858
|
+
TESTS:
|
|
859
|
+
|
|
860
|
+
Use of positional parameters is discouraged, will likely get
|
|
861
|
+
deprecated, but should still work for the time being::
|
|
862
|
+
|
|
863
|
+
sage: a.show(50, 3) # long time # optional -- ImageMagick
|
|
864
|
+
|
|
865
|
+
.. NOTE::
|
|
866
|
+
|
|
867
|
+
If you don't have ffmpeg or ImageMagick installed, you will
|
|
868
|
+
get an error message like this::
|
|
869
|
+
|
|
870
|
+
Error: Neither ImageMagick nor ffmpeg appears to be installed. Saving an
|
|
871
|
+
animation to a GIF file or displaying an animation requires one of these
|
|
872
|
+
packages, so please install one of them and try again.
|
|
873
|
+
|
|
874
|
+
See www.imagemagick.org and www.ffmpeg.org for more information.
|
|
875
|
+
"""
|
|
876
|
+
|
|
877
|
+
# Positional parameters for the sake of backwards compatibility
|
|
878
|
+
if delay is not None:
|
|
879
|
+
kwds.setdefault("delay", delay)
|
|
880
|
+
if iterations is not None:
|
|
881
|
+
kwds.setdefault("iterations", iterations)
|
|
882
|
+
|
|
883
|
+
from sage.repl.rich_output import get_display_manager
|
|
884
|
+
dm = get_display_manager()
|
|
885
|
+
dm.display_immediately(self, **kwds)
|
|
886
|
+
|
|
887
|
+
def ffmpeg(self, savefile=None, show_path=False, output_format=None,
|
|
888
|
+
ffmpeg_options='', delay=None, iterations=0, pix_fmt='rgb24'):
|
|
889
|
+
r"""
|
|
890
|
+
Return a movie showing an animation composed from rendering
|
|
891
|
+
the frames in ``self``.
|
|
892
|
+
|
|
893
|
+
This method will only work if ``ffmpeg`` is installed. See
|
|
894
|
+
https://www.ffmpeg.org for information about ``ffmpeg``.
|
|
895
|
+
|
|
896
|
+
INPUT:
|
|
897
|
+
|
|
898
|
+
- ``savefile`` -- file that the mpeg gets saved to
|
|
899
|
+
|
|
900
|
+
.. warning::
|
|
901
|
+
|
|
902
|
+
This will overwrite ``savefile`` if it already exists.
|
|
903
|
+
|
|
904
|
+
- ``show_path`` -- boolean (default: ``False``); if ``True``,
|
|
905
|
+
print the path to the saved file
|
|
906
|
+
|
|
907
|
+
- ``output_format`` -- string (default: ``None``); format and
|
|
908
|
+
suffix to use for the video. This may be ``'mpg'``, ``'mpeg'``,
|
|
909
|
+
``'avi'``, ``'gif'``, or any other format that ``ffmpeg`` can handle.
|
|
910
|
+
If this is ``None`` and the user specifies ``savefile`` with a
|
|
911
|
+
suffix, say ``savefile='animation.avi'``, try to determine the
|
|
912
|
+
format (``'avi'`` in this case) from that file name. If no file
|
|
913
|
+
is specified or if the suffix cannot be determined, ``'mpg'`` is
|
|
914
|
+
used.
|
|
915
|
+
|
|
916
|
+
- ``ffmpeg_options`` -- string (default: ``''``); this string is
|
|
917
|
+
passed directly to ffmpeg
|
|
918
|
+
|
|
919
|
+
- ``delay`` -- integer (default: ``None``); delay in hundredths of a
|
|
920
|
+
second between frames. The framerate is 100/delay.
|
|
921
|
+
This is not supported for mpeg files: for mpegs, the frame
|
|
922
|
+
rate is always 25 fps.
|
|
923
|
+
|
|
924
|
+
- ``iterations`` -- integer (default: 0); number of iterations
|
|
925
|
+
of animation. If 0, loop forever. This is only supported
|
|
926
|
+
for animated gif output and requires ``ffmpeg`` version 0.9 or
|
|
927
|
+
later. For older versions, set ``iterations=None``.
|
|
928
|
+
|
|
929
|
+
- ``pix_fmt`` -- string (default: ``'rgb24'``); used only for gif
|
|
930
|
+
output. Different values such as 'rgb8' or 'pal8' may be
|
|
931
|
+
necessary depending on how ffmpeg was installed. Set
|
|
932
|
+
``pix_fmt=None`` to disable this option.
|
|
933
|
+
|
|
934
|
+
If ``savefile`` is not specified: in notebook mode, display
|
|
935
|
+
the animation; otherwise, save it to a default file name. Use
|
|
936
|
+
:func:`sage.misc.verbose.set_verbose` with ``level=1`` to see
|
|
937
|
+
additional output.
|
|
938
|
+
|
|
939
|
+
EXAMPLES::
|
|
940
|
+
|
|
941
|
+
sage: x = SR.var("x")
|
|
942
|
+
sage: a = animate([sin(x + float(k))
|
|
943
|
+
....: for k in srange(0, 2*pi, 0.7)],
|
|
944
|
+
....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
|
|
945
|
+
sage: td = tmp_dir()
|
|
946
|
+
sage: a.ffmpeg(savefile=td + 'new.mpg') # long time # optional -- FFmpeg
|
|
947
|
+
sage: a.ffmpeg(savefile=td + 'new.avi') # long time # optional -- FFmpeg
|
|
948
|
+
sage: a.ffmpeg(savefile=td + 'new.gif') # long time # optional -- FFmpeg
|
|
949
|
+
sage: a.ffmpeg(savefile=td + 'new.mpg', show_path=True) # long time # optional -- FFmpeg
|
|
950
|
+
Animation saved to .../new.mpg.
|
|
951
|
+
|
|
952
|
+
.. NOTE::
|
|
953
|
+
|
|
954
|
+
If ffmpeg is not installed, you will get an error message
|
|
955
|
+
like this::
|
|
956
|
+
|
|
957
|
+
FeatureNotPresentError: ffmpeg is not available.
|
|
958
|
+
Executable 'ffmpeg' not found on PATH.
|
|
959
|
+
Further installation instructions might be available at https://www.ffmpeg.org/.
|
|
960
|
+
|
|
961
|
+
TESTS::
|
|
962
|
+
|
|
963
|
+
sage: a.ffmpeg(output_format='gif',delay=30,iterations=5) # long time # optional -- FFmpeg
|
|
964
|
+
"""
|
|
965
|
+
from sage.features.ffmpeg import FFmpeg
|
|
966
|
+
FFmpeg().require()
|
|
967
|
+
|
|
968
|
+
if savefile is None:
|
|
969
|
+
if output_format is None:
|
|
970
|
+
output_format = '.mpg'
|
|
971
|
+
else:
|
|
972
|
+
if output_format[0] != '.':
|
|
973
|
+
output_format = '.'+output_format
|
|
974
|
+
savefile = tmp_filename(ext=output_format)
|
|
975
|
+
else:
|
|
976
|
+
if output_format is None:
|
|
977
|
+
suffix = os.path.splitext(savefile)[1]
|
|
978
|
+
if len(suffix) > 0:
|
|
979
|
+
output_format = suffix
|
|
980
|
+
else:
|
|
981
|
+
output_format = '.mpg'
|
|
982
|
+
if not savefile.endswith(output_format):
|
|
983
|
+
savefile += output_format
|
|
984
|
+
early_options = ''
|
|
985
|
+
if output_format == '.gif':
|
|
986
|
+
# We try to set reasonable options for gif output.
|
|
987
|
+
#
|
|
988
|
+
# Older versions of ffmpeg (before 0.9, summer 2011)
|
|
989
|
+
# use the option -loop_output instead of -loop.
|
|
990
|
+
# Setting iterations=None is a way of preventing sage
|
|
991
|
+
# from adding the -loop option. A separate
|
|
992
|
+
# -loop_output option can be added with the
|
|
993
|
+
# ffmpeg_options argument.
|
|
994
|
+
if iterations is not None:
|
|
995
|
+
loop_cmd = f'-loop {iterations} '
|
|
996
|
+
else:
|
|
997
|
+
loop_cmd = ''
|
|
998
|
+
# A pix_fmt value is required for some but not all
|
|
999
|
+
# ffmpeg installations. Setting pix_fmt=None will
|
|
1000
|
+
# prevent sage from adding this option, and it may be
|
|
1001
|
+
# controlled separately through ffmpeg_options.
|
|
1002
|
+
if pix_fmt is not None:
|
|
1003
|
+
pix_fmt_cmd = f'-pix_fmt {pix_fmt} '
|
|
1004
|
+
else:
|
|
1005
|
+
pix_fmt_cmd = ''
|
|
1006
|
+
ffmpeg_options += f' {pix_fmt_cmd}{loop_cmd}'
|
|
1007
|
+
if delay is not None and output_format != '.mpeg' and output_format != '.mpg':
|
|
1008
|
+
early_options += ' -r %s ' % int(100/delay)
|
|
1009
|
+
savefile = os.path.abspath(savefile)
|
|
1010
|
+
pngdir = self.png()
|
|
1011
|
+
pngs = os.path.join(pngdir, "%08d.png")
|
|
1012
|
+
# For ffmpeg, it seems that some options, like '-g ... -r
|
|
1013
|
+
# ...', need to come before the input file names, while
|
|
1014
|
+
# some options, like '-pix_fmt rgb24', need to come
|
|
1015
|
+
# afterwards. Hence 'early_options' and 'ffmpeg_options'
|
|
1016
|
+
# The `-nostdin` is needed to avoid the command to hang, see
|
|
1017
|
+
# https://stackoverflow.com/questions/16523746/ffmpeg-hangs-when-run-in-background
|
|
1018
|
+
cmd = 'cd {}; {} -nostdin -y -f image2 {} -i {} {} {}'.format(
|
|
1019
|
+
shlex.quote(pngdir), shlex.quote(FFmpeg().absolute_filename()),
|
|
1020
|
+
early_options, shlex.quote(pngs), ffmpeg_options, shlex.quote(savefile))
|
|
1021
|
+
from subprocess import check_call, CalledProcessError, PIPE
|
|
1022
|
+
try:
|
|
1023
|
+
if sage.misc.verbose.get_verbose() > 0:
|
|
1024
|
+
set_stderr = None
|
|
1025
|
+
else:
|
|
1026
|
+
set_stderr = PIPE
|
|
1027
|
+
sage.misc.verbose.verbose("Executing '%s'" % cmd,level=1)
|
|
1028
|
+
sage.misc.verbose.verbose("\n---- ffmpeg output below ----\n")
|
|
1029
|
+
check_call(cmd, shell=True, stderr=set_stderr)
|
|
1030
|
+
if show_path:
|
|
1031
|
+
print("Animation saved to file %s." % savefile)
|
|
1032
|
+
except (CalledProcessError, OSError):
|
|
1033
|
+
print("Error running ffmpeg.")
|
|
1034
|
+
raise
|
|
1035
|
+
|
|
1036
|
+
def apng(self, savefile=None, show_path=False, delay=20, iterations=0):
|
|
1037
|
+
r"""
|
|
1038
|
+
Create an animated PNG composed from rendering the graphics
|
|
1039
|
+
objects in ``self``. Return the absolute path to that file.
|
|
1040
|
+
|
|
1041
|
+
Notice that not all web browsers are capable of displaying APNG
|
|
1042
|
+
files, though they should still present the first frame of the
|
|
1043
|
+
animation as a fallback.
|
|
1044
|
+
|
|
1045
|
+
The generated file is not optimized, so it may be quite large.
|
|
1046
|
+
|
|
1047
|
+
Input:
|
|
1048
|
+
|
|
1049
|
+
- ``delay`` -- (default: 20) delay in hundredths of a
|
|
1050
|
+
second between frames
|
|
1051
|
+
|
|
1052
|
+
- ``savefile`` -- file that the animated gif gets saved
|
|
1053
|
+
to
|
|
1054
|
+
|
|
1055
|
+
- ``iterations`` -- integer (default: 0); number of
|
|
1056
|
+
iterations of animation. If 0, loop forever.
|
|
1057
|
+
|
|
1058
|
+
- ``show_path`` -- boolean (default: ``False``); if True,
|
|
1059
|
+
print the path to the saved file
|
|
1060
|
+
|
|
1061
|
+
EXAMPLES::
|
|
1062
|
+
|
|
1063
|
+
sage: x = SR.var("x")
|
|
1064
|
+
sage: a = animate([sin(x + float(k))
|
|
1065
|
+
....: for k in srange(0,2*pi,0.7)],
|
|
1066
|
+
....: xmin=0, xmax=2*pi, figsize=[2,1])
|
|
1067
|
+
sage: dir = tmp_dir()
|
|
1068
|
+
sage: a.apng(show_path=True) # long time
|
|
1069
|
+
Animation saved to ....png.
|
|
1070
|
+
sage: a.apng(savefile=dir + 'my_animation.png', delay=35, iterations=3) # long time
|
|
1071
|
+
sage: a.apng(savefile=dir + 'my_animation.png', show_path=True) # long time
|
|
1072
|
+
Animation saved to .../my_animation.png.
|
|
1073
|
+
|
|
1074
|
+
If the individual frames have different sizes, an error will be raised::
|
|
1075
|
+
|
|
1076
|
+
sage: a = animate([plot(sin(x), (x, 0, k))
|
|
1077
|
+
....: for k in range(1,4)],
|
|
1078
|
+
....: ymin=-1, ymax=1, aspect_ratio=1, figsize=[2,1])
|
|
1079
|
+
sage: a.apng() # long time
|
|
1080
|
+
Traceback (most recent call last):
|
|
1081
|
+
...
|
|
1082
|
+
ValueError: Chunk IHDR mismatch
|
|
1083
|
+
|
|
1084
|
+
TESTS::
|
|
1085
|
+
|
|
1086
|
+
sage: a = animate([])
|
|
1087
|
+
sage: a.apng(show_path=True)
|
|
1088
|
+
Animation saved to file ....png.
|
|
1089
|
+
"""
|
|
1090
|
+
pngdir = self.png()
|
|
1091
|
+
if savefile is None:
|
|
1092
|
+
savefile = tmp_filename(ext='.png')
|
|
1093
|
+
with open(savefile, "wb") as out:
|
|
1094
|
+
apng = APngAssembler(
|
|
1095
|
+
out, len(self),
|
|
1096
|
+
delay=delay, num_plays=iterations)
|
|
1097
|
+
for i in range(len(self)):
|
|
1098
|
+
png = os.path.join(pngdir, "%08d.png" % i)
|
|
1099
|
+
apng.add_frame(png)
|
|
1100
|
+
if show_path:
|
|
1101
|
+
print("Animation saved to file %s." % savefile)
|
|
1102
|
+
|
|
1103
|
+
def save(self, filename=None, show_path=False, use_ffmpeg=False, **kwds):
|
|
1104
|
+
r"""
|
|
1105
|
+
Save this animation.
|
|
1106
|
+
|
|
1107
|
+
INPUT:
|
|
1108
|
+
|
|
1109
|
+
- ``filename`` -- (default: ``None``) name of save file
|
|
1110
|
+
|
|
1111
|
+
- ``show_path`` -- boolean (default: ``False``); if True,
|
|
1112
|
+
print the path to the saved file
|
|
1113
|
+
|
|
1114
|
+
- ``use_ffmpeg`` -- boolean (default: ``False``); if ``True``, use
|
|
1115
|
+
'ffmpeg' by default instead of ImageMagick when creating GIF files
|
|
1116
|
+
|
|
1117
|
+
If filename is ``None``, then in notebook mode, display the
|
|
1118
|
+
animation; otherwise, save the animation to a GIF file. If
|
|
1119
|
+
filename ends in '.html', save an :meth:`interactive` version of
|
|
1120
|
+
the animation to an HTML file that uses the Three.js viewer. If
|
|
1121
|
+
filename ends in '.sobj', save to an sobj file. Otherwise,
|
|
1122
|
+
try to determine the format from the filename extension
|
|
1123
|
+
('.mpg', '.gif', '.avi', etc.). If the format cannot be
|
|
1124
|
+
determined, default to GIF.
|
|
1125
|
+
|
|
1126
|
+
For GIF files, either ffmpeg or the ImageMagick suite must be
|
|
1127
|
+
installed. For other movie formats, ffmpeg must be installed.
|
|
1128
|
+
sobj and HTML files can be saved with no extra software installed.
|
|
1129
|
+
|
|
1130
|
+
EXAMPLES::
|
|
1131
|
+
|
|
1132
|
+
sage: x = SR.var("x")
|
|
1133
|
+
sage: a = animate([sin(x + float(k))
|
|
1134
|
+
....: for k in srange(0, 2*pi, 0.7)],
|
|
1135
|
+
....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
|
|
1136
|
+
sage: td = tmp_dir()
|
|
1137
|
+
sage: a.save() # not tested
|
|
1138
|
+
sage: a.save(td + 'wave.gif') # long time # optional -- ImageMagick
|
|
1139
|
+
sage: a.save(td + 'wave.gif', show_path=True) # long time # optional -- ImageMagick
|
|
1140
|
+
Animation saved to file .../wave.gif.
|
|
1141
|
+
sage: a.save(td + 'wave.avi', show_path=True) # long time # optional -- FFmpeg
|
|
1142
|
+
Animation saved to file .../wave.avi.
|
|
1143
|
+
sage: a.save(td + 'wave0.sobj')
|
|
1144
|
+
sage: a.save(td + 'wave1.sobj', show_path=True)
|
|
1145
|
+
Animation saved to file .../wave1.sobj.
|
|
1146
|
+
sage: a.save(td + 'wave0.html', online=True)
|
|
1147
|
+
sage: a.save(td + 'wave1.html', show_path=True, online=True)
|
|
1148
|
+
Animation saved to file .../wave1.html.
|
|
1149
|
+
|
|
1150
|
+
TESTS:
|
|
1151
|
+
|
|
1152
|
+
Ensure that we can pass delay and iteration count to the saved
|
|
1153
|
+
GIF image (see :issue:`18176`)::
|
|
1154
|
+
|
|
1155
|
+
sage: # long time, optional -- ImageMagick
|
|
1156
|
+
sage: a.save(td + 'wave.gif')
|
|
1157
|
+
sage: with open(td + 'wave.gif', 'rb') as f:
|
|
1158
|
+
....: print(b'GIF8' in f.read())
|
|
1159
|
+
True
|
|
1160
|
+
sage: with open(td + 'wave.gif', 'rb') as f:
|
|
1161
|
+
....: print(b'!\xff\x0bNETSCAPE2.0\x03\x01\x00\x00\x00' in f.read())
|
|
1162
|
+
True
|
|
1163
|
+
sage: a.save(td + 'wave.gif', delay=35)
|
|
1164
|
+
sage: with open(td + 'wave.gif', 'rb') as f:
|
|
1165
|
+
....: print(b'GIF8' in f.read())
|
|
1166
|
+
True
|
|
1167
|
+
sage: a.save(td + 'wave.gif', iterations=3)
|
|
1168
|
+
sage: with open(td + 'wave.gif', 'rb') as f:
|
|
1169
|
+
....: print(b'!\xff\x0bNETSCAPE2.0\x03\x01\x00\x00\x00' in f.read())
|
|
1170
|
+
False
|
|
1171
|
+
sage: with open(td + 'wave.gif', 'rb') as f:
|
|
1172
|
+
....: check1 = b'!\xff\x0bNETSCAPE2.0\x03\x01\x02\x00\x00'
|
|
1173
|
+
....: check2 = b'!\xff\x0bNETSCAPE2.0\x03\x01\x03\x00\x00'
|
|
1174
|
+
....: data = f.read()
|
|
1175
|
+
....: print(check1 in data or check2 in data)
|
|
1176
|
+
True
|
|
1177
|
+
"""
|
|
1178
|
+
if filename is None:
|
|
1179
|
+
suffix = '.gif'
|
|
1180
|
+
else:
|
|
1181
|
+
suffix = os.path.splitext(filename)[1]
|
|
1182
|
+
if not suffix:
|
|
1183
|
+
suffix = '.gif'
|
|
1184
|
+
|
|
1185
|
+
if filename is None or suffix == '.gif':
|
|
1186
|
+
self.gif(savefile=filename, show_path=show_path,
|
|
1187
|
+
use_ffmpeg=use_ffmpeg, **kwds)
|
|
1188
|
+
elif suffix == '.sobj':
|
|
1189
|
+
SageObject.save(self, filename)
|
|
1190
|
+
if show_path:
|
|
1191
|
+
print("Animation saved to file %s." % filename)
|
|
1192
|
+
elif suffix == '.html':
|
|
1193
|
+
self.interactive(**kwds).save(filename)
|
|
1194
|
+
if show_path:
|
|
1195
|
+
print("Animation saved to file %s." % filename)
|
|
1196
|
+
else:
|
|
1197
|
+
self.ffmpeg(savefile=filename, show_path=show_path, **kwds)
|
|
1198
|
+
|
|
1199
|
+
def interactive(self, **kwds):
|
|
1200
|
+
r"""
|
|
1201
|
+
Create an interactive depiction of the animation.
|
|
1202
|
+
|
|
1203
|
+
INPUT:
|
|
1204
|
+
|
|
1205
|
+
- ``**kwds`` -- any of the viewing options accepted by show() are valid
|
|
1206
|
+
as keyword arguments to this function and they will behave in the same
|
|
1207
|
+
way. Those that are animation-related and recognized by the Three.js
|
|
1208
|
+
viewer are: ``animate``, ``animation_controls``, ``auto_play``,
|
|
1209
|
+
``delay``, and ``loop``.
|
|
1210
|
+
|
|
1211
|
+
OUTPUT: a 3D graphics object which, by default, will use the Three.js viewer
|
|
1212
|
+
|
|
1213
|
+
EXAMPLES::
|
|
1214
|
+
|
|
1215
|
+
sage: x = SR.var("x")
|
|
1216
|
+
sage: frames = [point3d((sin(x), cos(x), x))
|
|
1217
|
+
....: for x in (0, pi/16, .., 2*pi)]
|
|
1218
|
+
sage: animate(frames).interactive(online=True)
|
|
1219
|
+
Graphics3d Object
|
|
1220
|
+
|
|
1221
|
+
Works with frames that are 2D or 3D graphics objects or convertible to
|
|
1222
|
+
2D or 3D graphics objects via a ``plot`` or ``plot3d`` method::
|
|
1223
|
+
|
|
1224
|
+
sage: frames = [dodecahedron(), circle(center=(0, 0), radius=1), x^2]
|
|
1225
|
+
sage: animate(frames).interactive(online=True, delay=100)
|
|
1226
|
+
Graphics3d Object
|
|
1227
|
+
|
|
1228
|
+
.. SEEALSO::
|
|
1229
|
+
|
|
1230
|
+
:ref:`threejs_viewer`
|
|
1231
|
+
"""
|
|
1232
|
+
from sage.plot.plot3d.base import Graphics3d, KeyframeAnimationGroup
|
|
1233
|
+
# Attempt to convert frames to Graphics3d objects.
|
|
1234
|
+
g3d_frames = []
|
|
1235
|
+
for i, frame in enumerate(self._frames):
|
|
1236
|
+
if not isinstance(frame, Graphics3d):
|
|
1237
|
+
try:
|
|
1238
|
+
frame = frame.plot3d()
|
|
1239
|
+
except (AttributeError, TypeError):
|
|
1240
|
+
try:
|
|
1241
|
+
frame = frame.plot().plot3d()
|
|
1242
|
+
except (AttributeError, TypeError):
|
|
1243
|
+
frame = None
|
|
1244
|
+
if not isinstance(frame, Graphics3d):
|
|
1245
|
+
raise TypeError(f"Could not convert frame {i} to Graphics3d")
|
|
1246
|
+
g3d_frames.append(frame)
|
|
1247
|
+
# Give preference to this method's keyword arguments over those provided
|
|
1248
|
+
# to animate or the constructor.
|
|
1249
|
+
kwds = dict(self._kwds, **kwds)
|
|
1250
|
+
# Three.js is the only viewer that supports animation at present.
|
|
1251
|
+
if 'viewer' not in kwds:
|
|
1252
|
+
kwds['viewer'] = 'threejs'
|
|
1253
|
+
return KeyframeAnimationGroup(g3d_frames, **kwds)
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
class APngAssembler:
|
|
1257
|
+
r"""
|
|
1258
|
+
Build an APNG_ (Animated PNG) from a sequence of PNG files.
|
|
1259
|
+
This is used by the :meth:`sage.plot.animate.Animation.apng` method.
|
|
1260
|
+
|
|
1261
|
+
This code is quite simple; it does little more than copying chunks
|
|
1262
|
+
from input PNG files to the output file. There is no optimization
|
|
1263
|
+
involved. This does not depend on external programs or libraries.
|
|
1264
|
+
|
|
1265
|
+
INPUT:
|
|
1266
|
+
|
|
1267
|
+
- ``out`` -- a file opened for binary writing to which the data
|
|
1268
|
+
will be written
|
|
1269
|
+
|
|
1270
|
+
- ``num_frames`` -- the number of frames in the animation
|
|
1271
|
+
|
|
1272
|
+
- ``num_plays`` -- how often to iterate, 0 means infinitely
|
|
1273
|
+
|
|
1274
|
+
- ``delay`` -- numerator of the delay fraction in seconds
|
|
1275
|
+
|
|
1276
|
+
- ``delay_denominator`` -- denominator of the delay in seconds
|
|
1277
|
+
|
|
1278
|
+
EXAMPLES::
|
|
1279
|
+
|
|
1280
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1281
|
+
sage: x = SR.var("x")
|
|
1282
|
+
sage: def assembleAPNG():
|
|
1283
|
+
....: a = animate([sin(x + float(k)) for k in srange(0,2*pi,0.7)],
|
|
1284
|
+
....: xmin=0, xmax=2*pi, figsize=[2,1])
|
|
1285
|
+
....: pngdir = a.png()
|
|
1286
|
+
....: outfile = sage.misc.temporary_file.tmp_filename(ext='.png')
|
|
1287
|
+
....: with open(outfile, "wb") as f:
|
|
1288
|
+
....: apng = APngAssembler(f, len(a))
|
|
1289
|
+
....: for i in range(len(a)):
|
|
1290
|
+
....: png = os.path.join(pngdir, "{:08d}.png".format(i))
|
|
1291
|
+
....: apng.add_frame(png, delay=10*i + 10)
|
|
1292
|
+
....: return outfile
|
|
1293
|
+
sage: assembleAPNG() # long time
|
|
1294
|
+
'...png'
|
|
1295
|
+
"""
|
|
1296
|
+
magic = b"\x89PNG\x0d\x0a\x1a\x0a"
|
|
1297
|
+
mustmatch = frozenset([b"IHDR", b"PLTE", b"bKGD", b"cHRM", b"gAMA",
|
|
1298
|
+
b"pHYs", b"sBIT", b"tRNS"])
|
|
1299
|
+
|
|
1300
|
+
def __init__(self, out, num_frames,
|
|
1301
|
+
num_plays=0, delay=200, delay_denominator=100):
|
|
1302
|
+
r"""
|
|
1303
|
+
Initialize for creation of an APNG file.
|
|
1304
|
+
"""
|
|
1305
|
+
self._last_seqno = -1
|
|
1306
|
+
self._idx = 0
|
|
1307
|
+
self._first = True
|
|
1308
|
+
self.out = out
|
|
1309
|
+
self.num_frames = num_frames
|
|
1310
|
+
self.num_plays = num_plays
|
|
1311
|
+
self.default_delay_numerator = delay
|
|
1312
|
+
self.default_delay_denominator = delay_denominator
|
|
1313
|
+
self._matchref = {}
|
|
1314
|
+
self.out.write(self.magic)
|
|
1315
|
+
|
|
1316
|
+
def add_frame(self, pngfile, delay=None, delay_denominator=None):
|
|
1317
|
+
r"""
|
|
1318
|
+
Add a single frame to the APNG file.
|
|
1319
|
+
|
|
1320
|
+
INPUT:
|
|
1321
|
+
|
|
1322
|
+
- ``pngfile`` -- file name of the PNG file with data for this frame
|
|
1323
|
+
|
|
1324
|
+
- ``delay`` -- numerator of the delay fraction in seconds
|
|
1325
|
+
|
|
1326
|
+
- ``delay_denominator`` -- denominator of the delay in seconds
|
|
1327
|
+
|
|
1328
|
+
If the delay is not specified, the default from the constructor
|
|
1329
|
+
applies.
|
|
1330
|
+
|
|
1331
|
+
TESTS::
|
|
1332
|
+
|
|
1333
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1334
|
+
sage: from io import BytesIO
|
|
1335
|
+
sage: buf = BytesIO()
|
|
1336
|
+
sage: apng = APngAssembler(buf, 2)
|
|
1337
|
+
sage: fn = APngAssembler._testData("input1", True)
|
|
1338
|
+
sage: apng.add_frame(fn, delay=0x567, delay_denominator=0x1234)
|
|
1339
|
+
sage: fn = APngAssembler._testData("input2", True)
|
|
1340
|
+
sage: apng.add_frame(fn)
|
|
1341
|
+
sage: len(buf.getvalue())
|
|
1342
|
+
217
|
|
1343
|
+
sage: buf.getvalue() == APngAssembler._testData("anim12", False)
|
|
1344
|
+
True
|
|
1345
|
+
sage: apng.add_frame(fn)
|
|
1346
|
+
Traceback (most recent call last):
|
|
1347
|
+
...
|
|
1348
|
+
RuntimeError: Already reached the declared number of frames
|
|
1349
|
+
"""
|
|
1350
|
+
if self._idx == self.num_frames:
|
|
1351
|
+
raise RuntimeError("Already reached the declared number of frames")
|
|
1352
|
+
self.delay_numerator = self.default_delay_numerator
|
|
1353
|
+
self.delay_denominator = self.default_delay_denominator
|
|
1354
|
+
self._actl_written = False
|
|
1355
|
+
self._fctl_written = False
|
|
1356
|
+
if delay is not None:
|
|
1357
|
+
self.delay_numerator = delay
|
|
1358
|
+
if delay_denominator is not None:
|
|
1359
|
+
self.delay_denominator = delay_denominator
|
|
1360
|
+
self._add_png(pngfile)
|
|
1361
|
+
self._idx += 1
|
|
1362
|
+
if self._idx == self.num_frames:
|
|
1363
|
+
self._chunk(b"IEND", b"")
|
|
1364
|
+
|
|
1365
|
+
def set_default(self, pngfile):
|
|
1366
|
+
r"""
|
|
1367
|
+
Add a default image for the APNG file.
|
|
1368
|
+
|
|
1369
|
+
This image is used as a fallback in case some application does
|
|
1370
|
+
not understand the APNG format. This method must be called
|
|
1371
|
+
prior to any calls to the ``add_frame`` method, if it is called
|
|
1372
|
+
at all. If it is not called, then the first frame of the
|
|
1373
|
+
animation will be the default.
|
|
1374
|
+
|
|
1375
|
+
INPUT:
|
|
1376
|
+
|
|
1377
|
+
- ``pngfile`` -- file name of the PNG file with data
|
|
1378
|
+
for the default image
|
|
1379
|
+
|
|
1380
|
+
TESTS::
|
|
1381
|
+
|
|
1382
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1383
|
+
sage: from io import BytesIO
|
|
1384
|
+
sage: buf = BytesIO()
|
|
1385
|
+
sage: apng = APngAssembler(buf, 1)
|
|
1386
|
+
sage: fn = APngAssembler._testData("input1", True)
|
|
1387
|
+
sage: apng.set_default(fn)
|
|
1388
|
+
sage: fn = APngAssembler._testData("input2", True)
|
|
1389
|
+
sage: apng.add_frame(fn, delay=0x567, delay_denominator=0x1234)
|
|
1390
|
+
sage: len(buf.getvalue())
|
|
1391
|
+
179
|
|
1392
|
+
sage: buf.getvalue() == APngAssembler._testData("still1anim2", False)
|
|
1393
|
+
True
|
|
1394
|
+
sage: apng.add_frame(fn)
|
|
1395
|
+
Traceback (most recent call last):
|
|
1396
|
+
...
|
|
1397
|
+
RuntimeError: Already reached the declared number of frames
|
|
1398
|
+
"""
|
|
1399
|
+
if self._idx != 0:
|
|
1400
|
+
raise RuntimeError("Default image must precede all animation frames")
|
|
1401
|
+
self._actl_written = False
|
|
1402
|
+
self._fctl_written = True
|
|
1403
|
+
self._add_png(pngfile)
|
|
1404
|
+
|
|
1405
|
+
def _add_png(self, pngfile):
|
|
1406
|
+
r"""
|
|
1407
|
+
Add data from one PNG still image.
|
|
1408
|
+
|
|
1409
|
+
TESTS::
|
|
1410
|
+
|
|
1411
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1412
|
+
sage: APngAssembler._testCase1("_add_png", reads=False)
|
|
1413
|
+
enter _add_png('...png')
|
|
1414
|
+
write _current_chunk = (...'\x00\x00\x00\r',...'IHDR',...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00',...'\xb8\x1f9\xc6')
|
|
1415
|
+
call _copy() -> None
|
|
1416
|
+
call _first_IHDR(...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00') -> None
|
|
1417
|
+
write _current_chunk = (...'\x00\x00\x00\x04',...'gAMA',...'\x00\x01\x86\xa0',...'1\xe8\x96_')
|
|
1418
|
+
call _copy() -> None
|
|
1419
|
+
write _current_chunk = (...'\x00\x00\x00\x07',...'tIME',...'\x07\xde\x06\x1b\x0b&$',...'\x1f0z\xd5')
|
|
1420
|
+
write _current_chunk = (...'\x00\x00\x00\x08',...'IDAT',...'img1data',...'\xce\x8aI\x99')
|
|
1421
|
+
call _first_IDAT(...'img1data') -> None
|
|
1422
|
+
write _current_chunk = (...'\x00\x00\x00\x00',...'IEND',...'',...'\xaeB`\x82')
|
|
1423
|
+
write _first = False
|
|
1424
|
+
exit _add_png -> None
|
|
1425
|
+
enter _add_png('...png')
|
|
1426
|
+
write _current_chunk = (...'\x00\x00\x00\r',...'IHDR',...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00',...'\xb8\x1f9\xc6')
|
|
1427
|
+
write _current_chunk = (...'\x00\x00\x00\x04',...'gAMA',...'\x00\x01\x86\xa0',...'1\xe8\x96_')
|
|
1428
|
+
write _current_chunk = (...'\x00\x00\x00\x04',...'IDAT',...'img2',...'\x0ei\xab\x1d')
|
|
1429
|
+
call _next_IDAT(...'img2') -> None
|
|
1430
|
+
write _current_chunk = (...'\x00\x00\x00\x04',...'IDAT',...'data',...'f\x94\xcbx')
|
|
1431
|
+
call _next_IDAT(...'data') -> None
|
|
1432
|
+
write _current_chunk = (...'\x00\x00\x00\x00',...'IEND',...'',...'\xaeB`\x82')
|
|
1433
|
+
write _first = False
|
|
1434
|
+
exit _add_png -> None
|
|
1435
|
+
"""
|
|
1436
|
+
with open(pngfile, 'rb') as png:
|
|
1437
|
+
if png.read(8) != self.magic:
|
|
1438
|
+
raise ValueError(f"{pngfile} is not a PNG file")
|
|
1439
|
+
while True:
|
|
1440
|
+
chead = png.read(8)
|
|
1441
|
+
if len(chead) == 0:
|
|
1442
|
+
break
|
|
1443
|
+
clen, ctype = struct.unpack(">L4s", chead)
|
|
1444
|
+
cdata = png.read(clen)
|
|
1445
|
+
ccrc = png.read(4)
|
|
1446
|
+
utype = ctype.decode("ascii")
|
|
1447
|
+
self._current_chunk = (chead[:4], ctype, cdata, ccrc)
|
|
1448
|
+
if ctype in self.mustmatch:
|
|
1449
|
+
ref = self._matchref.get(ctype)
|
|
1450
|
+
if ref is None:
|
|
1451
|
+
self._matchref[ctype] = cdata
|
|
1452
|
+
self._copy()
|
|
1453
|
+
else:
|
|
1454
|
+
if cdata != ref:
|
|
1455
|
+
raise ValueError(f"Chunk {utype} mismatch")
|
|
1456
|
+
met = ("_first_" if self._first else "_next_") + utype
|
|
1457
|
+
try:
|
|
1458
|
+
met = getattr(self, met)
|
|
1459
|
+
except AttributeError:
|
|
1460
|
+
pass
|
|
1461
|
+
else:
|
|
1462
|
+
met(cdata)
|
|
1463
|
+
self._first = False
|
|
1464
|
+
|
|
1465
|
+
def _seqno(self):
|
|
1466
|
+
r"""
|
|
1467
|
+
Generate next sequence number.
|
|
1468
|
+
|
|
1469
|
+
TESTS::
|
|
1470
|
+
|
|
1471
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1472
|
+
sage: from io import BytesIO
|
|
1473
|
+
sage: buf = BytesIO()
|
|
1474
|
+
sage: apng = APngAssembler(buf, 1)
|
|
1475
|
+
sage: apng._seqno() == b'\x00\x00\x00\x00'
|
|
1476
|
+
True
|
|
1477
|
+
sage: apng._seqno() == b'\x00\x00\x00\x01'
|
|
1478
|
+
True
|
|
1479
|
+
sage: apng._seqno() == b'\x00\x00\x00\x02'
|
|
1480
|
+
True
|
|
1481
|
+
"""
|
|
1482
|
+
self._last_seqno += 1
|
|
1483
|
+
return struct.pack(">L", self._last_seqno)
|
|
1484
|
+
|
|
1485
|
+
def _first_IHDR(self, data):
|
|
1486
|
+
r"""
|
|
1487
|
+
Remember image size.
|
|
1488
|
+
|
|
1489
|
+
TESTS::
|
|
1490
|
+
|
|
1491
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1492
|
+
sage: APngAssembler._testCase1("_first_IHDR")
|
|
1493
|
+
enter _first_IHDR(...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00')
|
|
1494
|
+
write width = 3
|
|
1495
|
+
write height = 2
|
|
1496
|
+
exit _first_IHDR -> None
|
|
1497
|
+
"""
|
|
1498
|
+
w, h, d, ctype, comp, filt, ilace = struct.unpack(">2L5B", data)
|
|
1499
|
+
self.width = w
|
|
1500
|
+
self.height = h
|
|
1501
|
+
|
|
1502
|
+
def _first_IDAT(self, data):
|
|
1503
|
+
r"""
|
|
1504
|
+
Write acTL and fcTL, then copy as IDAT.
|
|
1505
|
+
|
|
1506
|
+
TESTS::
|
|
1507
|
+
|
|
1508
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1509
|
+
sage: APngAssembler._testCase1("_first_IDAT")
|
|
1510
|
+
enter _first_IDAT(...'img1data')
|
|
1511
|
+
call _actl() -> None
|
|
1512
|
+
call _fctl() -> None
|
|
1513
|
+
call _copy() -> None
|
|
1514
|
+
exit _first_IDAT -> None
|
|
1515
|
+
"""
|
|
1516
|
+
self._actl()
|
|
1517
|
+
self._fctl()
|
|
1518
|
+
self._copy()
|
|
1519
|
+
|
|
1520
|
+
def _next_IDAT(self, data):
|
|
1521
|
+
r"""
|
|
1522
|
+
Write fcTL, then convert to fdAT.
|
|
1523
|
+
|
|
1524
|
+
TESTS::
|
|
1525
|
+
|
|
1526
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1527
|
+
sage: APngAssembler._testCase1("_next_IDAT")
|
|
1528
|
+
enter _next_IDAT(...'img2')
|
|
1529
|
+
call _fctl() -> None
|
|
1530
|
+
call _seqno() -> ...'\x00\x00\x00\x02'
|
|
1531
|
+
call _chunk(...'fdAT', ...'\x00\x00\x00\x02img2') -> None
|
|
1532
|
+
exit _next_IDAT -> None
|
|
1533
|
+
enter _next_IDAT(...'data')
|
|
1534
|
+
call _fctl() -> None
|
|
1535
|
+
call _seqno() -> ...'\x00\x00\x00\x03'
|
|
1536
|
+
call _chunk(...'fdAT', ...'\x00\x00\x00\x03data') -> None
|
|
1537
|
+
exit _next_IDAT -> None
|
|
1538
|
+
"""
|
|
1539
|
+
self._fctl()
|
|
1540
|
+
maxlen = 0x7ffffffb
|
|
1541
|
+
while len(data) > maxlen:
|
|
1542
|
+
self._chunk(b"fdAT", self._seqno() + data[:maxlen])
|
|
1543
|
+
data = data[maxlen:]
|
|
1544
|
+
self._chunk(b"fdAT", self._seqno() + data)
|
|
1545
|
+
|
|
1546
|
+
def _copy(self):
|
|
1547
|
+
r"""
|
|
1548
|
+
Copy an existing chunk without modification.
|
|
1549
|
+
|
|
1550
|
+
TESTS::
|
|
1551
|
+
|
|
1552
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1553
|
+
sage: APngAssembler._testCase1("_copy")
|
|
1554
|
+
enter _copy()
|
|
1555
|
+
read _current_chunk = (...'\x00\x00\x00\r',...'IHDR',...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00',...'\xb8\x1f9\xc6')
|
|
1556
|
+
read out = <_io.BytesIO... at ...
|
|
1557
|
+
read out = <_io.BytesIO... at ...
|
|
1558
|
+
read out = <_io.BytesIO... at ...
|
|
1559
|
+
read out = <_io.BytesIO... at ...
|
|
1560
|
+
exit _copy -> None
|
|
1561
|
+
enter _copy()
|
|
1562
|
+
read _current_chunk = (...'\x00\x00\x00\x04',...'gAMA',...'\x00\x01\x86\xa0',...'1\xe8\x96_')
|
|
1563
|
+
...
|
|
1564
|
+
read _current_chunk = (...'\x00\x00\x00\x08',...'IDAT',...'img1data',...'\xce\x8aI\x99')
|
|
1565
|
+
...
|
|
1566
|
+
exit _copy -> None
|
|
1567
|
+
"""
|
|
1568
|
+
for d in self._current_chunk:
|
|
1569
|
+
self.out.write(d)
|
|
1570
|
+
|
|
1571
|
+
def _actl(self):
|
|
1572
|
+
r"""
|
|
1573
|
+
Write animation control data (acTL).
|
|
1574
|
+
|
|
1575
|
+
TESTS::
|
|
1576
|
+
|
|
1577
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1578
|
+
sage: APngAssembler._testCase1("_actl")
|
|
1579
|
+
enter _actl()
|
|
1580
|
+
read _actl_written = False
|
|
1581
|
+
read num_frames = 2
|
|
1582
|
+
read num_plays = 0
|
|
1583
|
+
call _chunk(...'acTL',...'\x00\x00\x00\x02\x00\x00\x00\x00') -> None
|
|
1584
|
+
write _actl_written = True
|
|
1585
|
+
exit _actl -> None
|
|
1586
|
+
"""
|
|
1587
|
+
if self._actl_written:
|
|
1588
|
+
return
|
|
1589
|
+
data = struct.pack(">2L", self.num_frames, self.num_plays)
|
|
1590
|
+
self._chunk(b"acTL", data)
|
|
1591
|
+
self._actl_written = True
|
|
1592
|
+
|
|
1593
|
+
def _fctl(self):
|
|
1594
|
+
r"""
|
|
1595
|
+
Write frame control data (fcTL).
|
|
1596
|
+
|
|
1597
|
+
TESTS::
|
|
1598
|
+
|
|
1599
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1600
|
+
sage: APngAssembler._testCase1("_fctl")
|
|
1601
|
+
enter _fctl()
|
|
1602
|
+
read _fctl_written = False
|
|
1603
|
+
read width = 3
|
|
1604
|
+
read height = 2
|
|
1605
|
+
read delay_numerator = 1383
|
|
1606
|
+
read delay_denominator = 4660
|
|
1607
|
+
call _seqno() -> ...'\x00\x00\x00\x00'
|
|
1608
|
+
call _chunk(...'fcTL', ...'\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x05g\x124\x01\x00') -> None
|
|
1609
|
+
write _fctl_written = True
|
|
1610
|
+
exit _fctl -> None
|
|
1611
|
+
enter _fctl()
|
|
1612
|
+
read _fctl_written = False
|
|
1613
|
+
read width = 3
|
|
1614
|
+
read height = 2
|
|
1615
|
+
read delay_numerator = 200
|
|
1616
|
+
read delay_denominator = 100
|
|
1617
|
+
call _seqno() -> ...'\x00\x00\x00\x01'
|
|
1618
|
+
call _chunk(...'fcTL', ...'\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00d\x01\x00') -> None
|
|
1619
|
+
write _fctl_written = True
|
|
1620
|
+
exit _fctl -> None
|
|
1621
|
+
enter _fctl()
|
|
1622
|
+
read _fctl_written = True
|
|
1623
|
+
exit _fctl -> None
|
|
1624
|
+
"""
|
|
1625
|
+
if self._fctl_written:
|
|
1626
|
+
return
|
|
1627
|
+
data = struct.pack(
|
|
1628
|
+
">4L2H2B",
|
|
1629
|
+
self.width, self.height, 0, 0,
|
|
1630
|
+
self.delay_numerator, self.delay_denominator,
|
|
1631
|
+
1, 0)
|
|
1632
|
+
self._chunk(b"fcTL", self._seqno() + data)
|
|
1633
|
+
self._fctl_written = True
|
|
1634
|
+
|
|
1635
|
+
def _chunk(self, ctype, cdata):
|
|
1636
|
+
r"""
|
|
1637
|
+
Write a new (or modified) chunk of data.
|
|
1638
|
+
|
|
1639
|
+
TESTS::
|
|
1640
|
+
|
|
1641
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1642
|
+
sage: from io import BytesIO
|
|
1643
|
+
sage: buf = BytesIO()
|
|
1644
|
+
sage: apng = APngAssembler(buf, 1)
|
|
1645
|
+
sage: buf.getvalue() == b'\x89PNG\r\n\x1a\n'
|
|
1646
|
+
True
|
|
1647
|
+
sage: apng._chunk(b"abcd", b"efgh")
|
|
1648
|
+
sage: buf.getvalue() == b'\x89PNG\r\n\x1a\n\x00\x00\x00\x04abcdefgh\xae\xef*P'
|
|
1649
|
+
True
|
|
1650
|
+
"""
|
|
1651
|
+
ccrc = struct.pack(">L", zlib.crc32(ctype + cdata) & 0xffffffff)
|
|
1652
|
+
clen = struct.pack(">L", len(cdata))
|
|
1653
|
+
for d in [clen, ctype, cdata, ccrc]:
|
|
1654
|
+
self.out.write(d)
|
|
1655
|
+
|
|
1656
|
+
@classmethod
|
|
1657
|
+
def _hex2bin(cls, h):
|
|
1658
|
+
r"""
|
|
1659
|
+
Convert hex data to binary.
|
|
1660
|
+
|
|
1661
|
+
This is a helper method used for testing.
|
|
1662
|
+
Most data is given as lower-case hex digits,
|
|
1663
|
+
possibly intermixed with whitespace.
|
|
1664
|
+
A dot causes the next four bytes to be copied verbatim
|
|
1665
|
+
even if they look like hex digits. This is used for chunk types.
|
|
1666
|
+
Other characters which are not hex digits are passed verbatim.
|
|
1667
|
+
|
|
1668
|
+
EXAMPLES::
|
|
1669
|
+
|
|
1670
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1671
|
+
sage: h2b = APngAssembler._hex2bin
|
|
1672
|
+
sage: h2b("0123") == b"\x01\x23"
|
|
1673
|
+
True
|
|
1674
|
+
sage: h2b(" 01 \n 23 ") == b"\x01\x23"
|
|
1675
|
+
True
|
|
1676
|
+
sage: h2b(".abcdef") == b"abcd\xef"
|
|
1677
|
+
True
|
|
1678
|
+
sage: h2b("PNG") == b"PNG"
|
|
1679
|
+
True
|
|
1680
|
+
"""
|
|
1681
|
+
b = []
|
|
1682
|
+
while h:
|
|
1683
|
+
if h[0] in ' \n': # ignore whitespace
|
|
1684
|
+
h = h[1:]
|
|
1685
|
+
elif h[0] in '0123456789abcdef': # hex byte
|
|
1686
|
+
b.append(int(h[:2], 16))
|
|
1687
|
+
h = h[2:]
|
|
1688
|
+
elif h[0] == '.': # for chunk type
|
|
1689
|
+
b.extend(ord(h[i]) for i in range(1, 5))
|
|
1690
|
+
h = h[5:]
|
|
1691
|
+
else: # for PNG magic
|
|
1692
|
+
b.append(ord(h[0]))
|
|
1693
|
+
h = h[1:]
|
|
1694
|
+
|
|
1695
|
+
return bytes(b)
|
|
1696
|
+
|
|
1697
|
+
@classmethod
|
|
1698
|
+
def _testData(cls, name, asFile):
|
|
1699
|
+
r"""
|
|
1700
|
+
Retrieve data for test cases.
|
|
1701
|
+
|
|
1702
|
+
INPUT:
|
|
1703
|
+
|
|
1704
|
+
- ``name`` -- the name of the file content.
|
|
1705
|
+
|
|
1706
|
+
- ``asFile`` -- whether to return a binary string of the named data
|
|
1707
|
+
or the path of a file containing that data
|
|
1708
|
+
|
|
1709
|
+
EXAMPLES::
|
|
1710
|
+
|
|
1711
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1712
|
+
sage: APngAssembler._testData("input1", False).startswith(b'\x89PNG')
|
|
1713
|
+
True
|
|
1714
|
+
sage: APngAssembler._testData("input2", True)
|
|
1715
|
+
'...png'
|
|
1716
|
+
"""
|
|
1717
|
+
data = {
|
|
1718
|
+
|
|
1719
|
+
# Input 1: one PNG image, except the data makes no real sense
|
|
1720
|
+
"input1": """89 PNG 0d0a1a0a
|
|
1721
|
+
0000000d.IHDR 00000003000000020800000000 b81f39c6
|
|
1722
|
+
00000004.gAMA 000186a0 31e8965f
|
|
1723
|
+
00000007.tIME 07de061b0b2624 1f307ad5
|
|
1724
|
+
00000008.IDAT 696d673164617461 ce8a4999
|
|
1725
|
+
00000000.IEND ae426082""",
|
|
1726
|
+
|
|
1727
|
+
# Input 2: slightly different, data in two chunks
|
|
1728
|
+
"input2": """89 PNG 0d0a1a0a
|
|
1729
|
+
0000000d.IHDR 00000003000000020800000000 b81f39c6
|
|
1730
|
+
00000004.gAMA 000186a0 31e8965f
|
|
1731
|
+
00000004.IDAT 696d6732 0e69ab1d
|
|
1732
|
+
00000004.IDAT 64617461 6694cb78
|
|
1733
|
+
00000000.IEND ae426082""",
|
|
1734
|
+
|
|
1735
|
+
# Expected output 1: both images as frames of an animation
|
|
1736
|
+
"anim12": """89 PNG 0d0a1a0a
|
|
1737
|
+
0000000d.IHDR 00000003000000020800000000 b81f39c6
|
|
1738
|
+
00000004.gAMA 000186a0 31e8965f
|
|
1739
|
+
00000008.acTL 0000000200000000 f38d9370
|
|
1740
|
+
0000001a.fcTL 000000000000000300000002
|
|
1741
|
+
0000000000000000056712340100 b4f729c9
|
|
1742
|
+
00000008.IDAT 696d673164617461 ce8a4999
|
|
1743
|
+
0000001a.fcTL 000000010000000300000002
|
|
1744
|
+
000000000000000000c800640100 1b92eb4d
|
|
1745
|
+
00000008.fdAT 00000002696d6732 9cfb89a3
|
|
1746
|
+
00000008.fdAT 0000000364617461 c966c076
|
|
1747
|
+
00000000.IEND ae426082""",
|
|
1748
|
+
|
|
1749
|
+
# Expected output 2: first image as fallback, second as animation
|
|
1750
|
+
"still1anim2": """89 PNG 0d0a1a0a
|
|
1751
|
+
0000000d.IHDR 00000003000000020800000000 b81f39c6
|
|
1752
|
+
00000004.gAMA 000186a0 31e8965f
|
|
1753
|
+
00000008.acTL 0000000100000000 b42de9a0
|
|
1754
|
+
00000008.IDAT 696d673164617461 ce8a4999
|
|
1755
|
+
0000001a.fcTL 000000000000000300000002
|
|
1756
|
+
0000000000000000056712340100 b4f729c9
|
|
1757
|
+
00000008.fdAT 00000001696d6732 db5bf373
|
|
1758
|
+
00000008.fdAT 0000000264617461 f406e9c6
|
|
1759
|
+
00000000.IEND ae426082""",
|
|
1760
|
+
|
|
1761
|
+
}
|
|
1762
|
+
d = cls._hex2bin(data[name])
|
|
1763
|
+
if asFile:
|
|
1764
|
+
from sage.misc.temporary_file import tmp_filename
|
|
1765
|
+
fn = tmp_filename(ext='.png')
|
|
1766
|
+
with open(fn, 'wb') as f:
|
|
1767
|
+
f.write(d)
|
|
1768
|
+
return fn
|
|
1769
|
+
return d
|
|
1770
|
+
|
|
1771
|
+
@classmethod
|
|
1772
|
+
def _testCase1(cls, methodToTrace=None, **kwds):
|
|
1773
|
+
r"""
|
|
1774
|
+
Run common test case.
|
|
1775
|
+
|
|
1776
|
+
This test case is one animation of two frames.
|
|
1777
|
+
The named method (if not None) will be traced during execution.
|
|
1778
|
+
This will demonstrate the role of each method in the doctests.
|
|
1779
|
+
|
|
1780
|
+
TESTS::
|
|
1781
|
+
|
|
1782
|
+
sage: from sage.plot.animate import APngAssembler
|
|
1783
|
+
sage: APngAssembler._testCase1()
|
|
1784
|
+
"""
|
|
1785
|
+
from sage.doctest.fixtures import trace_method
|
|
1786
|
+
from io import BytesIO
|
|
1787
|
+
buf = BytesIO()
|
|
1788
|
+
apng = cls(buf, 2)
|
|
1789
|
+
if methodToTrace is not None:
|
|
1790
|
+
trace_method(apng, methodToTrace, **kwds)
|
|
1791
|
+
apng.add_frame(cls._testData("input1", True),
|
|
1792
|
+
delay=0x567, delay_denominator=0x1234)
|
|
1793
|
+
apng.add_frame(cls._testData("input2", True))
|
|
1794
|
+
out = buf.getvalue()
|
|
1795
|
+
assert len(out) == 217
|
|
1796
|
+
assert out == cls._testData("anim12", False)
|