diplotocus 0.0.0__py3-none-any.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.
- diplotocus/GUI/GUI.html +120 -0
- diplotocus/GUI/__init__.py +2 -0
- diplotocus/GUI/app.js +1646 -0
- diplotocus/GUI/server.py +1352 -0
- diplotocus/GUI/src/bg.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Draw.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Morph.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Rotate.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Scale.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Sequence.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Translate.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_Tween.png +0 -0
- diplotocus/GUI/src/btn/btn_bg_default.png +0 -0
- diplotocus/GUI/src/favicon.ico +0 -0
- diplotocus/GUI/src/logo_bg.png +0 -0
- diplotocus/GUI/style.css +622 -0
- diplotocus/__init__.py +4 -0
- diplotocus/_version.py +1 -0
- diplotocus/animations.py +3686 -0
- diplotocus/core.py +358 -0
- diplotocus/easings.py +284 -0
- diplotocus-0.0.0.dist-info/METADATA +16 -0
- diplotocus-0.0.0.dist-info/RECORD +26 -0
- diplotocus-0.0.0.dist-info/WHEEL +5 -0
- diplotocus-0.0.0.dist-info/licenses/LICENSE +21 -0
- diplotocus-0.0.0.dist-info/top_level.txt +1 -0
diplotocus/core.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import subprocess,os,shlex,pickle
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import matplotlib
|
|
5
|
+
import warnings
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
matplotlib.use("Agg")
|
|
8
|
+
import shutil
|
|
9
|
+
try:
|
|
10
|
+
has_ipython = True
|
|
11
|
+
from IPython.display import HTML, display
|
|
12
|
+
except:
|
|
13
|
+
has_ipython = False
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
from .easings import *
|
|
17
|
+
from .animations import *
|
|
18
|
+
|
|
19
|
+
if shutil.which("ffmpeg") is None:
|
|
20
|
+
warnings.warn("FFmpeg is required to render videos, but is not installed. Please install it system-wide.")
|
|
21
|
+
|
|
22
|
+
def in_notebook():
|
|
23
|
+
try:
|
|
24
|
+
from IPython import get_ipython
|
|
25
|
+
from IPython.display import HTML, display
|
|
26
|
+
shell = get_ipython().__class__.__name__
|
|
27
|
+
return shell == "ZMQInteractiveShell"
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
if in_notebook():
|
|
32
|
+
try:
|
|
33
|
+
from tqdm.notebook import tqdm
|
|
34
|
+
has_tqdm = True
|
|
35
|
+
except:
|
|
36
|
+
has_tqdm = False
|
|
37
|
+
else:
|
|
38
|
+
try:
|
|
39
|
+
from tqdm import tqdm
|
|
40
|
+
has_tqdm = True
|
|
41
|
+
except:
|
|
42
|
+
has_tqdm = False
|
|
43
|
+
|
|
44
|
+
def status_message(start_msg, end_msg):
|
|
45
|
+
if in_notebook():
|
|
46
|
+
from IPython.display import display, update_display, Markdown
|
|
47
|
+
display_id = display(Markdown(start_msg), display_id=True)
|
|
48
|
+
def update():
|
|
49
|
+
update_display(Markdown(end_msg), display_id=display_id.display_id)
|
|
50
|
+
return update
|
|
51
|
+
else:
|
|
52
|
+
print(start_msg, end="\r", flush=True)
|
|
53
|
+
def update():
|
|
54
|
+
print(end_msg + " " * max(0, len(start_msg) - len(end_msg)))
|
|
55
|
+
return update
|
|
56
|
+
|
|
57
|
+
#----------------------------------------------
|
|
58
|
+
# TIMELINE CLASS
|
|
59
|
+
#----------------------------------------------
|
|
60
|
+
|
|
61
|
+
def load_project(path):
|
|
62
|
+
tl = pickle.load(open(path, 'rb'))
|
|
63
|
+
animations = tl.animations
|
|
64
|
+
tl.x = 0
|
|
65
|
+
tl.clean_all()
|
|
66
|
+
return tl,animations
|
|
67
|
+
|
|
68
|
+
class Timeline:
|
|
69
|
+
"""Create and manage an animation timeline.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
name : str
|
|
74
|
+
the name used for the rendered images folder.
|
|
75
|
+
fig : Figure | None
|
|
76
|
+
an existing Figure to plot on. If None, a new Figure will be created.
|
|
77
|
+
quiet : bool
|
|
78
|
+
if True, any message will not be printed, and no progress bar will be shown.
|
|
79
|
+
dpi : int
|
|
80
|
+
The DPI of the rendered images (higher = better resolution/slower render).
|
|
81
|
+
transparent : bool
|
|
82
|
+
if True, the background color of the Figure and axes will be transparent. Can be used to render a transparent video (only compatible with a .mov video file).
|
|
83
|
+
axis_color : str
|
|
84
|
+
if True, changes the color of the axes, ticks and labels to the specified color.
|
|
85
|
+
noaxis : bool
|
|
86
|
+
if True, hides the axis lines, ticks and labels.
|
|
87
|
+
easing : easing
|
|
88
|
+
a global easing to be used if no other easing is specified on an animation.
|
|
89
|
+
xlim : tuple | None
|
|
90
|
+
if fig is None, sets the X limits of the created axis.
|
|
91
|
+
ylim : tuple | None
|
|
92
|
+
if fig is None, sets the Y limits of the created axis.
|
|
93
|
+
"""
|
|
94
|
+
def __init__(self,
|
|
95
|
+
name='Unnamed',
|
|
96
|
+
fig=None,
|
|
97
|
+
quiet=False,
|
|
98
|
+
dpi=200,
|
|
99
|
+
transparent=False,
|
|
100
|
+
axis_color='k',
|
|
101
|
+
noaxis=False,
|
|
102
|
+
easing=easeLinear(),
|
|
103
|
+
xlim=None,
|
|
104
|
+
ylim=None,
|
|
105
|
+
figsize=None
|
|
106
|
+
):
|
|
107
|
+
if fig is None:
|
|
108
|
+
if figsize is not None:
|
|
109
|
+
fig,_ = plt.subplots(figsize=figsize)
|
|
110
|
+
else:
|
|
111
|
+
fig,_ = plt.subplots()
|
|
112
|
+
self.fig = fig
|
|
113
|
+
self.ax = self.fig.get_axes()
|
|
114
|
+
if len(self.ax) == 1:
|
|
115
|
+
self.ax = self.ax[0]
|
|
116
|
+
else:
|
|
117
|
+
self.ax = np.array(self.ax)
|
|
118
|
+
if isinstance(self.ax,np.ndarray):
|
|
119
|
+
if isinstance(self.ax[0],np.ndarray):
|
|
120
|
+
self.main_axis = self.ax[0,0]
|
|
121
|
+
else:
|
|
122
|
+
self.main_axis = self.ax[0]
|
|
123
|
+
else:
|
|
124
|
+
self.main_axis = self.ax
|
|
125
|
+
if xlim is not None:
|
|
126
|
+
self.main_axis.set_xlim(*xlim)
|
|
127
|
+
if ylim is not None:
|
|
128
|
+
self.main_axis.set_ylim(*ylim)
|
|
129
|
+
self.name = name
|
|
130
|
+
self.quiet = quiet
|
|
131
|
+
self.dpi = dpi
|
|
132
|
+
self.easing = easing
|
|
133
|
+
self.x = 0
|
|
134
|
+
self.timeline_str = ''
|
|
135
|
+
self.transparent = transparent
|
|
136
|
+
self.animations = []
|
|
137
|
+
if in_notebook() == False:
|
|
138
|
+
namespace = sys._getframe(1).f_globals['__file__']
|
|
139
|
+
self.full_path = '/'.join(namespace.split('/')[:-1])
|
|
140
|
+
if axis_color != 'k':
|
|
141
|
+
self.set_axis_color(axis_color)
|
|
142
|
+
if noaxis:
|
|
143
|
+
self.main_axis.set_axis_off()
|
|
144
|
+
|
|
145
|
+
def set_axis_color(self,color):
|
|
146
|
+
if isinstance(self.ax,np.ndarray) == False:
|
|
147
|
+
axes = (self.ax,)
|
|
148
|
+
else:
|
|
149
|
+
axes = self.ax.flatten()
|
|
150
|
+
for axis in axes:
|
|
151
|
+
axis.spines['bottom'].set_color(color)
|
|
152
|
+
axis.spines['top'].set_color(color)
|
|
153
|
+
axis.spines['left'].set_color(color)
|
|
154
|
+
axis.spines['right'].set_color(color)
|
|
155
|
+
axis.xaxis.label.set_color(color)
|
|
156
|
+
axis.yaxis.label.set_color(color)
|
|
157
|
+
axis.tick_params(axis='both',colors=color)
|
|
158
|
+
|
|
159
|
+
def clean_all(self):
|
|
160
|
+
axes = np.ravel(self.ax)
|
|
161
|
+
for axis in axes:
|
|
162
|
+
for line in axis.lines:
|
|
163
|
+
line = np.ravel(line)
|
|
164
|
+
for l in line:
|
|
165
|
+
if l.get_animated():
|
|
166
|
+
l.remove()
|
|
167
|
+
for coll in axis.collections:
|
|
168
|
+
coll = np.ravel(coll)
|
|
169
|
+
for c in coll:
|
|
170
|
+
if c.get_animated():
|
|
171
|
+
c.remove()
|
|
172
|
+
for patch in axis.patches:
|
|
173
|
+
patch = np.ravel(patch)
|
|
174
|
+
for p in patch:
|
|
175
|
+
if p.get_animated():
|
|
176
|
+
p.remove()
|
|
177
|
+
|
|
178
|
+
def plot(self,animations,x=0,easing=None,debug=False):
|
|
179
|
+
if isinstance(animations,Animation):
|
|
180
|
+
animations = (animations,)
|
|
181
|
+
|
|
182
|
+
for animation in animations:
|
|
183
|
+
if animation.obj is not None and ((animation.x_min <= x < animation.x_max) or (x >= animation.x_max and animation.persistent == False)):
|
|
184
|
+
objects = np.ravel(animation.obj)
|
|
185
|
+
for obj in objects:
|
|
186
|
+
try:
|
|
187
|
+
obj.remove()
|
|
188
|
+
except:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
animation.initialize(self)
|
|
192
|
+
#only plot if within an anim or if last one is persistent
|
|
193
|
+
last_anim = None
|
|
194
|
+
closest_dist = np.inf
|
|
195
|
+
for anim in animation.anims:
|
|
196
|
+
anim_x_max = anim['duration']+anim['delay']
|
|
197
|
+
if anim_x_max > x:
|
|
198
|
+
continue
|
|
199
|
+
dist = x - anim_x_max
|
|
200
|
+
if dist < closest_dist:
|
|
201
|
+
closest_dist = dist
|
|
202
|
+
last_anim = anim
|
|
203
|
+
is_within_anim = False
|
|
204
|
+
if last_anim is not None:
|
|
205
|
+
is_within_anim = last_anim['persistent'] == True
|
|
206
|
+
if is_within_anim == False:
|
|
207
|
+
for anim in animation.anims:
|
|
208
|
+
anim_x_min = anim['delay']
|
|
209
|
+
anim_x_max = anim['duration']+anim['delay']
|
|
210
|
+
if anim_x_min <= x < anim_x_max:
|
|
211
|
+
is_within_anim = True
|
|
212
|
+
break
|
|
213
|
+
if is_within_anim:
|
|
214
|
+
animation.apply(x,easing)
|
|
215
|
+
prev_x = self.x
|
|
216
|
+
self.x = x
|
|
217
|
+
self.save_plot(debug)
|
|
218
|
+
for animation in animations:
|
|
219
|
+
animation.clean(x,clear_anims=False)
|
|
220
|
+
self.x = prev_x
|
|
221
|
+
|
|
222
|
+
def animate(self,plot_objects,easing=None,debug=False):
|
|
223
|
+
"""Render one or more plot objects into the timeline.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
plot_objects : Animation or sequence of Animation
|
|
228
|
+
a single plot object (e.g. `scatter`, `plot`, `hist`...) or a list of plot objects.
|
|
229
|
+
easing : easing
|
|
230
|
+
an easing override used while rendering.
|
|
231
|
+
debug : bool
|
|
232
|
+
if True, skip writing frame images to disk.
|
|
233
|
+
"""
|
|
234
|
+
if isinstance(plot_objects,Animation):
|
|
235
|
+
plot_objects = (plot_objects,)
|
|
236
|
+
self.animations = deepcopy(np.array(plot_objects))
|
|
237
|
+
|
|
238
|
+
x_max = 0
|
|
239
|
+
for animation in plot_objects:
|
|
240
|
+
for anim in animation.anims:
|
|
241
|
+
if anim['played'] == False:
|
|
242
|
+
anim['delay'] += self.x
|
|
243
|
+
animation.initialize(self)
|
|
244
|
+
|
|
245
|
+
if animation.x_max > x_max:
|
|
246
|
+
x_max = animation.x_max
|
|
247
|
+
|
|
248
|
+
loop = range(self.x,x_max)
|
|
249
|
+
if not self.quiet and has_tqdm:
|
|
250
|
+
loop = tqdm(loop)
|
|
251
|
+
|
|
252
|
+
for x in loop:
|
|
253
|
+
self.plot(animations=plot_objects,x=x,easing=easing,debug=debug)
|
|
254
|
+
for animation in plot_objects:
|
|
255
|
+
for anim in animation.anims:
|
|
256
|
+
anim['played'] = True
|
|
257
|
+
self.x = x_max
|
|
258
|
+
|
|
259
|
+
def wait(self,duration):
|
|
260
|
+
"""Pause the animation for a number of frames.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
duration : int
|
|
265
|
+
the number of frames to pause the animation for.
|
|
266
|
+
"""
|
|
267
|
+
if self.x == 0:
|
|
268
|
+
raise UserWarning('No rendered frames, run animate with animations to generate frames !')
|
|
269
|
+
|
|
270
|
+
last_frame = self.x - 1
|
|
271
|
+
source_png = self.name + '/' + self.name + '_{}.png'.format(last_frame)
|
|
272
|
+
for _ in range(duration):
|
|
273
|
+
target_png = self.name + '/' + self.name + '_{}.png'.format(self.x)
|
|
274
|
+
shutil.copyfile(source_png, target_png)
|
|
275
|
+
self.timeline_str += 'file \' ' + self.name + '_{}.png\'\n'.format(self.x)
|
|
276
|
+
self.timeline_str += 'duration {}\n'.format(1/30)
|
|
277
|
+
self.x += 1
|
|
278
|
+
|
|
279
|
+
def save_plot(self,debug):
|
|
280
|
+
if debug is False:
|
|
281
|
+
if os.path.isdir(self.name) == False:
|
|
282
|
+
os.makedirs(self.name)
|
|
283
|
+
fn = self.name + '/' + self.name + '_{}.png'.format(self.x)
|
|
284
|
+
self.fig.savefig(fn,dpi=self.dpi,transparent=self.transparent)
|
|
285
|
+
|
|
286
|
+
self.timeline_str += 'file \'' + self.name + '_{}.png\'\n'.format(self.x)
|
|
287
|
+
self.timeline_str += 'duration {}\n'.format(1/30)
|
|
288
|
+
self.x += 1
|
|
289
|
+
|
|
290
|
+
def save_video(self,path=None,speed=1,ffmpeg_path='ffmpeg',multialpha=False,prerendered=False,clean=True):
|
|
291
|
+
"""Render the current timeline to a video file.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
path : str | None
|
|
296
|
+
the path to save the video file to.
|
|
297
|
+
speed : float
|
|
298
|
+
by default, animations are rendered at 30fps (frames per second). You can change this by setting the speed. For 60fps videos, choose speed=2.
|
|
299
|
+
ffmpeg_path : str
|
|
300
|
+
if ffmpeg is not recognised as is, you can set the path to the ffmpeg executable through this parameter.
|
|
301
|
+
multialpha : bool
|
|
302
|
+
when rendering to a transparent .mov video, if you have semi-transparent frames, can be used to get better results.
|
|
303
|
+
prerendered : bool
|
|
304
|
+
if save_video() was already called and clean was set to False, image files and the text list file associated still exist. If you want to rerender the video, without having to rerender all the frames, you can set prerendered to true, which will only run the ffmpeg command.
|
|
305
|
+
clean : bool
|
|
306
|
+
if True, image files and the text list file associated are deleted after the video has been rendered.
|
|
307
|
+
"""
|
|
308
|
+
if self.x == 0:
|
|
309
|
+
raise UserWarning('No rendered frames, run animate with animations to generate frames !')
|
|
310
|
+
if path is None:
|
|
311
|
+
if self.transparent:
|
|
312
|
+
path = self.name + '.mov'
|
|
313
|
+
else:
|
|
314
|
+
path = self.name + '.mp4'
|
|
315
|
+
video_fn = path
|
|
316
|
+
is_gif = video_fn.lower().endswith('.gif')
|
|
317
|
+
if not self.quiet:
|
|
318
|
+
update = status_message("Rendering video...", "Saved video " + video_fn)
|
|
319
|
+
if not prerendered:
|
|
320
|
+
self.timeline_str = ''
|
|
321
|
+
for i in range(self.x):
|
|
322
|
+
self.timeline_str += 'file \'' + self.name + '_{}.png\'\n'.format(i)
|
|
323
|
+
self.timeline_str += 'duration {}\n'.format(1/30/speed)
|
|
324
|
+
self.timeline_str += 'file \'' + self.name + '_{}.png\'\n'.format(i)
|
|
325
|
+
self.timeline_str += 'duration {}\n'.format(1/30/speed)
|
|
326
|
+
with open(self.name + '/' + self.name + '.txt','w+') as f:
|
|
327
|
+
f.write(self.timeline_str)
|
|
328
|
+
sequence_fn = self.name + '/' + self.name + '.txt'
|
|
329
|
+
|
|
330
|
+
if '.mov' in video_fn:
|
|
331
|
+
if multialpha:
|
|
332
|
+
command = ffmpeg_path + ' -f concat -safe 0 -i {} -y -vf premultiply=inplace=1 -c:v prores_ks -profile:v 4 -pix_fmt yuva444p10le -hide_banner -loglevel error {}'.format(sequence_fn,video_fn)
|
|
333
|
+
else:
|
|
334
|
+
command = ffmpeg_path + ' -f concat -safe 0 -i {} -y -c:v prores -pix_fmt yuva444p10le -hide_banner -loglevel error {}'.format(sequence_fn,video_fn)
|
|
335
|
+
elif is_gif:
|
|
336
|
+
command = ffmpeg_path + ' -f concat -safe 0 -i {} -y -lavfi "split[s0][s1];[s0]palettegen=reserve_transparent=1:stats_mode=diff[p];[s1][p]paletteuse=dither=sierra2_4a:alpha_threshold=128" -gifflags -offsetting -loop 0 -hide_banner -loglevel error {}'.format(sequence_fn,video_fn)
|
|
337
|
+
else:
|
|
338
|
+
command = ffmpeg_path + ' -f concat -safe 0 -i {} -c:v libx264 -pix_fmt yuv420p -c:a aac -movflags +faststart -hide_banner -loglevel error -y {}'.format(sequence_fn,video_fn)
|
|
339
|
+
subprocess.run(shlex.split(command))
|
|
340
|
+
if clean:
|
|
341
|
+
shutil.rmtree(self.name)
|
|
342
|
+
plt.close()
|
|
343
|
+
if not self.quiet:
|
|
344
|
+
update()
|
|
345
|
+
|
|
346
|
+
rand_id = np.random.randint(0,10_000)
|
|
347
|
+
if has_ipython:
|
|
348
|
+
if is_gif:
|
|
349
|
+
return display(HTML('<img src="{}?randId={}" style="max-width:640px;height:auto;" />'.format(video_fn,rand_id)))
|
|
350
|
+
return display(HTML("""
|
|
351
|
+
<video width="640" height="360" autoplay loop muted playsinline>
|
|
352
|
+
<source src="{}?randId={}" type="video/mp4">
|
|
353
|
+
</video>
|
|
354
|
+
""".format(video_fn,rand_id)))
|
|
355
|
+
|
|
356
|
+
def save_project(self,path):
|
|
357
|
+
with open(path, 'wb') as f:
|
|
358
|
+
pickle.dump(self, f)
|
diplotocus/easings.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sys, inspect
|
|
3
|
+
#----------------------------------------------
|
|
4
|
+
# EASING CLASSES
|
|
5
|
+
#----------------------------------------------
|
|
6
|
+
#TODO: add invert method to easing to behave in the inverted way so all inverted animations can work both ways
|
|
7
|
+
#TODO: change plot of easings.ipynb like this : https://api.flutter.dev/flutter/animation/Curves/easeInBack-constant.html
|
|
8
|
+
|
|
9
|
+
class Easing:
|
|
10
|
+
def __init__(self, func=None):
|
|
11
|
+
self.func = func if func is not None else (lambda x: x)
|
|
12
|
+
|
|
13
|
+
def ease(self,x):
|
|
14
|
+
return self.func(x)
|
|
15
|
+
|
|
16
|
+
class easeCubicBezier(Easing):
|
|
17
|
+
def bezier(self, t, a1, a2):
|
|
18
|
+
return (
|
|
19
|
+
3 * (1 - t)**2 * t * a1
|
|
20
|
+
+ 3 * (1 - t) * t**2 * a2
|
|
21
|
+
+ t**3
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def bezier_derivative(self, t, a1, a2):
|
|
25
|
+
return (
|
|
26
|
+
3 * (1 - t)**2 * a1
|
|
27
|
+
+ 6 * (1 - t) * t * (a2 - a1)
|
|
28
|
+
+ 3 * t**2 * (1 - a2)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def ease(self, times):
|
|
32
|
+
times = np.asarray(times, dtype=float)
|
|
33
|
+
times_clipped = np.clip(times, 0.0, 1.0)
|
|
34
|
+
|
|
35
|
+
# Initial guess: t ≈ x
|
|
36
|
+
t = times_clipped.copy()
|
|
37
|
+
|
|
38
|
+
# Newton–Raphson (vectorized)
|
|
39
|
+
for _ in range(self.max_iter):
|
|
40
|
+
x_t = self.bezier(t, self.x1, self.x2)
|
|
41
|
+
dx = x_t - times_clipped
|
|
42
|
+
|
|
43
|
+
if np.all(np.abs(dx) < self.epsilon):
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
dxd_t = self.bezier_derivative(t, self.x1, self.x2)
|
|
47
|
+
mask = np.abs(dxd_t) > 1e-8
|
|
48
|
+
if isinstance(t,np.ndarray):
|
|
49
|
+
t[mask] -= dx[mask] / dxd_t[mask]
|
|
50
|
+
else:
|
|
51
|
+
t -= dx[mask] / dxd_t[mask]
|
|
52
|
+
|
|
53
|
+
# Keep t in bounds
|
|
54
|
+
t = np.clip(t, 0.0, 1.0)
|
|
55
|
+
|
|
56
|
+
# Compute y(t)
|
|
57
|
+
y = self.bezier(t, self.y1, self.y2)
|
|
58
|
+
|
|
59
|
+
# Respect exact endpoints
|
|
60
|
+
if isinstance(y,np.ndarray):
|
|
61
|
+
y[times <= 0.0] = 0.0
|
|
62
|
+
y[times >= 1.0] = 1.0
|
|
63
|
+
else:
|
|
64
|
+
if times <= 0.0:
|
|
65
|
+
y = 0.0
|
|
66
|
+
elif times >= 1.0:
|
|
67
|
+
y = 1.0
|
|
68
|
+
|
|
69
|
+
if isinstance(y,np.ndarray) and len(y) == 1:
|
|
70
|
+
return y[0]
|
|
71
|
+
return y
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __init__(self,x1,y1,x2,y2,epsilon=1e-6, max_iter=10):
|
|
75
|
+
self.x1 = x1
|
|
76
|
+
self.x2 = x2
|
|
77
|
+
self.y1 = y1
|
|
78
|
+
self.y2 = y2
|
|
79
|
+
self.epsilon = epsilon
|
|
80
|
+
self.max_iter = max_iter
|
|
81
|
+
|
|
82
|
+
#LINEAR
|
|
83
|
+
class easeLinear(Easing):
|
|
84
|
+
def __init__(self):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def func(self,x):
|
|
88
|
+
return x
|
|
89
|
+
|
|
90
|
+
# SINE
|
|
91
|
+
class easeInSine(Easing):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
super().__init__(lambda x:1 - np.cos((x * np.pi) / 2))
|
|
94
|
+
|
|
95
|
+
class easeOutSine(Easing):
|
|
96
|
+
def __init__(self):
|
|
97
|
+
super().__init__(lambda x: np.sin((x * np.pi) / 2))
|
|
98
|
+
|
|
99
|
+
class easeInOutSine(Easing):
|
|
100
|
+
def __init__(self):
|
|
101
|
+
super().__init__(lambda x: -(np.cos(np.pi * x) - 1) / 2)
|
|
102
|
+
|
|
103
|
+
# QUADRATIC
|
|
104
|
+
class easeInQuad(Easing):
|
|
105
|
+
def __init__(self):
|
|
106
|
+
super().__init__(lambda x: x**2)
|
|
107
|
+
|
|
108
|
+
class easeOutQuad(Easing):
|
|
109
|
+
def __init__(self):
|
|
110
|
+
super().__init__(lambda x: 1 - (1-x)**2)
|
|
111
|
+
|
|
112
|
+
class easeInOutQuad(Easing):
|
|
113
|
+
def __init__(self):
|
|
114
|
+
super().__init__(lambda x: np.where(x<0.5,2*x**2,1-(-2*x+2)**2 / 2))
|
|
115
|
+
|
|
116
|
+
# SUBIC
|
|
117
|
+
class easeInCubic(Easing):
|
|
118
|
+
def __init__(self):
|
|
119
|
+
super().__init__(lambda x: x**3)
|
|
120
|
+
|
|
121
|
+
class easeOutCubic(Easing):
|
|
122
|
+
def __init__(self):
|
|
123
|
+
super().__init__(lambda x: 1 - (1-x)**3)
|
|
124
|
+
|
|
125
|
+
class easeInOutCubic(Easing):
|
|
126
|
+
def __init__(self):
|
|
127
|
+
super().__init__(lambda x: np.where(x<0.5,4*x**3,1-(-2*x+2)**3 / 2))
|
|
128
|
+
|
|
129
|
+
# QUARTIC
|
|
130
|
+
class easeInQuart(Easing):
|
|
131
|
+
def __init__(self):
|
|
132
|
+
super().__init__(lambda x: x**4)
|
|
133
|
+
|
|
134
|
+
class easeOutQuart(Easing):
|
|
135
|
+
def __init__(self):
|
|
136
|
+
super().__init__(lambda x: 1-(1-x)**4)
|
|
137
|
+
|
|
138
|
+
class easeInOutQuart(Easing):
|
|
139
|
+
def __init__(self):
|
|
140
|
+
super().__init__(lambda x:np.where(x<0.5,8*x**4,1-(-2*x+2)**4 / 2))
|
|
141
|
+
|
|
142
|
+
# QUINTIC
|
|
143
|
+
class easeInQuint(Easing):
|
|
144
|
+
def __init__(self):
|
|
145
|
+
super().__init__(lambda x: x**5)
|
|
146
|
+
|
|
147
|
+
class easeOutQuint(Easing):
|
|
148
|
+
def __init__(self):
|
|
149
|
+
super().__init__(lambda x: 1-(1-x)**5)
|
|
150
|
+
|
|
151
|
+
class easeInOutQuint(Easing):
|
|
152
|
+
def __init__(self):
|
|
153
|
+
super().__init__(lambda x:np.where(x<0.5,16*x**5,1-(-2*x+2)**5 / 2))
|
|
154
|
+
|
|
155
|
+
# EXPONENTIAL
|
|
156
|
+
class easeInExpo(Easing):
|
|
157
|
+
def __init__(self):
|
|
158
|
+
super().__init__(lambda x:np.where(x==0,0,2**(10*x-10)))
|
|
159
|
+
|
|
160
|
+
class easeOutExpo(Easing):
|
|
161
|
+
def __init__(self):
|
|
162
|
+
super().__init__(lambda x:np.where(x==1,1,1-2**(-10*x)))
|
|
163
|
+
|
|
164
|
+
class easeInOutExpo(Easing):
|
|
165
|
+
def __init__(self):
|
|
166
|
+
super().__init__(lambda x:np.where(x==0,0,np.where(x==1,1,np.where(x<0.5,2**(20*x-10) / 2,(2-2**(-20*x+10)) / 2))))
|
|
167
|
+
|
|
168
|
+
# CIRCLE
|
|
169
|
+
class easeInCirc(Easing):
|
|
170
|
+
def __init__(self):
|
|
171
|
+
super().__init__(lambda x: 1 - np.sqrt(1 - x**2))
|
|
172
|
+
|
|
173
|
+
class easeOutCirc(Easing):
|
|
174
|
+
def __init__(self):
|
|
175
|
+
super().__init__(lambda x: np.sqrt(1 - (x - 1)**2))
|
|
176
|
+
|
|
177
|
+
class easeInOutCirc(Easing):
|
|
178
|
+
def __init__(self):
|
|
179
|
+
#(Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2
|
|
180
|
+
#(np.sqrt(1 - (-2*x)**2) + 1) / 2
|
|
181
|
+
super().__init__(lambda x:np.where(
|
|
182
|
+
x<0.5,
|
|
183
|
+
(1 - np.sqrt(np.abs(1 - (2*x)**2))) / 2,
|
|
184
|
+
(np.sqrt(np.abs(1 - (-2*x+2)**2)) + 1) / 2)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# BACK
|
|
188
|
+
class easeInBack(Easing):
|
|
189
|
+
def __init__(self):
|
|
190
|
+
def func(x):
|
|
191
|
+
c1 = 1.70158
|
|
192
|
+
c3 = c1 + 1
|
|
193
|
+
|
|
194
|
+
return c3 * x * x * x - c1 * x * x
|
|
195
|
+
super().__init__(func)
|
|
196
|
+
|
|
197
|
+
class easeOutBack(Easing):
|
|
198
|
+
def __init__(self):
|
|
199
|
+
def func(x):
|
|
200
|
+
c1 = 1.70158
|
|
201
|
+
c3 = c1 + 1
|
|
202
|
+
|
|
203
|
+
return 1 + c3 * (x - 1)**3 + c1 * (x - 1)**2
|
|
204
|
+
super().__init__(func)
|
|
205
|
+
|
|
206
|
+
class easeInOutBack(Easing):
|
|
207
|
+
def __init__(self):
|
|
208
|
+
def func(x):
|
|
209
|
+
c1 = 1.70158
|
|
210
|
+
c2 = c1 * 1.525
|
|
211
|
+
|
|
212
|
+
return np.where(x < 0.5,
|
|
213
|
+
((2 * x)**2 * ((c2 + 1) * 2 * x - c2)) / 2,
|
|
214
|
+
((2 * x - 2)**2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2
|
|
215
|
+
)
|
|
216
|
+
super().__init__(func)
|
|
217
|
+
|
|
218
|
+
# ELASTIC
|
|
219
|
+
class easeInElastic(Easing):
|
|
220
|
+
def __init__(self):
|
|
221
|
+
def func(x):
|
|
222
|
+
c4 = (2 * np.pi) / 3
|
|
223
|
+
|
|
224
|
+
return np.where(x == 0,0,np.where(x == 1,1,
|
|
225
|
+
-2**(10 * x - 10) * np.sin((x * 10 - 10.75) * c4)
|
|
226
|
+
))
|
|
227
|
+
super().__init__(func)
|
|
228
|
+
|
|
229
|
+
class easeOutElastic(Easing):
|
|
230
|
+
def __init__(self):
|
|
231
|
+
def func(x):
|
|
232
|
+
c4 = (2 * np.pi) / 3
|
|
233
|
+
|
|
234
|
+
return np.where(x == 0,0,np.where(x == 1,1,
|
|
235
|
+
2**(-10 * x) * np.sin((x * 10 - 0.75) * c4) + 1
|
|
236
|
+
))
|
|
237
|
+
super().__init__(func)
|
|
238
|
+
|
|
239
|
+
class easeInOutElastic(Easing):
|
|
240
|
+
def __init__(self):
|
|
241
|
+
def func(x):
|
|
242
|
+
c5 = (2 * np.pi) / 4.5
|
|
243
|
+
|
|
244
|
+
return np.where(x == 0,0,np.where(x == 1,1,np.where(x<0.5,
|
|
245
|
+
-(2**(20 * x - 10) * np.sin((20 * x - 11.125) * c5)) / 2,
|
|
246
|
+
(2**(-20 * x + 10) * np.sin((20 * x - 11.125) * c5)) / 2 + 1
|
|
247
|
+
)))
|
|
248
|
+
super().__init__(func)
|
|
249
|
+
|
|
250
|
+
# BOUNCE
|
|
251
|
+
def outBounce(x):
|
|
252
|
+
n1 = 7.5625
|
|
253
|
+
d1 = 2.75
|
|
254
|
+
|
|
255
|
+
return np.where(x < 1 / d1,
|
|
256
|
+
n1 * x * x,
|
|
257
|
+
np.where(x < 2 / d1,
|
|
258
|
+
n1 * (x - 1.5 / d1) * (x - 1.5 / d1) + 0.75,
|
|
259
|
+
np.where(x < 2.5 / d1,
|
|
260
|
+
n1 * (x - 2.25 / d1) * (x - 2.25 / d1) + 0.9375,
|
|
261
|
+
n1 * (x - 2.625 / d1) * (x - 2.625 / d1) + 0.984375
|
|
262
|
+
)))
|
|
263
|
+
|
|
264
|
+
class easeOutBounce(Easing):
|
|
265
|
+
def __init__(self):
|
|
266
|
+
super().__init__(outBounce)
|
|
267
|
+
|
|
268
|
+
class easeInBounce(Easing):
|
|
269
|
+
def __init__(self):
|
|
270
|
+
def func(x):
|
|
271
|
+
x = 1-x
|
|
272
|
+
return 1-outBounce(x)
|
|
273
|
+
super().__init__(func)
|
|
274
|
+
|
|
275
|
+
class easeInOutBounce(Easing):
|
|
276
|
+
def __init__(self):
|
|
277
|
+
def func(x):
|
|
278
|
+
return np.where(x<0.5,(1 - outBounce(1 - 2 * x)) / 2,(1 + outBounce(2 * x - 1)) / 2)
|
|
279
|
+
super().__init__(func)
|
|
280
|
+
|
|
281
|
+
available_easings = []
|
|
282
|
+
for name, obj in inspect.getmembers(sys.modules[__name__]):
|
|
283
|
+
if inspect.isclass(obj) and name != 'Easing':
|
|
284
|
+
available_easings.append(name)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diplotocus
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A python package to generate animations of matplotlib objects.
|
|
5
|
+
Author-email: Tristan Boin <tristan.boin@obspm.fr>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Framework :: Matplotlib
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Requires-Dist: svgpath2mpl
|
|
16
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
diplotocus/__init__.py,sha256=57u1-7-LURrHkFfv_ZxrsxjD69j4AJFQ56GDR9Ii2mY,111
|
|
2
|
+
diplotocus/_version.py,sha256=rqyZtt1djmoU2vxYGslRgV4TaBWrxK8uesP_Xai_8-U,19
|
|
3
|
+
diplotocus/animations.py,sha256=Bnbc7O8MnSscYKNSxfwlyDCaQLI6CLQqj1v_Kq0NIpA,138717
|
|
4
|
+
diplotocus/core.py,sha256=ITDigZA78T4vOYz1pCP9Eksm2DvgKQ_FP_rKQy8Z0DM,14019
|
|
5
|
+
diplotocus/easings.py,sha256=zhwPItn_YnOczCX5czOQ3H47rJ0-qY3nsx3Gbgt8PNc,7798
|
|
6
|
+
diplotocus/GUI/GUI.html,sha256=r62SAu4Q_HM1vg04P0l1-rpWFulZ1DVypyO9uvfAA_k,7096
|
|
7
|
+
diplotocus/GUI/__init__.py,sha256=lKTP_zNmcPE__OcOD9QniK7h6nLZ7U93b9Izo6oxtc8,42
|
|
8
|
+
diplotocus/GUI/app.js,sha256=x_sZiaQCnKDe8IyN8RbU8qKUODDpi7EpgeXnmtidCec,50053
|
|
9
|
+
diplotocus/GUI/server.py,sha256=cHC1cbTW_wSnH4_42jL2W-OjptgE2MOJlZb2APn52SI,56039
|
|
10
|
+
diplotocus/GUI/style.css,sha256=rT__vQuB20q1PZywP8te0DV917H9AQHwJclBg4elqY0,10824
|
|
11
|
+
diplotocus/GUI/src/bg.png,sha256=ySX6slK8Ddlg7V16fOeAJxWuVT9i3_Dub-wsT5Y1ZqI,64165
|
|
12
|
+
diplotocus/GUI/src/favicon.ico,sha256=7sz7EfCWA-cba-_z_1jO7Ik2VY24K1VvubgEPli-1I4,11643
|
|
13
|
+
diplotocus/GUI/src/logo_bg.png,sha256=SrlSmKGQ8-O2quENjM16NFq2RjtYtbU7YH_c_bO27h8,33131
|
|
14
|
+
diplotocus/GUI/src/btn/btn_bg_Draw.png,sha256=PUG13hBQpFdd5mQsmB_wD6sigWC7HDi-npJVBwNIn34,34804
|
|
15
|
+
diplotocus/GUI/src/btn/btn_bg_Morph.png,sha256=QgLIpy7P4lidTVQQ32kivqndBgNU9cZPYNmgTixtxKo,35437
|
|
16
|
+
diplotocus/GUI/src/btn/btn_bg_Rotate.png,sha256=Sqji90eWk8oRHRjvm1a2qez07c0AuAL4sRMr6cWJows,35000
|
|
17
|
+
diplotocus/GUI/src/btn/btn_bg_Scale.png,sha256=VgykxIa4RJ_2bfISLU3rYvJ7yy95NQ91VMndpDe7JvU,35441
|
|
18
|
+
diplotocus/GUI/src/btn/btn_bg_Sequence.png,sha256=Z7yQytDrtwaNfHL5Sazt3AFIWHBE7mOQ2qYocg0oCOo,36230
|
|
19
|
+
diplotocus/GUI/src/btn/btn_bg_Translate.png,sha256=RVrUq2RwnVLWj0SzoE-NnZeLJHwfvkeoLfUdeWBFmm8,35179
|
|
20
|
+
diplotocus/GUI/src/btn/btn_bg_Tween.png,sha256=IjtpQqIQP8ePltwPJ2zqKBQooobI47Ojczi0PSSbSWo,34926
|
|
21
|
+
diplotocus/GUI/src/btn/btn_bg_default.png,sha256=QfrqN7N8c5TRgtBfU3Pw01tU5KfxyKihFXy5R9VrQsw,33705
|
|
22
|
+
diplotocus-0.0.0.dist-info/licenses/LICENSE,sha256=s8eQhVWRZLr1lA8akzj1JW1RiWTocDzEpUfKzSARYvU,1072
|
|
23
|
+
diplotocus-0.0.0.dist-info/METADATA,sha256=g9GeD3mWF2e5OpPQwUgT58s3AyHJC5KEUMQHhzDGjts,540
|
|
24
|
+
diplotocus-0.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
25
|
+
diplotocus-0.0.0.dist-info/top_level.txt,sha256=0nwg4ncIvxlo7OPlygJSE2zdUYFkV87w8a2-wRzhIhg,11
|
|
26
|
+
diplotocus-0.0.0.dist-info/RECORD,,
|