manim 0.18.1__py3-none-any.whl → 0.19.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.
Potentially problematic release.
This version of manim might be problematic. Click here for more details.
- manim/__main__.py +45 -12
- manim/_config/__init__.py +2 -2
- manim/_config/cli_colors.py +8 -4
- manim/_config/default.cfg +0 -2
- manim/_config/logger_utils.py +5 -0
- manim/_config/utils.py +29 -38
- manim/animation/animation.py +148 -8
- manim/animation/composition.py +16 -13
- manim/animation/creation.py +184 -8
- manim/animation/fading.py +5 -8
- manim/animation/indication.py +93 -26
- manim/animation/movement.py +21 -3
- manim/animation/rotation.py +2 -1
- manim/animation/specialized.py +3 -5
- manim/animation/speedmodifier.py +3 -3
- manim/animation/transform.py +4 -5
- manim/animation/updaters/mobject_update_utils.py +17 -14
- manim/camera/camera.py +2 -2
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +52 -36
- manim/cli/checkhealth/checks.py +92 -76
- manim/cli/checkhealth/commands.py +12 -5
- manim/cli/default_group.py +148 -24
- manim/cli/init/commands.py +28 -23
- manim/cli/plugins/commands.py +13 -3
- manim/cli/render/commands.py +47 -42
- manim/cli/render/global_options.py +43 -9
- manim/cli/render/render_options.py +84 -19
- manim/constants.py +11 -4
- manim/mobject/frame.py +0 -1
- manim/mobject/geometry/arc.py +109 -75
- manim/mobject/geometry/boolean_ops.py +20 -17
- manim/mobject/geometry/labeled.py +300 -77
- manim/mobject/geometry/line.py +120 -60
- manim/mobject/geometry/polygram.py +109 -25
- manim/mobject/geometry/shape_matchers.py +35 -15
- manim/mobject/geometry/tips.py +36 -27
- manim/mobject/graph.py +48 -40
- manim/mobject/graphing/coordinate_systems.py +110 -45
- manim/mobject/graphing/functions.py +16 -10
- manim/mobject/graphing/number_line.py +23 -9
- manim/mobject/graphing/probability.py +2 -10
- manim/mobject/graphing/scale.py +6 -5
- manim/mobject/matrix.py +17 -19
- manim/mobject/mobject.py +149 -103
- manim/mobject/opengl/opengl_geometry.py +4 -8
- manim/mobject/opengl/opengl_mobject.py +506 -343
- manim/mobject/opengl/opengl_point_cloud_mobject.py +3 -7
- manim/mobject/opengl/opengl_surface.py +1 -2
- manim/mobject/opengl/opengl_vectorized_mobject.py +27 -65
- manim/mobject/svg/brace.py +61 -13
- manim/mobject/svg/svg_mobject.py +2 -1
- manim/mobject/table.py +11 -12
- manim/mobject/text/code_mobject.py +186 -550
- manim/mobject/text/numbers.py +7 -7
- manim/mobject/text/tex_mobject.py +22 -13
- manim/mobject/text/text_mobject.py +29 -20
- manim/mobject/three_d/polyhedra.py +98 -1
- manim/mobject/three_d/three_dimensions.py +59 -31
- manim/mobject/types/image_mobject.py +37 -23
- manim/mobject/types/point_cloud_mobject.py +103 -67
- manim/mobject/types/vectorized_mobject.py +387 -214
- manim/mobject/value_tracker.py +2 -1
- manim/mobject/vector_field.py +2 -4
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +2 -3
- manim/plugins/plugins_flags.py +3 -3
- manim/renderer/cairo_renderer.py +11 -11
- manim/renderer/opengl_renderer.py +19 -20
- manim/renderer/shader.py +2 -3
- manim/renderer/shader_wrapper.py +3 -2
- manim/scene/moving_camera_scene.py +23 -0
- manim/scene/scene.py +72 -41
- manim/scene/scene_file_writer.py +313 -164
- manim/scene/section.py +15 -15
- manim/scene/three_d_scene.py +8 -15
- manim/scene/vector_space_scene.py +3 -6
- manim/typing.py +326 -66
- manim/utils/bezier.py +1658 -381
- manim/utils/caching.py +11 -5
- manim/utils/color/AS2700.py +2 -0
- manim/utils/color/BS381.py +2 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +3 -0
- manim/utils/color/XKCD.py +2 -0
- manim/utils/color/__init__.py +8 -5
- manim/utils/color/core.py +818 -301
- manim/utils/color/manim_colors.py +7 -9
- manim/utils/commands.py +40 -19
- manim/utils/config_ops.py +18 -13
- manim/utils/debug.py +8 -6
- manim/utils/deprecation.py +92 -43
- manim/utils/docbuild/autoaliasattr_directive.py +45 -8
- manim/utils/docbuild/autocolor_directive.py +12 -13
- manim/utils/docbuild/manim_directive.py +35 -29
- manim/utils/docbuild/module_parsing.py +74 -27
- manim/utils/family.py +3 -3
- manim/utils/family_ops.py +12 -4
- manim/utils/file_ops.py +22 -16
- manim/utils/hashing.py +7 -7
- manim/utils/images.py +10 -4
- manim/utils/ipython_magic.py +12 -8
- manim/utils/iterables.py +161 -119
- manim/utils/module_ops.py +55 -19
- manim/utils/opengl.py +68 -23
- manim/utils/parameter_parsing.py +3 -2
- manim/utils/paths.py +11 -5
- manim/utils/polylabel.py +168 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +69 -32
- manim/utils/simple_functions.py +24 -15
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +48 -37
- manim/utils/testing/_frames_testers.py +13 -8
- manim/utils/testing/_show_diff.py +5 -3
- manim/utils/testing/_test_class_makers.py +33 -18
- manim/utils/testing/frames_comparison.py +20 -14
- manim/utils/tex.py +4 -2
- manim/utils/tex_file_writing.py +45 -45
- manim/utils/tex_templates.py +1 -1
- manim/utils/unit.py +6 -5
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/METADATA +16 -9
- manim-0.19.0.dist-info/RECORD +221 -0
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
- manim-0.18.1.dist-info/RECORD +0 -217
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE.community +0 -0
- {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
manim/utils/rate_functions.py
CHANGED
|
@@ -105,21 +105,25 @@ __all__ = [
|
|
|
105
105
|
"exponential_decay",
|
|
106
106
|
]
|
|
107
107
|
|
|
108
|
-
import typing
|
|
109
108
|
from functools import wraps
|
|
110
109
|
from math import sqrt
|
|
110
|
+
from typing import Any, Protocol
|
|
111
111
|
|
|
112
112
|
import numpy as np
|
|
113
113
|
|
|
114
|
-
from
|
|
115
|
-
|
|
114
|
+
from manim.utils.simple_functions import sigmoid
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# TODO: rewrite this to use ParamSpec when Python 3.9 is out of life
|
|
118
|
+
class RateFunction(Protocol):
|
|
119
|
+
def __call__(self, t: float, *args: Any, **kwargs: Any) -> float: ...
|
|
116
120
|
|
|
117
121
|
|
|
118
122
|
# This is a decorator that makes sure any function it's used on will
|
|
119
123
|
# return 0 if t<0 and 1 if t>1.
|
|
120
|
-
def unit_interval(function):
|
|
124
|
+
def unit_interval(function: RateFunction) -> RateFunction:
|
|
121
125
|
@wraps(function)
|
|
122
|
-
def wrapper(t, *args, **kwargs):
|
|
126
|
+
def wrapper(t: float, *args: Any, **kwargs: Any) -> float:
|
|
123
127
|
if 0 <= t <= 1:
|
|
124
128
|
return function(t, *args, **kwargs)
|
|
125
129
|
elif t < 0:
|
|
@@ -132,9 +136,9 @@ def unit_interval(function):
|
|
|
132
136
|
|
|
133
137
|
# This is a decorator that makes sure any function it's used on will
|
|
134
138
|
# return 0 if t<0 or t>1.
|
|
135
|
-
def zero(function):
|
|
139
|
+
def zero(function: RateFunction) -> RateFunction:
|
|
136
140
|
@wraps(function)
|
|
137
|
-
def wrapper(t, *args, **kwargs):
|
|
141
|
+
def wrapper(t: float, *args: Any, **kwargs: Any) -> float:
|
|
138
142
|
if 0 <= t <= 1:
|
|
139
143
|
return function(t, *args, **kwargs)
|
|
140
144
|
else:
|
|
@@ -178,7 +182,7 @@ def smoothererstep(t: float) -> float:
|
|
|
178
182
|
The 1st, 2nd and 3rd derivatives (speed, acceleration and jerk) are zero at the endpoints.
|
|
179
183
|
https://en.wikipedia.org/wiki/Smoothstep
|
|
180
184
|
"""
|
|
181
|
-
alpha = 0
|
|
185
|
+
alpha: float = 0
|
|
182
186
|
if 0 < t < 1:
|
|
183
187
|
alpha = 35 * t**4 - 84 * t**5 + 70 * t**6 - 20 * t**7
|
|
184
188
|
elif t >= 1:
|
|
@@ -198,7 +202,8 @@ def rush_from(t: float, inflection: float = 10.0) -> float:
|
|
|
198
202
|
|
|
199
203
|
@unit_interval
|
|
200
204
|
def slow_into(t: float) -> float:
|
|
201
|
-
|
|
205
|
+
val: float = np.sqrt(1 - (1 - t) * (1 - t))
|
|
206
|
+
return val
|
|
202
207
|
|
|
203
208
|
|
|
204
209
|
@unit_interval
|
|
@@ -217,7 +222,7 @@ def there_and_back(t: float, inflection: float = 10.0) -> float:
|
|
|
217
222
|
|
|
218
223
|
@zero
|
|
219
224
|
def there_and_back_with_pause(t: float, pause_ratio: float = 1.0 / 3) -> float:
|
|
220
|
-
a =
|
|
225
|
+
a = 2.0 / (1.0 - pause_ratio)
|
|
221
226
|
if t < 0.5 - pause_ratio / 2:
|
|
222
227
|
return smooth(a * t)
|
|
223
228
|
elif t < 0.5 + pause_ratio / 2:
|
|
@@ -230,40 +235,60 @@ def there_and_back_with_pause(t: float, pause_ratio: float = 1.0 / 3) -> float:
|
|
|
230
235
|
def running_start(
|
|
231
236
|
t: float,
|
|
232
237
|
pull_factor: float = -0.5,
|
|
233
|
-
) ->
|
|
234
|
-
|
|
238
|
+
) -> float:
|
|
239
|
+
t2 = t * t
|
|
240
|
+
t3 = t2 * t
|
|
241
|
+
t4 = t3 * t
|
|
242
|
+
t5 = t4 * t
|
|
243
|
+
t6 = t5 * t
|
|
244
|
+
mt = 1 - t
|
|
245
|
+
mt2 = mt * mt
|
|
246
|
+
mt3 = mt2 * mt
|
|
247
|
+
mt4 = mt3 * mt
|
|
248
|
+
|
|
249
|
+
# This is equivalent to creating a Bézier with [0, 0, pull_factor, pull_factor, 1, 1, 1]
|
|
250
|
+
# and evaluating it at t.
|
|
251
|
+
return (
|
|
252
|
+
15 * t2 * mt4 * pull_factor
|
|
253
|
+
+ 20 * t3 * mt3 * pull_factor
|
|
254
|
+
+ 15 * t4 * mt2
|
|
255
|
+
+ 6 * t5 * mt
|
|
256
|
+
+ t6
|
|
257
|
+
)
|
|
235
258
|
|
|
236
259
|
|
|
237
260
|
def not_quite_there(
|
|
238
|
-
func:
|
|
261
|
+
func: RateFunction = smooth,
|
|
239
262
|
proportion: float = 0.7,
|
|
240
|
-
) ->
|
|
241
|
-
def result(t):
|
|
242
|
-
return proportion * func(t)
|
|
263
|
+
) -> RateFunction:
|
|
264
|
+
def result(t: float, *args: Any, **kwargs: Any) -> float:
|
|
265
|
+
return proportion * func(t, *args, **kwargs)
|
|
243
266
|
|
|
244
267
|
return result
|
|
245
268
|
|
|
246
269
|
|
|
247
270
|
@zero
|
|
248
271
|
def wiggle(t: float, wiggles: float = 2) -> float:
|
|
249
|
-
|
|
272
|
+
val: float = np.sin(wiggles * np.pi * t)
|
|
273
|
+
return there_and_back(t) * val
|
|
250
274
|
|
|
251
275
|
|
|
252
276
|
def squish_rate_func(
|
|
253
|
-
func:
|
|
277
|
+
func: RateFunction,
|
|
254
278
|
a: float = 0.4,
|
|
255
279
|
b: float = 0.6,
|
|
256
|
-
) ->
|
|
257
|
-
def result(t):
|
|
280
|
+
) -> RateFunction:
|
|
281
|
+
def result(t: float, *args: Any, **kwargs: Any) -> float:
|
|
258
282
|
if a == b:
|
|
259
283
|
return a
|
|
260
284
|
|
|
261
285
|
if t < a:
|
|
262
|
-
|
|
286
|
+
new_t = 0.0
|
|
263
287
|
elif t > b:
|
|
264
|
-
|
|
288
|
+
new_t = 1.0
|
|
265
289
|
else:
|
|
266
|
-
|
|
290
|
+
new_t = (t - a) / (b - a)
|
|
291
|
+
return func(new_t, *args, **kwargs)
|
|
267
292
|
|
|
268
293
|
return result
|
|
269
294
|
|
|
@@ -276,29 +301,37 @@ def squish_rate_func(
|
|
|
276
301
|
|
|
277
302
|
@unit_interval
|
|
278
303
|
def lingering(t: float) -> float:
|
|
279
|
-
|
|
304
|
+
def identity(t: float) -> float:
|
|
305
|
+
return t
|
|
306
|
+
|
|
307
|
+
# TODO: Isn't this just 0.8 * t?
|
|
308
|
+
return squish_rate_func(identity, 0, 0.8)(t)
|
|
280
309
|
|
|
281
310
|
|
|
282
311
|
@unit_interval
|
|
283
312
|
def exponential_decay(t: float, half_life: float = 0.1) -> float:
|
|
284
313
|
# The half-life should be rather small to minimize
|
|
285
314
|
# the cut-off error at the end
|
|
286
|
-
|
|
315
|
+
val: float = 1 - np.exp(-t / half_life)
|
|
316
|
+
return val
|
|
287
317
|
|
|
288
318
|
|
|
289
319
|
@unit_interval
|
|
290
320
|
def ease_in_sine(t: float) -> float:
|
|
291
|
-
|
|
321
|
+
val: float = 1 - np.cos((t * np.pi) / 2)
|
|
322
|
+
return val
|
|
292
323
|
|
|
293
324
|
|
|
294
325
|
@unit_interval
|
|
295
326
|
def ease_out_sine(t: float) -> float:
|
|
296
|
-
|
|
327
|
+
val: float = np.sin((t * np.pi) / 2)
|
|
328
|
+
return val
|
|
297
329
|
|
|
298
330
|
|
|
299
331
|
@unit_interval
|
|
300
332
|
def ease_in_out_sine(t: float) -> float:
|
|
301
|
-
|
|
333
|
+
val: float = -(np.cos(np.pi * t) - 1) / 2
|
|
334
|
+
return val
|
|
302
335
|
|
|
303
336
|
|
|
304
337
|
@unit_interval
|
|
@@ -435,7 +468,8 @@ def ease_in_elastic(t: float) -> float:
|
|
|
435
468
|
elif t == 1:
|
|
436
469
|
return 1
|
|
437
470
|
else:
|
|
438
|
-
|
|
471
|
+
val: float = -pow(2, 10 * t - 10) * np.sin((t * 10 - 10.75) * c4)
|
|
472
|
+
return val
|
|
439
473
|
|
|
440
474
|
|
|
441
475
|
@unit_interval
|
|
@@ -446,7 +480,8 @@ def ease_out_elastic(t: float) -> float:
|
|
|
446
480
|
elif t == 1:
|
|
447
481
|
return 1
|
|
448
482
|
else:
|
|
449
|
-
|
|
483
|
+
val: float = pow(2, -10 * t) * np.sin((t * 10 - 0.75) * c4) + 1
|
|
484
|
+
return val
|
|
450
485
|
|
|
451
486
|
|
|
452
487
|
@unit_interval
|
|
@@ -457,9 +492,11 @@ def ease_in_out_elastic(t: float) -> float:
|
|
|
457
492
|
elif t == 1:
|
|
458
493
|
return 1
|
|
459
494
|
elif t < 0.5:
|
|
460
|
-
|
|
495
|
+
val: float = -(pow(2, 20 * t - 10) * np.sin((20 * t - 11.125) * c5)) / 2
|
|
496
|
+
return val
|
|
461
497
|
else:
|
|
462
|
-
|
|
498
|
+
val = (pow(2, -20 * t + 10) * np.sin((20 * t - 11.125) * c5)) / 2 + 1
|
|
499
|
+
return val
|
|
463
500
|
|
|
464
501
|
|
|
465
502
|
@unit_interval
|
manim/utils/simple_functions.py
CHANGED
|
@@ -10,22 +10,20 @@ __all__ = [
|
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
import inspect
|
|
14
13
|
from functools import lru_cache
|
|
15
|
-
from
|
|
16
|
-
from typing import Callable
|
|
14
|
+
from typing import Any, Callable, Protocol, TypeVar
|
|
17
15
|
|
|
18
16
|
import numpy as np
|
|
19
17
|
from scipy import special
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
def binary_search(
|
|
23
|
-
function: Callable[[
|
|
24
|
-
target:
|
|
25
|
-
lower_bound:
|
|
26
|
-
upper_bound:
|
|
27
|
-
tolerance:
|
|
28
|
-
) ->
|
|
21
|
+
function: Callable[[float], float],
|
|
22
|
+
target: float,
|
|
23
|
+
lower_bound: float,
|
|
24
|
+
upper_bound: float,
|
|
25
|
+
tolerance: float = 1e-4,
|
|
26
|
+
) -> float | None:
|
|
29
27
|
"""Searches for a value in a range by repeatedly dividing the range in half.
|
|
30
28
|
|
|
31
29
|
To be more precise, performs numerical binary search to determine the
|
|
@@ -42,10 +40,10 @@ def binary_search(
|
|
|
42
40
|
::
|
|
43
41
|
|
|
44
42
|
>>> solution = binary_search(lambda x: x**2 + 3*x + 1, 11, 0, 5)
|
|
45
|
-
>>> abs(solution - 2) < 1e-4
|
|
43
|
+
>>> bool(abs(solution - 2) < 1e-4)
|
|
46
44
|
True
|
|
47
45
|
>>> solution = binary_search(lambda x: x**2 + 3*x + 1, 11, 0, 5, tolerance=0.01)
|
|
48
|
-
>>> abs(solution - 2) < 0.01
|
|
46
|
+
>>> bool(abs(solution - 2) < 0.01)
|
|
49
47
|
True
|
|
50
48
|
|
|
51
49
|
Searching in the interval :math:`[0, 5]` for a target value of :math:`71`
|
|
@@ -56,7 +54,7 @@ def binary_search(
|
|
|
56
54
|
"""
|
|
57
55
|
lh = lower_bound
|
|
58
56
|
rh = upper_bound
|
|
59
|
-
mh = np.mean(np.array([lh, rh]))
|
|
57
|
+
mh: float = np.mean(np.array([lh, rh]))
|
|
60
58
|
while abs(rh - lh) > tolerance:
|
|
61
59
|
mh = np.mean(np.array([lh, rh]))
|
|
62
60
|
lx, mx, rx = (function(h) for h in (lh, mh, rh))
|
|
@@ -90,10 +88,20 @@ def choose(n: int, k: int) -> int:
|
|
|
90
88
|
- https://en.wikipedia.org/wiki/Combination
|
|
91
89
|
- https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html
|
|
92
90
|
"""
|
|
93
|
-
|
|
91
|
+
value: int = special.comb(n, k, exact=True)
|
|
92
|
+
return value
|
|
94
93
|
|
|
95
94
|
|
|
96
|
-
|
|
95
|
+
class Comparable(Protocol):
|
|
96
|
+
def __lt__(self, other: Any) -> bool: ...
|
|
97
|
+
|
|
98
|
+
def __gt__(self, other: Any) -> bool: ...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
ComparableT = TypeVar("ComparableT", bound=Comparable) # noqa: Y001
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def clip(a: ComparableT, min_a: ComparableT, max_a: ComparableT) -> ComparableT:
|
|
97
105
|
"""Clips ``a`` to the interval [``min_a``, ``max_a``].
|
|
98
106
|
|
|
99
107
|
Accepts any comparable objects (i.e. those that support <, >).
|
|
@@ -127,4 +135,5 @@ def sigmoid(x: float) -> float:
|
|
|
127
135
|
- https://en.wikipedia.org/wiki/Sigmoid_function
|
|
128
136
|
- https://en.wikipedia.org/wiki/Logistic_function
|
|
129
137
|
"""
|
|
130
|
-
|
|
138
|
+
value: float = 1.0 / (1 + np.exp(-x))
|
|
139
|
+
return value
|
manim/utils/sounds.py
CHANGED
|
@@ -6,13 +6,19 @@ __all__ = [
|
|
|
6
6
|
"get_full_sound_file_path",
|
|
7
7
|
]
|
|
8
8
|
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
from .. import config
|
|
11
12
|
from ..utils.file_ops import seek_full_path_from_defaults
|
|
12
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from manim.typing import StrPath
|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
# Still in use by add_sound() function in scene_file_writer.py
|
|
15
|
-
def get_full_sound_file_path(sound_file_name):
|
|
21
|
+
def get_full_sound_file_path(sound_file_name: StrPath) -> Path:
|
|
16
22
|
return seek_full_path_from_defaults(
|
|
17
23
|
sound_file_name,
|
|
18
24
|
default_dir=config.get_dir("assets_dir"),
|
manim/utils/space_ops.py
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import itertools as it
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import TYPE_CHECKING, Callable
|
|
7
8
|
|
|
8
9
|
import numpy as np
|
|
9
10
|
from mapbox_earcut import triangulate_float32 as earcut
|
|
10
11
|
from scipy.spatial.transform import Rotation
|
|
11
12
|
|
|
12
|
-
from manim.constants import DOWN, OUT, PI, RIGHT, TAU, UP
|
|
13
|
+
from manim.constants import DOWN, OUT, PI, RIGHT, TAU, UP
|
|
13
14
|
from manim.utils.iterables import adjacent_pairs
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -17,10 +18,17 @@ if TYPE_CHECKING:
|
|
|
17
18
|
|
|
18
19
|
from manim.typing import (
|
|
19
20
|
ManimFloat,
|
|
20
|
-
|
|
21
|
+
MatrixMN,
|
|
22
|
+
Point2D_Array,
|
|
23
|
+
Point3D,
|
|
24
|
+
Point3DLike,
|
|
25
|
+
Point3DLike_Array,
|
|
26
|
+
PointND,
|
|
27
|
+
PointNDLike_Array,
|
|
21
28
|
Vector2D,
|
|
22
29
|
Vector2D_Array,
|
|
23
30
|
Vector3D,
|
|
31
|
+
Vector3D_Array,
|
|
24
32
|
)
|
|
25
33
|
|
|
26
34
|
__all__ = [
|
|
@@ -58,7 +66,8 @@ __all__ = [
|
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
def norm_squared(v: float) -> float:
|
|
61
|
-
|
|
69
|
+
val: float = np.dot(v, v)
|
|
70
|
+
return val
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
def cross(v1: Vector3D, v2: Vector3D) -> Vector3D:
|
|
@@ -192,7 +201,6 @@ def rotate_vector(
|
|
|
192
201
|
ValueError
|
|
193
202
|
If vector is not of dimension 2 or 3.
|
|
194
203
|
"""
|
|
195
|
-
|
|
196
204
|
if len(vector) > 3:
|
|
197
205
|
raise ValueError("Vector must have the correct dimensions.")
|
|
198
206
|
if len(vector) == 2:
|
|
@@ -200,7 +208,7 @@ def rotate_vector(
|
|
|
200
208
|
return rotation_matrix(angle, axis) @ vector
|
|
201
209
|
|
|
202
210
|
|
|
203
|
-
def thick_diagonal(dim: int, thickness=2) ->
|
|
211
|
+
def thick_diagonal(dim: int, thickness: int = 2) -> MatrixMN:
|
|
204
212
|
row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
|
|
205
213
|
col_indices = np.transpose(row_indices)
|
|
206
214
|
return (np.abs(row_indices - col_indices) < thickness).astype("uint8")
|
|
@@ -248,9 +256,7 @@ def rotation_matrix(
|
|
|
248
256
|
axis: np.ndarray,
|
|
249
257
|
homogeneous: bool = False,
|
|
250
258
|
) -> np.ndarray:
|
|
251
|
-
"""
|
|
252
|
-
Rotation in R^3 about a specified axis of rotation.
|
|
253
|
-
"""
|
|
259
|
+
"""Rotation in R^3 about a specified axis of rotation."""
|
|
254
260
|
inhomogeneous_rotation_matrix = Rotation.from_rotvec(
|
|
255
261
|
angle * normalize(np.array(axis))
|
|
256
262
|
).as_matrix()
|
|
@@ -320,8 +326,10 @@ def angle_of_vector(vector: Sequence[float] | np.ndarray) -> float:
|
|
|
320
326
|
c_vec = np.empty(vector.shape[1], dtype=np.complex128)
|
|
321
327
|
c_vec.real = vector[0]
|
|
322
328
|
c_vec.imag = vector[1]
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
val1: float = np.angle(c_vec)
|
|
330
|
+
return val1
|
|
331
|
+
val: float = np.angle(complex(*vector[:2]))
|
|
332
|
+
return val
|
|
325
333
|
|
|
326
334
|
|
|
327
335
|
def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
|
|
@@ -340,14 +348,17 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
|
|
|
340
348
|
float
|
|
341
349
|
The angle between the vectors.
|
|
342
350
|
"""
|
|
343
|
-
|
|
344
|
-
return 2 * np.arctan2(
|
|
351
|
+
val: float = 2 * np.arctan2(
|
|
345
352
|
np.linalg.norm(normalize(v1) - normalize(v2)),
|
|
346
353
|
np.linalg.norm(normalize(v1) + normalize(v2)),
|
|
347
354
|
)
|
|
348
355
|
|
|
356
|
+
return val
|
|
357
|
+
|
|
349
358
|
|
|
350
|
-
def normalize(
|
|
359
|
+
def normalize(
|
|
360
|
+
vect: np.ndarray | tuple[float], fall_back: np.ndarray | None = None
|
|
361
|
+
) -> np.ndarray:
|
|
351
362
|
norm = np.linalg.norm(vect)
|
|
352
363
|
if norm > 0:
|
|
353
364
|
return np.array(vect) / norm
|
|
@@ -473,12 +484,8 @@ def regular_vertices(
|
|
|
473
484
|
start_angle : :class:`float`
|
|
474
485
|
The angle the vertices start at.
|
|
475
486
|
"""
|
|
476
|
-
|
|
477
487
|
if start_angle is None:
|
|
478
|
-
if n % 2 == 0
|
|
479
|
-
start_angle = 0
|
|
480
|
-
else:
|
|
481
|
-
start_angle = TAU / 4
|
|
488
|
+
start_angle = 0 if n % 2 == 0 else TAU / 4
|
|
482
489
|
|
|
483
490
|
start_vector = rotate_vector(RIGHT * radius, start_angle)
|
|
484
491
|
vertices = compass_directions(n, start_vector)
|
|
@@ -494,11 +501,13 @@ def R3_to_complex(point: Sequence[float]) -> np.ndarray:
|
|
|
494
501
|
return complex(*point[:2])
|
|
495
502
|
|
|
496
503
|
|
|
497
|
-
def complex_func_to_R3_func(
|
|
504
|
+
def complex_func_to_R3_func(
|
|
505
|
+
complex_func: Callable[[complex], complex],
|
|
506
|
+
) -> Callable[[Point3DLike], Point3D]:
|
|
498
507
|
return lambda p: complex_to_R3(complex_func(R3_to_complex(p)))
|
|
499
508
|
|
|
500
509
|
|
|
501
|
-
def center_of_mass(points:
|
|
510
|
+
def center_of_mass(points: PointNDLike_Array) -> PointND:
|
|
502
511
|
"""Gets the center of mass of the points in space.
|
|
503
512
|
|
|
504
513
|
Parameters
|
|
@@ -580,12 +589,12 @@ def line_intersection(
|
|
|
580
589
|
|
|
581
590
|
|
|
582
591
|
def find_intersection(
|
|
583
|
-
p0s:
|
|
584
|
-
v0s:
|
|
585
|
-
p1s:
|
|
586
|
-
v1s:
|
|
592
|
+
p0s: Point3DLike_Array,
|
|
593
|
+
v0s: Vector3D_Array,
|
|
594
|
+
p1s: Point3DLike_Array,
|
|
595
|
+
v1s: Vector3D_Array,
|
|
587
596
|
threshold: float = 1e-5,
|
|
588
|
-
) ->
|
|
597
|
+
) -> list[Point3D]:
|
|
589
598
|
"""
|
|
590
599
|
Return the intersection of a line passing through p0 in direction v0
|
|
591
600
|
with one passing through p1 in direction v1 (or array of intersections
|
|
@@ -620,21 +629,22 @@ def get_winding_number(points: Sequence[np.ndarray]) -> float:
|
|
|
620
629
|
>>> from manim import Square, get_winding_number
|
|
621
630
|
>>> polygon = Square()
|
|
622
631
|
>>> get_winding_number(polygon.get_vertices())
|
|
623
|
-
1.0
|
|
624
|
-
>>> polygon.shift(2*UP)
|
|
632
|
+
np.float64(1.0)
|
|
633
|
+
>>> polygon.shift(2 * UP)
|
|
625
634
|
Square
|
|
626
635
|
>>> get_winding_number(polygon.get_vertices())
|
|
627
|
-
0.0
|
|
636
|
+
np.float64(0.0)
|
|
628
637
|
"""
|
|
629
|
-
total_angle = 0
|
|
638
|
+
total_angle: float = 0
|
|
630
639
|
for p1, p2 in adjacent_pairs(points):
|
|
631
640
|
d_angle = angle_of_vector(p2) - angle_of_vector(p1)
|
|
632
641
|
d_angle = ((d_angle + PI) % TAU) - PI
|
|
633
642
|
total_angle += d_angle
|
|
634
|
-
|
|
643
|
+
val: float = total_angle / TAU
|
|
644
|
+
return val
|
|
635
645
|
|
|
636
646
|
|
|
637
|
-
def shoelace(x_y:
|
|
647
|
+
def shoelace(x_y: Point2D_Array) -> float:
|
|
638
648
|
"""2D implementation of the shoelace formula.
|
|
639
649
|
|
|
640
650
|
Returns
|
|
@@ -644,10 +654,11 @@ def shoelace(x_y: np.ndarray) -> float:
|
|
|
644
654
|
"""
|
|
645
655
|
x = x_y[:, 0]
|
|
646
656
|
y = x_y[:, 1]
|
|
647
|
-
|
|
657
|
+
val: float = np.trapz(y, x)
|
|
658
|
+
return val
|
|
648
659
|
|
|
649
660
|
|
|
650
|
-
def shoelace_direction(x_y:
|
|
661
|
+
def shoelace_direction(x_y: Point2D_Array) -> str:
|
|
651
662
|
"""
|
|
652
663
|
Uses the area determined by the shoelace method to determine whether
|
|
653
664
|
the input set of points is directed clockwise or counterclockwise.
|
|
@@ -687,7 +698,7 @@ def cross2d(
|
|
|
687
698
|
.. code-block:: pycon
|
|
688
699
|
|
|
689
700
|
>>> cross2d(np.array([1, 2]), np.array([3, 4]))
|
|
690
|
-
-2
|
|
701
|
+
np.int64(-2)
|
|
691
702
|
>>> cross2d(
|
|
692
703
|
... np.array([[1, 2, 0], [1, 0, 0]]),
|
|
693
704
|
... np.array([[3, 4, 0], [0, 1, 0]]),
|
|
@@ -765,7 +776,7 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
|
|
|
765
776
|
raise Exception("Could not find a ring to attach")
|
|
766
777
|
|
|
767
778
|
# Setup linked list
|
|
768
|
-
after = []
|
|
779
|
+
after: list[int] = []
|
|
769
780
|
end0 = 0
|
|
770
781
|
for end1 in ring_ends:
|
|
771
782
|
after.extend(range(end0 + 1, end1))
|
|
@@ -836,7 +847,7 @@ def spherical_to_cartesian(spherical: Sequence[float]) -> np.ndarray:
|
|
|
836
847
|
|
|
837
848
|
def perpendicular_bisector(
|
|
838
849
|
line: Sequence[np.ndarray],
|
|
839
|
-
norm_vector=OUT,
|
|
850
|
+
norm_vector: Vector3D = OUT,
|
|
840
851
|
) -> Sequence[np.ndarray]:
|
|
841
852
|
"""Returns a list of two points that correspond
|
|
842
853
|
to the ends of the perpendicular bisector of the
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
import logging
|
|
4
5
|
import warnings
|
|
6
|
+
from collections.abc import Generator
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
import numpy as np
|
|
8
10
|
|
|
9
|
-
from manim import
|
|
11
|
+
from manim.typing import PixelArray
|
|
10
12
|
|
|
11
13
|
from ._show_diff import show_diff_helper
|
|
12
14
|
|
|
13
15
|
FRAME_ABSOLUTE_TOLERANCE = 1.01
|
|
14
16
|
FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5
|
|
15
17
|
|
|
18
|
+
logger = logging.getLogger("manim")
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
class _FramesTester:
|
|
18
|
-
def __init__(self, file_path: Path, show_diff=False) -> None:
|
|
22
|
+
def __init__(self, file_path: Path, show_diff: bool = False) -> None:
|
|
19
23
|
self._file_path = file_path
|
|
20
24
|
self._show_diff = show_diff
|
|
21
25
|
self._frames: np.ndarray
|
|
@@ -23,7 +27,7 @@ class _FramesTester:
|
|
|
23
27
|
self._frames_compared = 0
|
|
24
28
|
|
|
25
29
|
@contextlib.contextmanager
|
|
26
|
-
def testing(self):
|
|
30
|
+
def testing(self) -> Generator[None, None, None]:
|
|
27
31
|
with np.load(self._file_path) as data:
|
|
28
32
|
self._frames = data["frame_data"]
|
|
29
33
|
# For backward compatibility, when the control data contains only one frame (<= v0.8.0)
|
|
@@ -37,7 +41,7 @@ class _FramesTester:
|
|
|
37
41
|
f"when there are {self._number_frames} control frames for this test."
|
|
38
42
|
)
|
|
39
43
|
|
|
40
|
-
def check_frame(self, frame_number: int, frame:
|
|
44
|
+
def check_frame(self, frame_number: int, frame: PixelArray) -> None:
|
|
41
45
|
assert frame_number < self._number_frames, (
|
|
42
46
|
f"The tested scene is at frame number {frame_number} "
|
|
43
47
|
f"when there are {self._number_frames} control frames."
|
|
@@ -63,7 +67,8 @@ class _FramesTester:
|
|
|
63
67
|
warnings.warn(
|
|
64
68
|
f"Mismatch of {number_of_mismatches} pixel values in frame {frame_number} "
|
|
65
69
|
f"against control data in {self._file_path}. Below error threshold, "
|
|
66
|
-
"continuing..."
|
|
70
|
+
"continuing...",
|
|
71
|
+
stacklevel=1,
|
|
67
72
|
)
|
|
68
73
|
return
|
|
69
74
|
|
|
@@ -84,17 +89,17 @@ class _ControlDataWriter(_FramesTester):
|
|
|
84
89
|
self._number_frames_written: int = 0
|
|
85
90
|
|
|
86
91
|
# Actually write a frame.
|
|
87
|
-
def check_frame(self, index: int, frame:
|
|
92
|
+
def check_frame(self, index: int, frame: PixelArray) -> None:
|
|
88
93
|
frame = frame[np.newaxis, ...]
|
|
89
94
|
self.frames = np.concatenate((self.frames, frame))
|
|
90
95
|
self._number_frames_written += 1
|
|
91
96
|
|
|
92
97
|
@contextlib.contextmanager
|
|
93
|
-
def testing(self):
|
|
98
|
+
def testing(self) -> Generator[None, None, None]:
|
|
94
99
|
yield
|
|
95
100
|
self.save_contol_data()
|
|
96
101
|
|
|
97
|
-
def save_contol_data(self):
|
|
102
|
+
def save_contol_data(self) -> None:
|
|
98
103
|
self.frames = self.frames.astype("uint8")
|
|
99
104
|
np.savez_compressed(self.file_path, frame_data=self.frames)
|
|
100
105
|
logger.info(
|
|
@@ -5,13 +5,15 @@ import warnings
|
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
|
+
from manim.typing import PixelArray
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def show_diff_helper(
|
|
10
12
|
frame_number: int,
|
|
11
|
-
frame_data:
|
|
12
|
-
expected_frame_data:
|
|
13
|
+
frame_data: PixelArray,
|
|
14
|
+
expected_frame_data: PixelArray,
|
|
13
15
|
control_data_filename: str,
|
|
14
|
-
):
|
|
16
|
+
) -> None:
|
|
15
17
|
"""Will visually display with matplotlib differences between frame generated and the one expected."""
|
|
16
18
|
import matplotlib.gridspec as gridspec
|
|
17
19
|
import matplotlib.pyplot as plt
|