diplotocus 0.0.1__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/__init__.py +3 -0
- diplotocus/_version.py +1 -0
- diplotocus/animations.py +599 -0
- diplotocus/core.py +173 -0
- diplotocus/easings.py +17 -0
- diplotocus-0.0.1.dist-info/METADATA +14 -0
- diplotocus-0.0.1.dist-info/RECORD +10 -0
- diplotocus-0.0.1.dist-info/WHEEL +5 -0
- diplotocus-0.0.1.dist-info/licenses/LICENSE +21 -0
- diplotocus-0.0.1.dist-info/top_level.txt +1 -0
diplotocus/__init__.py
ADDED
diplotocus/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
diplotocus/animations.py
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
#----------------------------------------------
|
|
4
|
+
# ANIMATION CLASSES
|
|
5
|
+
#----------------------------------------------
|
|
6
|
+
|
|
7
|
+
class Animation:
|
|
8
|
+
def __init__(self,duration,delay=0,easing=None,axis=None,**kwargs):
|
|
9
|
+
self.anim_function = None
|
|
10
|
+
self.delay = delay
|
|
11
|
+
self.duration = duration
|
|
12
|
+
self.set_easing(easing)
|
|
13
|
+
self.compute_timings()
|
|
14
|
+
self.set_axis(axis)
|
|
15
|
+
self.anim_type = 'show'
|
|
16
|
+
self.obj = None
|
|
17
|
+
kwargs = self.clean_kwargs(kwargs)
|
|
18
|
+
self.kwargs = kwargs
|
|
19
|
+
self.anim_function = self.function
|
|
20
|
+
|
|
21
|
+
def set_axis(self,axis):
|
|
22
|
+
self.axis = axis
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def compute_timings(self):
|
|
26
|
+
self.x_min = self.delay
|
|
27
|
+
self.x_max = self.duration + self.delay
|
|
28
|
+
|
|
29
|
+
def set_duration(self,duration):
|
|
30
|
+
self.duration = duration
|
|
31
|
+
self.compute_timings()
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def set_delay(self,delay):
|
|
35
|
+
self.delay = delay
|
|
36
|
+
self.compute_timings()
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def set_easing(self,easing):
|
|
40
|
+
self.easing = easing
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def apply(self,x):
|
|
44
|
+
if x < self.x_min or x >= self.x_max:
|
|
45
|
+
return
|
|
46
|
+
if self.duration == 1:
|
|
47
|
+
self.anim_function(1)
|
|
48
|
+
return
|
|
49
|
+
t = self.easing.ease((x-self.delay)/(self.duration-1))
|
|
50
|
+
self.anim_function(t)
|
|
51
|
+
|
|
52
|
+
def initialize(self):
|
|
53
|
+
self.clean(0)
|
|
54
|
+
|
|
55
|
+
def show(self):
|
|
56
|
+
self.anim_type = 'show'
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def hide(self,delay=0,duration=None):
|
|
60
|
+
self.anim_type = 'hide'
|
|
61
|
+
self.set_delay(delay)
|
|
62
|
+
if duration is not None:
|
|
63
|
+
self.set_duration(duration)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def spawn(self):
|
|
67
|
+
self.anim_type = 'spawn'
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def despawn(self,delay=0,duration=None):
|
|
71
|
+
self.anim_type = 'despawn'
|
|
72
|
+
self.set_delay(delay)
|
|
73
|
+
if duration is not None:
|
|
74
|
+
self.set_duration(duration)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def grow(self):
|
|
78
|
+
self.anim_type = 'grow'
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def shrink(self,delay=0,duration=None):
|
|
82
|
+
self.anim_type = 'shrink'
|
|
83
|
+
self.set_delay(delay)
|
|
84
|
+
if duration is not None:
|
|
85
|
+
self.set_duration(duration)
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def update(self):
|
|
89
|
+
self.anim_type = 'update'
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def deupdate(self):
|
|
93
|
+
self.anim_type = 'deupdate'
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def morph(self,new_x,new_y,new_alpha=None):
|
|
97
|
+
self.new_x = new_x
|
|
98
|
+
self.new_y = new_y
|
|
99
|
+
self.new_alpha = new_alpha
|
|
100
|
+
self.anim_type = 'morph'
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def demorph(self):
|
|
104
|
+
self.anim_type = 'demorph'
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def reverse(self,delay=0,duration=None):
|
|
108
|
+
self.set_delay(delay)
|
|
109
|
+
if duration is not None:
|
|
110
|
+
self.set_duration(duration)
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def clean(self,x):
|
|
114
|
+
if x >= self.x_min-1 and x < self.x_max-1 and self.obj is not None:
|
|
115
|
+
if isinstance(self.obj,list):
|
|
116
|
+
for obj in self.obj:
|
|
117
|
+
obj.remove()
|
|
118
|
+
else:
|
|
119
|
+
self.obj.remove()
|
|
120
|
+
self.obj = None
|
|
121
|
+
|
|
122
|
+
def clean_kwargs(self,kwargs):
|
|
123
|
+
if 'duration' in kwargs:
|
|
124
|
+
del kwargs['duration']
|
|
125
|
+
if 'delay' in kwargs:
|
|
126
|
+
del kwargs['delay']
|
|
127
|
+
if 'easing' in kwargs:
|
|
128
|
+
del kwargs['delay']
|
|
129
|
+
if 'axis' in kwargs:
|
|
130
|
+
del kwargs['axis']
|
|
131
|
+
return kwargs
|
|
132
|
+
|
|
133
|
+
class zoom(Animation):
|
|
134
|
+
def __init__(self,zoom, *args, **kwargs):
|
|
135
|
+
super().__init__(*args, **kwargs)
|
|
136
|
+
self.zoom = 1/zoom
|
|
137
|
+
|
|
138
|
+
def reverse(self):
|
|
139
|
+
super().reverse()
|
|
140
|
+
self.zoom = 1/self.zoom
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def initialize(self):
|
|
144
|
+
self.width_init = self.axis.get_xlim()[1]-self.axis.get_xlim()[0]
|
|
145
|
+
self.height_init = self.axis.get_ylim()[1]-self.axis.get_ylim()[0]
|
|
146
|
+
self.width_end = self.width_init * self.zoom
|
|
147
|
+
self.height_end = self.height_init * self.zoom
|
|
148
|
+
|
|
149
|
+
def function(self,t):
|
|
150
|
+
center = ((self.axis.get_xlim()[1]+self.axis.get_xlim()[0])/2,(self.axis.get_ylim()[1]+self.axis.get_ylim()[0])/2)
|
|
151
|
+
width = self.width_init + (self.width_end - self.width_init)*t
|
|
152
|
+
height = self.height_init + (self.height_end - self.height_init)*t
|
|
153
|
+
x_range = (center[0] - width/2, center[0] + width/2)
|
|
154
|
+
y_range = (center[1] - height/2, center[1] + height/2)
|
|
155
|
+
self.axis.set_xlim(x_range[0],x_range[1])
|
|
156
|
+
self.axis.set_ylim(y_range[0],y_range[1])
|
|
157
|
+
|
|
158
|
+
class lims(Animation):
|
|
159
|
+
def __init__(self,xlim=None,ylim=None, *args, **kwargs):
|
|
160
|
+
if xlim is None and ylim is None:
|
|
161
|
+
raise ValueError('Both xlim and ylim cannot be empty.')
|
|
162
|
+
super().__init__(*args, **kwargs)
|
|
163
|
+
self.xlim = xlim
|
|
164
|
+
self.ylim = ylim
|
|
165
|
+
|
|
166
|
+
def initialize(self):
|
|
167
|
+
self.xlim_init = self.axis.get_xlim()
|
|
168
|
+
self.ylim_init = self.axis.get_ylim()
|
|
169
|
+
if self.xlim is not None:
|
|
170
|
+
self.xlim_end = self.xlim
|
|
171
|
+
else:
|
|
172
|
+
self.xlim_end = self.xlim_init
|
|
173
|
+
if self.ylim is not None:
|
|
174
|
+
self.ylim_end = self.ylim
|
|
175
|
+
else:
|
|
176
|
+
self.ylim_end = self.ylim_init
|
|
177
|
+
|
|
178
|
+
def function(self,t):
|
|
179
|
+
xlim_left = self.xlim_init[0] + (self.xlim_end[0] - self.xlim_init[0])*t
|
|
180
|
+
xlim_right = self.xlim_init[1] + (self.xlim_end[1] - self.xlim_init[1])*t
|
|
181
|
+
ylim_bottom = self.ylim_init[0] + (self.ylim_end[0] - self.ylim_init[0])*t
|
|
182
|
+
ylim_top = self.ylim_init[1] + (self.ylim_end[1] - self.ylim_init[1])*t
|
|
183
|
+
self.axis.set_xlim(xlim_left,xlim_right)
|
|
184
|
+
self.axis.set_ylim(ylim_bottom,ylim_top)
|
|
185
|
+
|
|
186
|
+
class move(Animation):
|
|
187
|
+
def __init__(self,pos, *args, **kwargs):
|
|
188
|
+
super().__init__(*args, **kwargs)
|
|
189
|
+
self.pos = pos
|
|
190
|
+
self.anim_function = self.function
|
|
191
|
+
|
|
192
|
+
def reverse(self):
|
|
193
|
+
super().reverse()
|
|
194
|
+
self.pos = self.pos_init
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
def initialize(self):
|
|
198
|
+
width = self.axis.get_xlim()[1]-self.axis.get_xlim()[0]
|
|
199
|
+
height = self.axis.get_ylim()[1]-self.axis.get_ylim()[0]
|
|
200
|
+
self.pos_init = (self.axis.get_xlim()[0]+width/2,self.axis.get_ylim()[0]+height/2)
|
|
201
|
+
self.pos_end = self.pos
|
|
202
|
+
|
|
203
|
+
def function(self,t):
|
|
204
|
+
width = self.axis.get_xlim()[1]-self.axis.get_xlim()[0]
|
|
205
|
+
height = self.axis.get_ylim()[1]-self.axis.get_ylim()[0]
|
|
206
|
+
pos_x = self.pos_init[0] + (self.pos_end[0] - self.pos_init[0])*t
|
|
207
|
+
pos_y = self.pos_init[1] + (self.pos_end[1] - self.pos_init[1])*t
|
|
208
|
+
self.axis.set_xlim(pos_x-width/2,pos_x+width/2)
|
|
209
|
+
self.axis.set_ylim(pos_y-height/2,pos_y+height/2)
|
|
210
|
+
|
|
211
|
+
class scatter(Animation):
|
|
212
|
+
def __init__(self,x,y, *args, **kwargs):
|
|
213
|
+
super().__init__(*args, **kwargs)
|
|
214
|
+
self.x = x
|
|
215
|
+
self.y = y
|
|
216
|
+
|
|
217
|
+
def function(self,t):
|
|
218
|
+
kwargs = self.kwargs.copy()
|
|
219
|
+
if 'alpha' not in kwargs:
|
|
220
|
+
kwargs['alpha'] = 1
|
|
221
|
+
if self.anim_type == 'show':
|
|
222
|
+
kwargs['alpha'] *= t
|
|
223
|
+
x = self.x
|
|
224
|
+
y = self.y
|
|
225
|
+
elif self.anim_type == 'hide':
|
|
226
|
+
kwargs['alpha'] *= 1-t
|
|
227
|
+
x = self.x
|
|
228
|
+
y = self.y
|
|
229
|
+
elif self.anim_type == 'spawn':
|
|
230
|
+
i_max = min(round(t*len(self.x)) + 1,len(self.x))
|
|
231
|
+
x = self.x[:i_max]
|
|
232
|
+
y = self.y[:i_max]
|
|
233
|
+
if 'c' in kwargs:
|
|
234
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
235
|
+
elif self.anim_type == 'despawn':
|
|
236
|
+
i_max = max(round((1-t)*len(self.x)),0)
|
|
237
|
+
x = self.x[:i_max]
|
|
238
|
+
y = self.y[:i_max]
|
|
239
|
+
if 'c' in kwargs:
|
|
240
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
241
|
+
elif self.anim_type == 'morph':
|
|
242
|
+
x = []
|
|
243
|
+
y = []
|
|
244
|
+
a = []
|
|
245
|
+
if 'alpha' not in kwargs:
|
|
246
|
+
kwargs['alpha'] = 1
|
|
247
|
+
for i in range(len(self.x)):
|
|
248
|
+
x.append(self.x[i] + (self.new_x[i] - self.x[i])*t)
|
|
249
|
+
y.append(self.y[i] + (self.new_y[i] - self.y[i])*t)
|
|
250
|
+
if self.new_alpha is not None:
|
|
251
|
+
a.append(kwargs['alpha'] + (self.new_alpha[i] - kwargs['alpha'])*t)
|
|
252
|
+
else:
|
|
253
|
+
a.append(kwargs['alpha'])
|
|
254
|
+
kwargs['alpha'] = a
|
|
255
|
+
elif self.anim_type == 'demorph':
|
|
256
|
+
x = []
|
|
257
|
+
y = []
|
|
258
|
+
a = []
|
|
259
|
+
if 'alpha' not in kwargs:
|
|
260
|
+
kwargs['alpha'] = 1
|
|
261
|
+
for i in range(len(self.x)):
|
|
262
|
+
x.append(self.x[i] + (self.new_x[i] - self.x[i])*(1-t))
|
|
263
|
+
y.append(self.y[i] + (self.new_y[i] - self.y[i])*(1-t))
|
|
264
|
+
if self.new_alpha is not None:
|
|
265
|
+
a.append(kwargs['alpha'] + (self.new_alpha[i] - kwargs['alpha'])*t)
|
|
266
|
+
else:
|
|
267
|
+
a.append(kwargs['alpha'])
|
|
268
|
+
kwargs['alpha'] = a
|
|
269
|
+
|
|
270
|
+
self.obj = self.axis.scatter(x,y,**kwargs)
|
|
271
|
+
|
|
272
|
+
class plot(Animation):
|
|
273
|
+
def __init__(self,x,y, *args, **kwargs):
|
|
274
|
+
super().__init__(*args, **kwargs)
|
|
275
|
+
self.new_x = None
|
|
276
|
+
self.new_y = None
|
|
277
|
+
self.x = x
|
|
278
|
+
self.y = y
|
|
279
|
+
|
|
280
|
+
def clean(self,x):
|
|
281
|
+
super().clean(x)
|
|
282
|
+
if x == self.x_max-1 and self.new_x is not None:
|
|
283
|
+
self.x = self.new_x
|
|
284
|
+
self.y = self.new_y
|
|
285
|
+
self.new_x = None
|
|
286
|
+
self.new_y = None
|
|
287
|
+
|
|
288
|
+
def function(self,t):
|
|
289
|
+
kwargs = self.kwargs.copy()
|
|
290
|
+
if 'alpha' not in kwargs:
|
|
291
|
+
kwargs['alpha'] = 1
|
|
292
|
+
if self.anim_type == 'show':
|
|
293
|
+
kwargs['alpha'] *= t
|
|
294
|
+
x = self.x
|
|
295
|
+
y = self.y
|
|
296
|
+
elif self.anim_type == 'hide':
|
|
297
|
+
kwargs['alpha'] *= 1-t
|
|
298
|
+
x = self.x
|
|
299
|
+
y = self.y
|
|
300
|
+
elif self.anim_type == 'spawn':
|
|
301
|
+
i_max = min(round(t*len(self.x)) + 1,len(self.x))
|
|
302
|
+
x = self.x[:i_max]
|
|
303
|
+
y = self.y[:i_max]
|
|
304
|
+
if 'c' in kwargs:
|
|
305
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
306
|
+
elif self.anim_type == 'despawn':
|
|
307
|
+
i_max = max(round((1-t)*len(self.x)),0)
|
|
308
|
+
x = self.x[:i_max]
|
|
309
|
+
y = self.y[:i_max]
|
|
310
|
+
if 'c' in kwargs:
|
|
311
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
312
|
+
elif self.anim_type == 'morph':
|
|
313
|
+
x = []
|
|
314
|
+
y = []
|
|
315
|
+
for i in range(len(self.x)):
|
|
316
|
+
x.append(self.x[i] + (self.new_x[i] - self.x[i])*t)
|
|
317
|
+
y.append(self.y[i] + (self.new_y[i] - self.y[i])*t)
|
|
318
|
+
elif self.anim_type == 'demorph':
|
|
319
|
+
x = []
|
|
320
|
+
y = []
|
|
321
|
+
for i in range(len(self.x)):
|
|
322
|
+
x.append(self.x[i] + (self.new_x[i] - self.x[i])*(1-t))
|
|
323
|
+
y.append(self.y[i] + (self.new_y[i] - self.y[i])*(1-t))
|
|
324
|
+
|
|
325
|
+
self.obj = self.axis.plot(x,y,**kwargs)
|
|
326
|
+
|
|
327
|
+
class fill_between(Animation):
|
|
328
|
+
def __init__(self,x,y1,y2, *args, **kwargs):
|
|
329
|
+
super().__init__(*args, **kwargs)
|
|
330
|
+
self.x = np.array(x)
|
|
331
|
+
self.y1 = np.array(y1).reshape(-1)
|
|
332
|
+
self.y2 = np.array(y2).reshape(-1)
|
|
333
|
+
if self.y1.size != self.x.size:
|
|
334
|
+
self.y1 = np.ones_like(self.x)*self.y1[0]
|
|
335
|
+
if self.y2.size != self.x.size:
|
|
336
|
+
self.y2 = np.ones_like(self.x)*self.y2[0]
|
|
337
|
+
|
|
338
|
+
def function(self,t):
|
|
339
|
+
kwargs = self.kwargs.copy()
|
|
340
|
+
if 'alpha' not in kwargs:
|
|
341
|
+
kwargs['alpha'] = 1
|
|
342
|
+
if self.anim_type == 'show':
|
|
343
|
+
kwargs['alpha'] *= t
|
|
344
|
+
x = self.x
|
|
345
|
+
y1 = self.y1
|
|
346
|
+
y2 = self.y2
|
|
347
|
+
elif self.anim_type == 'hide':
|
|
348
|
+
kwargs['alpha'] *= 1-t
|
|
349
|
+
x = self.x
|
|
350
|
+
y1 = self.y1
|
|
351
|
+
y2 = self.y2
|
|
352
|
+
elif self.anim_type == 'spawn':
|
|
353
|
+
i_max = min(round(t*len(self.x)) + 1,len(self.x))
|
|
354
|
+
x = self.x[:i_max]
|
|
355
|
+
y1 = self.y1[:i_max]
|
|
356
|
+
y2 = self.y2[:i_max]
|
|
357
|
+
if 'c' in kwargs:
|
|
358
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
359
|
+
elif self.anim_type == 'despawn':
|
|
360
|
+
i_max = max(round((1-t)*len(self.x)),0)
|
|
361
|
+
x = self.x[:i_max]
|
|
362
|
+
y1 = self.y1[:i_max]
|
|
363
|
+
y2 = self.y2[:i_max]
|
|
364
|
+
if 'c' in kwargs:
|
|
365
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
366
|
+
elif self.anim_type == 'grow':
|
|
367
|
+
widths = self.y2 - self.y1
|
|
368
|
+
centers = (self.y2 + self.y1)/2
|
|
369
|
+
x = self.x
|
|
370
|
+
y1 = centers - t*widths/2
|
|
371
|
+
y2 = centers + t*widths/2
|
|
372
|
+
elif self.anim_type == 'shrink':
|
|
373
|
+
widths = self.y2 - self.y1
|
|
374
|
+
centers = (self.y2 + self.y1)/2
|
|
375
|
+
x = self.x
|
|
376
|
+
y1 = centers - (1-t)*widths/2
|
|
377
|
+
y2 = centers + (1-t)*widths/2
|
|
378
|
+
|
|
379
|
+
self.obj = self.axis.fill_between(x=x,y1=y1,y2=y2,**kwargs)
|
|
380
|
+
|
|
381
|
+
class fill_betweenx(Animation):
|
|
382
|
+
def __init__(self,y,x1,x2, *args, **kwargs):
|
|
383
|
+
super().__init__(*args, **kwargs)
|
|
384
|
+
self.y = np.array(y)
|
|
385
|
+
self.x1 = np.array(x1).reshape(-1)
|
|
386
|
+
self.x2 = np.array(x2).reshape(-1)
|
|
387
|
+
if self.x1.size != self.y.size:
|
|
388
|
+
self.x1 = np.ones_like(self.y[0])*self.x1[0]
|
|
389
|
+
if self.x2.size != self.y.size:
|
|
390
|
+
self.x2 = np.ones_like(self.y[0])*self.x2[0]
|
|
391
|
+
|
|
392
|
+
def function(self,t):
|
|
393
|
+
kwargs = self.kwargs.copy()
|
|
394
|
+
if 'alpha' not in kwargs:
|
|
395
|
+
kwargs['alpha'] = 1
|
|
396
|
+
if self.anim_type == 'show':
|
|
397
|
+
kwargs['alpha'] *= t
|
|
398
|
+
y = self.y
|
|
399
|
+
x1 = self.x1
|
|
400
|
+
x2 = self.x2
|
|
401
|
+
elif self.anim_type == 'hide':
|
|
402
|
+
kwargs['alpha'] *= 1-t
|
|
403
|
+
y = self.y
|
|
404
|
+
x1 = self.x1
|
|
405
|
+
x2 = self.x2
|
|
406
|
+
elif self.anim_type == 'spawn':
|
|
407
|
+
i_max = min(round(t*len(self.y)) + 1,len(self.y))
|
|
408
|
+
y = self.y[:i_max]
|
|
409
|
+
x1 = self.x1[:i_max]
|
|
410
|
+
x2 = self.x2[:i_max]
|
|
411
|
+
if 'c' in kwargs:
|
|
412
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
413
|
+
elif self.anim_type == 'despawn':
|
|
414
|
+
i_max = max(round((1-t)*len(self.y)),0)
|
|
415
|
+
y = self.y[:i_max]
|
|
416
|
+
x1 = self.x1[:i_max]
|
|
417
|
+
x2 = self.x2[:i_max]
|
|
418
|
+
if 'c' in kwargs:
|
|
419
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
420
|
+
elif self.anim_type == 'grow':
|
|
421
|
+
widths = self.x2 - self.x1
|
|
422
|
+
centers = (self.x2 + self.x1)/2
|
|
423
|
+
y = self.y
|
|
424
|
+
x1 = centers - t*widths/2
|
|
425
|
+
x2 = centers + t*widths/2
|
|
426
|
+
elif self.anim_type == 'shrink':
|
|
427
|
+
widths = self.x2 - self.x1
|
|
428
|
+
centers = (self.x2 + self.x1)/2
|
|
429
|
+
y = self.y
|
|
430
|
+
x1 = centers - (1-t)*widths/2
|
|
431
|
+
x2 = centers + (1-t)*widths/2
|
|
432
|
+
|
|
433
|
+
self.obj = self.axis.fill_betweenx(y=y,x1=x1,x2=x2,**kwargs)
|
|
434
|
+
|
|
435
|
+
class axvline(Animation):
|
|
436
|
+
def __init__(self,x, *args, **kwargs):
|
|
437
|
+
super().__init__(*args, **kwargs)
|
|
438
|
+
self.x = x
|
|
439
|
+
|
|
440
|
+
def function(self,t):
|
|
441
|
+
kwargs = self.kwargs.copy()
|
|
442
|
+
if 'alpha' not in kwargs:
|
|
443
|
+
kwargs['alpha'] = 1
|
|
444
|
+
x = self.x
|
|
445
|
+
y_max = 1
|
|
446
|
+
if self.anim_type == 'show':
|
|
447
|
+
kwargs['alpha'] *= t
|
|
448
|
+
elif self.anim_type == 'hide':
|
|
449
|
+
kwargs['alpha'] *= 1-t
|
|
450
|
+
elif self.anim_type == 'spawn':
|
|
451
|
+
y_max = t
|
|
452
|
+
elif self.anim_type == 'despawn':
|
|
453
|
+
y_max = (1-t)
|
|
454
|
+
|
|
455
|
+
self.obj = self.axis.axvline(x,ymin=0,ymax=y_max,**kwargs)
|
|
456
|
+
|
|
457
|
+
class errorbar(Animation):
|
|
458
|
+
def __init__(self,x,y,xerr=None,yerr=None, *args, **kwargs):
|
|
459
|
+
super().__init__(*args, **kwargs)
|
|
460
|
+
self.x = x
|
|
461
|
+
self.y = y
|
|
462
|
+
if xerr is None:
|
|
463
|
+
xerr = np.zeros_like(x)
|
|
464
|
+
if yerr is None:
|
|
465
|
+
yerr = np.zeros_like(y)
|
|
466
|
+
self.xerr = xerr
|
|
467
|
+
self.yerr = yerr
|
|
468
|
+
|
|
469
|
+
def function(self,t):
|
|
470
|
+
kwargs = self.kwargs.copy()
|
|
471
|
+
if 'alpha' not in kwargs:
|
|
472
|
+
kwargs['alpha'] = 1
|
|
473
|
+
if self.anim_type == 'show':
|
|
474
|
+
kwargs['alpha'] *= t
|
|
475
|
+
x = self.x
|
|
476
|
+
y = self.y
|
|
477
|
+
xerr = self.xerr
|
|
478
|
+
yerr = self.yerr
|
|
479
|
+
elif self.anim_type == 'hide':
|
|
480
|
+
kwargs['alpha'] *= 1-t
|
|
481
|
+
x = self.x
|
|
482
|
+
y = self.y
|
|
483
|
+
xerr = self.xerr
|
|
484
|
+
yerr = self.yerr
|
|
485
|
+
elif self.anim_type == 'spawn':
|
|
486
|
+
i_max = min(round(t*len(self.x)) + 1,len(self.x))
|
|
487
|
+
x = self.x[:i_max]
|
|
488
|
+
y = self.y[:i_max]
|
|
489
|
+
xerr = self.xerr[:i_max]
|
|
490
|
+
yerr = self.yerr[:i_max]
|
|
491
|
+
if 'c' in kwargs:
|
|
492
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
493
|
+
elif self.anim_type == 'despawn':
|
|
494
|
+
i_max = max(round((1-t)*len(self.x)),0)
|
|
495
|
+
x = self.x[:i_max]
|
|
496
|
+
y = self.y[:i_max]
|
|
497
|
+
xerr = self.xerr[:i_max]
|
|
498
|
+
yerr = self.yerr[:i_max]
|
|
499
|
+
if 'c' in kwargs:
|
|
500
|
+
kwargs['c'] = kwargs['c'][:i_max]
|
|
501
|
+
|
|
502
|
+
self.obj = self.axis.errorbar(x,y,xerr=xerr,yerr=yerr,**kwargs)
|
|
503
|
+
|
|
504
|
+
class hist(Animation):
|
|
505
|
+
def __init__(self,x,bins=None,range=None, *args, **kwargs):
|
|
506
|
+
super().__init__(*args, **kwargs)
|
|
507
|
+
self.x = x
|
|
508
|
+
self.bins = bins
|
|
509
|
+
self.range = range
|
|
510
|
+
|
|
511
|
+
def function(self,t):
|
|
512
|
+
kwargs = self.kwargs.copy()
|
|
513
|
+
if self.anim_type == 'show':
|
|
514
|
+
kwargs['alpha'] *= t
|
|
515
|
+
if self.anim_type == 'hide':
|
|
516
|
+
kwargs['alpha'] *= 1-t
|
|
517
|
+
if self.anim_type == 'spawn':
|
|
518
|
+
i_max = min(round(t*len(self.x)) + 1,len(self.x))
|
|
519
|
+
elif self.anim_type == 'despawn':
|
|
520
|
+
i_max = max(round((1-t)*len(self.x)),0)
|
|
521
|
+
x = self.x[:i_max]
|
|
522
|
+
h,edges,self.obj = self.axis.hist(x,bins=self.bins,range=self.range,**kwargs)
|
|
523
|
+
|
|
524
|
+
class hist2d(Animation):
|
|
525
|
+
def __init__(self,x,y,bins=None,range=None, *args, **kwargs):
|
|
526
|
+
super().__init__(*args, **kwargs)
|
|
527
|
+
self.x = x
|
|
528
|
+
self.y = y
|
|
529
|
+
self.bins = bins
|
|
530
|
+
self.range = range
|
|
531
|
+
|
|
532
|
+
def function(self,t):
|
|
533
|
+
kwargs = self.kwargs.copy()
|
|
534
|
+
if 'alpha' not in kwargs:
|
|
535
|
+
kwargs['alpha'] = 1
|
|
536
|
+
i_max = len(self.x)
|
|
537
|
+
if self.anim_type == 'show':
|
|
538
|
+
kwargs['alpha'] *= t
|
|
539
|
+
if self.anim_type == 'hide':
|
|
540
|
+
kwargs['alpha'] *= 1-t
|
|
541
|
+
elif self.anim_type == 'spawn':
|
|
542
|
+
i_max = min(round(t*len(self.x)) + 1,len(self.x))
|
|
543
|
+
elif self.anim_type == 'despawn':
|
|
544
|
+
i_max = max(round((1-t)*len(self.x)),0)
|
|
545
|
+
x = self.x[:i_max]
|
|
546
|
+
y = self.y[:i_max]
|
|
547
|
+
old_xlim = self.axis.get_xlim()
|
|
548
|
+
old_ylim = self.axis.get_ylim()
|
|
549
|
+
h,xedges,yedges,self.obj = self.axis.hist2d(x,y,bins=self.bins,range=self.range,**kwargs)
|
|
550
|
+
self.axis.set_xlim(old_xlim)
|
|
551
|
+
self.axis.set_ylim(old_ylim)
|
|
552
|
+
|
|
553
|
+
class contourf(Animation):
|
|
554
|
+
def __init__(self,z,levels=None, *args, **kwargs):
|
|
555
|
+
super().__init__(*args, **kwargs)
|
|
556
|
+
self.z = z
|
|
557
|
+
self.levels = levels
|
|
558
|
+
|
|
559
|
+
def function(self,t):
|
|
560
|
+
kwargs = self.kwargs.copy()
|
|
561
|
+
if 'alpha' not in kwargs:
|
|
562
|
+
kwargs['alpha'] = 1
|
|
563
|
+
if self.anim_type == 'show':
|
|
564
|
+
kwargs['alpha'] *= t
|
|
565
|
+
elif self.anim_type == 'hide':
|
|
566
|
+
kwargs['alpha'] *= (1-t)
|
|
567
|
+
old_xlim = self.axis.get_xlim()
|
|
568
|
+
old_ylim = self.axis.get_ylim()
|
|
569
|
+
self.obj = self.axis.contourf(self.z,levels=self.levels,**kwargs)
|
|
570
|
+
self.axis.set_xlim(old_xlim)
|
|
571
|
+
self.axis.set_ylim(old_ylim)
|
|
572
|
+
|
|
573
|
+
class text(Animation):
|
|
574
|
+
def __init__(self,x,y,string, *args, **kwargs):
|
|
575
|
+
super().__init__(*args, **kwargs)
|
|
576
|
+
self.x = x
|
|
577
|
+
self.y = y
|
|
578
|
+
self.string = string
|
|
579
|
+
if isinstance(self.string,str):
|
|
580
|
+
self.string = np.array(self.string).reshape(-1)
|
|
581
|
+
|
|
582
|
+
def function(self,t):
|
|
583
|
+
kwargs = self.kwargs.copy()
|
|
584
|
+
if 'alpha' not in kwargs:
|
|
585
|
+
kwargs['alpha'] = 1
|
|
586
|
+
if self.anim_type == 'show':
|
|
587
|
+
kwargs['alpha'] *= t
|
|
588
|
+
s = self.string[0]
|
|
589
|
+
elif self.anim_type == 'hide':
|
|
590
|
+
kwargs['alpha'] *= 1-t
|
|
591
|
+
s = self.string[0]
|
|
592
|
+
elif self.anim_type == 'update':
|
|
593
|
+
i = min(round(t*len(self.string)) + 1,len(self.string))
|
|
594
|
+
s = self.string[i]
|
|
595
|
+
elif self.anim_type == 'deupdate':
|
|
596
|
+
i = max(round((1-t)*len(self.string)),0)
|
|
597
|
+
s = self.string[i]
|
|
598
|
+
|
|
599
|
+
self.obj = self.axis.text(x=self.x,y=self.y,s=s,**kwargs)
|
diplotocus/core.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import subprocess,os,shlex,io,pickle
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from .easings import *
|
|
7
|
+
from .animations import *
|
|
8
|
+
|
|
9
|
+
def in_notebook():
|
|
10
|
+
try:
|
|
11
|
+
from IPython import get_ipython
|
|
12
|
+
shell = get_ipython().__class__.__name__
|
|
13
|
+
return shell == "ZMQInteractiveShell"
|
|
14
|
+
except Exception:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
if in_notebook():
|
|
18
|
+
from tqdm.notebook import tqdm
|
|
19
|
+
else:
|
|
20
|
+
from tqdm import tqdm
|
|
21
|
+
|
|
22
|
+
def status_message(start_msg, end_msg):
|
|
23
|
+
if in_notebook():
|
|
24
|
+
from IPython.display import display, update_display, Markdown
|
|
25
|
+
display_id = display(Markdown(start_msg), display_id=True)
|
|
26
|
+
def update():
|
|
27
|
+
update_display(Markdown(end_msg), display_id=display_id.display_id)
|
|
28
|
+
return update
|
|
29
|
+
else:
|
|
30
|
+
print(start_msg, end="\r", flush=True)
|
|
31
|
+
def update():
|
|
32
|
+
print(end_msg + " " * max(0, len(start_msg) - len(end_msg)))
|
|
33
|
+
return update
|
|
34
|
+
|
|
35
|
+
#----------------------------------------------
|
|
36
|
+
# SEQUENCE CLASS
|
|
37
|
+
#----------------------------------------------
|
|
38
|
+
|
|
39
|
+
class Sequence:
|
|
40
|
+
def __init__(self,fig,name,tqdm=True,dpi=200,easing=easeLinear()):
|
|
41
|
+
#buf = io.BytesIO()
|
|
42
|
+
#pickle.dump(fig, buf)
|
|
43
|
+
#buf.seek(0)
|
|
44
|
+
#self.fig = pickle.load(buf)
|
|
45
|
+
self.fig = fig
|
|
46
|
+
self.ax = self.fig.get_axes()
|
|
47
|
+
if len(self.ax) == 1:
|
|
48
|
+
self.ax = self.ax[0]
|
|
49
|
+
else:
|
|
50
|
+
self.ax = np.array(self.ax)
|
|
51
|
+
if isinstance(self.ax,np.ndarray):
|
|
52
|
+
if isinstance(self.ax[0],np.ndarray):
|
|
53
|
+
self.main_axis = self.ax[0,0]
|
|
54
|
+
else:
|
|
55
|
+
self.main_axis = self.ax[0]
|
|
56
|
+
else:
|
|
57
|
+
self.main_axis = self.ax
|
|
58
|
+
self.name = name
|
|
59
|
+
self.tqdm = tqdm
|
|
60
|
+
self.dpi = dpi
|
|
61
|
+
self.easing = easing
|
|
62
|
+
self.x = 0
|
|
63
|
+
self.sequence_str = ''
|
|
64
|
+
self.set_white()
|
|
65
|
+
|
|
66
|
+
def set_white(self):
|
|
67
|
+
if isinstance(self.ax,np.ndarray) == False:
|
|
68
|
+
axes = (self.ax,)
|
|
69
|
+
else:
|
|
70
|
+
axes = self.ax.flatten()
|
|
71
|
+
for axis in axes:
|
|
72
|
+
axis.spines['bottom'].set_color('white')
|
|
73
|
+
axis.spines['top'].set_color('white')
|
|
74
|
+
axis.spines['left'].set_color('white')
|
|
75
|
+
axis.spines['right'].set_color('white')
|
|
76
|
+
axis.xaxis.label.set_color('white')
|
|
77
|
+
axis.yaxis.label.set_color('white')
|
|
78
|
+
axis.tick_params(axis='both',colors='white')
|
|
79
|
+
|
|
80
|
+
def plot(self,animations,easing=None,debug=False):
|
|
81
|
+
if isinstance(animations,Animation):
|
|
82
|
+
animations = (animations,)
|
|
83
|
+
for animation in animations:
|
|
84
|
+
if animation.axis is None:
|
|
85
|
+
animation.set_axis(self.main_axis)
|
|
86
|
+
animation.initialize()
|
|
87
|
+
if animation.easing == None:
|
|
88
|
+
if easing is None:
|
|
89
|
+
animation.easing = self.easing
|
|
90
|
+
else:
|
|
91
|
+
animation.easing = easing
|
|
92
|
+
animation.apply(animation.x_max-1)
|
|
93
|
+
self.save_plot(debug)
|
|
94
|
+
|
|
95
|
+
def animate(self,animations,easing=None,debug=False):
|
|
96
|
+
if isinstance(animations,Animation):
|
|
97
|
+
animations = (animations,)
|
|
98
|
+
x_max = 0
|
|
99
|
+
for animation in animations:
|
|
100
|
+
if animation.axis is None:
|
|
101
|
+
animation.set_axis(self.main_axis)
|
|
102
|
+
animation.initialize()
|
|
103
|
+
if animation.easing == None:
|
|
104
|
+
if easing is None:
|
|
105
|
+
animation.easing = self.easing
|
|
106
|
+
else:
|
|
107
|
+
animation.easing = easing
|
|
108
|
+
|
|
109
|
+
if animation.x_max > x_max:
|
|
110
|
+
x_max = animation.x_max
|
|
111
|
+
|
|
112
|
+
loop = range(x_max)
|
|
113
|
+
if self.tqdm:
|
|
114
|
+
loop = tqdm(loop)
|
|
115
|
+
|
|
116
|
+
for x in loop:
|
|
117
|
+
for animation in animations:
|
|
118
|
+
animation.apply(x)
|
|
119
|
+
self.save_plot(debug)
|
|
120
|
+
for animation in animations:
|
|
121
|
+
animation.clean(x)
|
|
122
|
+
|
|
123
|
+
def wait(self,duration):
|
|
124
|
+
#Have to do in two steps, because doesn't work in 1 if wait is the last animation of the sequence
|
|
125
|
+
self.sequence_str += 'file \'' + self.name + '_{}.png\'\n'.format(self.x - 1)
|
|
126
|
+
self.sequence_str += 'duration {}\n'.format((duration-1)/30)
|
|
127
|
+
self.sequence_str += 'file \'' + self.name + '_{}.png\'\n'.format(self.x - 1)
|
|
128
|
+
self.sequence_str += 'duration {}\n'.format(1/30)
|
|
129
|
+
self.x += 1
|
|
130
|
+
|
|
131
|
+
def save_plot(self,debug):
|
|
132
|
+
if debug is False:
|
|
133
|
+
if os.path.isdir(self.name) == False:
|
|
134
|
+
os.makedirs(self.name)
|
|
135
|
+
fn = self.name + '/' + self.name + '_{}.png'.format(self.x)
|
|
136
|
+
self.fig.savefig(fn,dpi=self.dpi,transparent=True)
|
|
137
|
+
|
|
138
|
+
self.sequence_str += 'file \'' + self.name + '_{}.png\'\n'.format(self.x)
|
|
139
|
+
self.sequence_str += 'duration {}\n'.format(1/30)
|
|
140
|
+
self.x += 1
|
|
141
|
+
|
|
142
|
+
def save_video(self,speed=1,multialpha=False,prerendered=False,clean=True):
|
|
143
|
+
video_fn = self.name + '.mov'
|
|
144
|
+
update = status_message("Rendering video...", "Saved video " + video_fn)
|
|
145
|
+
if not prerendered:
|
|
146
|
+
self.sequence_str += 'file \'' + self.name + '_{}.png\'\n'.format(self.x-1)#repeat last frame otherwise not seen for some reason
|
|
147
|
+
self.sequence_str += 'duration {}\n'.format(1/30)
|
|
148
|
+
with open(self.name + '/' + self.name + '.txt','w+') as f:
|
|
149
|
+
lines = self.sequence_str.split('\n')
|
|
150
|
+
for line in lines:
|
|
151
|
+
if speed != 1:
|
|
152
|
+
if 'duration' in line:
|
|
153
|
+
line = 'duration {}'.format(float(line.split(' ')[-1])/speed)
|
|
154
|
+
f.write(line + '\n')
|
|
155
|
+
sequence_fn = self.name + '/' + self.name + '.txt'
|
|
156
|
+
|
|
157
|
+
if multialpha:
|
|
158
|
+
command = 'ffmpeg -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)
|
|
159
|
+
else:
|
|
160
|
+
command = 'ffmpeg -f concat -safe 0 -i {} -y -c:v prores -pix_fmt yuva444p10le -hide_banner -loglevel error {}'.format(sequence_fn,video_fn)
|
|
161
|
+
subprocess.run(shlex.split(command))
|
|
162
|
+
if clean:
|
|
163
|
+
shutil.rmtree(self.name)
|
|
164
|
+
plt.close()
|
|
165
|
+
update()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#TODO : implementing blitting as in https://matplotlib.org/stable/users/explain/animations/blitting.html
|
|
169
|
+
#has to be an option on the sequence Class, as it can only draw objects on top of the saved background.
|
|
170
|
+
|
|
171
|
+
#TODO : implement deepcopy of fig so that we can rerun the same code of Sequence without having to reinitialize the fig, because it changes in the sequence code
|
|
172
|
+
#code implemented in the init of sequence, but the axes are regenerated from the copied figure, so if some anims use a specific axis, it does not exist anymore.
|
|
173
|
+
#have to, as initialisation, replace the specific axis with the new one (save old axis in Sequence, find index of specific axis in old axes, get new axis using index)
|
diplotocus/easings.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#----------------------------------------------
|
|
2
|
+
# EASING CLASSES
|
|
3
|
+
#----------------------------------------------
|
|
4
|
+
class Easing:
|
|
5
|
+
def __init__(self,func):
|
|
6
|
+
self.func = func
|
|
7
|
+
|
|
8
|
+
def ease(self,x):
|
|
9
|
+
return self.func(x)
|
|
10
|
+
|
|
11
|
+
class easeInOutQuart(Easing):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__(lambda x:8*x**4 if x < 0.5 else 1 - (-2*x + 2)**4 / 2)
|
|
14
|
+
|
|
15
|
+
class easeLinear(Easing):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__(lambda x:x)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diplotocus
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A python package to generate animations using pyplot.
|
|
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: Programming Language :: Python :: 3
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
Diplotocus is a package to render animations of matplotlib plots, in a sequential timeline style.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
diplotocus/__init__.py,sha256=bODPCi7-gbI4nGoIUKVJ7WclnrncvNhAi7wIfZoaw4I,90
|
|
2
|
+
diplotocus/_version.py,sha256=ugyuFliEqtAwQmH4sTlc16YXKYbFWDmfyk87fErB8-8,21
|
|
3
|
+
diplotocus/animations.py,sha256=EbT8wd8tgY8CgRKLkQj3s0xfeCbfWcW4pxYDoZR9CTc,20137
|
|
4
|
+
diplotocus/core.py,sha256=lUMA3PXlSxCWWKbnYOMJYM9gEjGJdx9M4xcnDnC8FqQ,6909
|
|
5
|
+
diplotocus/easings.py,sha256=XqzW3jSQqczuDUVwVQ8HPOr7z_mkvveWnIp6QOn3Trw,454
|
|
6
|
+
diplotocus-0.0.1.dist-info/licenses/LICENSE,sha256=s8eQhVWRZLr1lA8akzj1JW1RiWTocDzEpUfKzSARYvU,1072
|
|
7
|
+
diplotocus-0.0.1.dist-info/METADATA,sha256=pF1ZpkmFPhZANlGoDBtE984-O5w707MLENQ1amqrYuI,520
|
|
8
|
+
diplotocus-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
diplotocus-0.0.1.dist-info/top_level.txt,sha256=0nwg4ncIvxlo7OPlygJSE2zdUYFkV87w8a2-wRzhIhg,11
|
|
10
|
+
diplotocus-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026] [Tristan Boin]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
diplotocus
|