manim 0.18.0.post0__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/__init__.py +3 -6
- manim/__main__.py +61 -20
- manim/_config/__init__.py +6 -3
- manim/_config/cli_colors.py +16 -8
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +14 -8
- manim/_config/utils.py +651 -472
- manim/animation/animation.py +152 -5
- manim/animation/composition.py +80 -39
- manim/animation/creation.py +196 -14
- manim/animation/fading.py +5 -9
- manim/animation/indication.py +103 -47
- manim/animation/movement.py +22 -5
- manim/animation/rotation.py +3 -2
- manim/animation/specialized.py +4 -6
- manim/animation/speedmodifier.py +10 -5
- manim/animation/transform.py +4 -5
- manim/animation/transform_matching_parts.py +1 -1
- manim/animation/updaters/mobject_update_utils.py +17 -14
- manim/camera/camera.py +15 -6
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +70 -44
- manim/cli/checkhealth/checks.py +93 -75
- manim/cli/checkhealth/commands.py +14 -5
- manim/cli/default_group.py +157 -25
- manim/cli/init/commands.py +32 -24
- manim/cli/plugins/commands.py +16 -3
- manim/cli/render/commands.py +72 -60
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +51 -15
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +97 -32
- manim/constants.py +65 -19
- manim/gui/gui.py +2 -0
- manim/mobject/frame.py +0 -1
- manim/mobject/geometry/arc.py +112 -78
- manim/mobject/geometry/boolean_ops.py +32 -25
- manim/mobject/geometry/labeled.py +300 -77
- manim/mobject/geometry/line.py +132 -64
- manim/mobject/geometry/polygram.py +126 -30
- manim/mobject/geometry/shape_matchers.py +35 -15
- manim/mobject/geometry/tips.py +38 -29
- manim/mobject/graph.py +414 -133
- manim/mobject/graphing/coordinate_systems.py +126 -64
- manim/mobject/graphing/functions.py +25 -15
- manim/mobject/graphing/number_line.py +24 -10
- manim/mobject/graphing/probability.py +2 -10
- manim/mobject/graphing/scale.py +6 -5
- manim/mobject/matrix.py +17 -19
- manim/mobject/mobject.py +314 -165
- manim/mobject/opengl/opengl_compatibility.py +2 -0
- manim/mobject/opengl/opengl_geometry.py +30 -9
- manim/mobject/opengl/opengl_image_mobject.py +2 -0
- manim/mobject/opengl/opengl_mobject.py +509 -343
- manim/mobject/opengl/opengl_point_cloud_mobject.py +5 -7
- manim/mobject/opengl/opengl_surface.py +3 -2
- manim/mobject/opengl/opengl_three_dimensions.py +2 -0
- manim/mobject/opengl/opengl_vectorized_mobject.py +46 -79
- manim/mobject/svg/brace.py +63 -13
- manim/mobject/svg/svg_mobject.py +4 -3
- manim/mobject/table.py +11 -13
- manim/mobject/text/code_mobject.py +186 -548
- manim/mobject/text/numbers.py +9 -7
- manim/mobject/text/tex_mobject.py +23 -14
- manim/mobject/text/text_mobject.py +70 -24
- manim/mobject/three_d/polyhedra.py +98 -1
- manim/mobject/three_d/three_d_utils.py +4 -4
- manim/mobject/three_d/three_dimensions.py +62 -34
- manim/mobject/types/image_mobject.py +42 -24
- manim/mobject/types/point_cloud_mobject.py +105 -67
- manim/mobject/types/vectorized_mobject.py +496 -228
- manim/mobject/value_tracker.py +5 -4
- manim/mobject/vector_field.py +5 -5
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +14 -8
- manim/renderer/cairo_renderer.py +20 -10
- manim/renderer/opengl_renderer.py +21 -23
- manim/renderer/opengl_renderer_window.py +2 -0
- manim/renderer/shader.py +2 -3
- manim/renderer/shader_wrapper.py +5 -2
- manim/renderer/vectorized_mobject_rendering.py +5 -0
- manim/scene/moving_camera_scene.py +23 -0
- manim/scene/scene.py +90 -43
- manim/scene/scene_file_writer.py +316 -165
- manim/scene/section.py +17 -15
- manim/scene/three_d_scene.py +13 -21
- manim/scene/vector_space_scene.py +22 -9
- manim/typing.py +830 -70
- manim/utils/bezier.py +1667 -399
- manim/utils/caching.py +13 -5
- manim/utils/color/AS2700.py +2 -0
- manim/utils/color/BS381.py +3 -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 +3 -0
- manim/utils/color/__init__.py +8 -5
- manim/utils/color/core.py +844 -309
- manim/utils/color/manim_colors.py +7 -9
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +18 -13
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +90 -40
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +234 -0
- manim/utils/docbuild/autocolor_directive.py +21 -17
- manim/utils/docbuild/manim_directive.py +50 -35
- manim/utils/docbuild/module_parsing.py +245 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +5 -3
- manim/utils/family_ops.py +17 -4
- manim/utils/file_ops.py +26 -16
- manim/utils/hashing.py +9 -7
- manim/utils/images.py +10 -4
- manim/utils/ipython_magic.py +14 -8
- manim/utils/iterables.py +161 -119
- manim/utils/module_ops.py +57 -19
- manim/utils/opengl.py +83 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +21 -23
- manim/utils/polylabel.py +168 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +74 -39
- manim/utils/simple_functions.py +24 -15
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +125 -69
- manim/utils/testing/__init__.py +17 -0
- 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 +27 -19
- manim/utils/tex.py +127 -197
- manim/utils/tex_file_writing.py +47 -45
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE.community +1 -1
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/METADATA +40 -39
- manim-0.19.0.dist-info/RECORD +221 -0
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
- manim/cli/new/__init__.py +0 -0
- manim/cli/new/group.py +0 -189
- manim/plugins/import_plugins.py +0 -43
- manim-0.18.0.post0.dist-info/RECORD +0 -217
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
- {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
manim/utils/qhull.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from manim.typing import PointND, PointND_Array
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QuickHullPoint:
|
|
13
|
+
def __init__(self, coordinates: PointND_Array) -> None:
|
|
14
|
+
self.coordinates = coordinates
|
|
15
|
+
|
|
16
|
+
def __hash__(self) -> int:
|
|
17
|
+
return hash(self.coordinates.tobytes())
|
|
18
|
+
|
|
19
|
+
def __eq__(self, other: object) -> bool:
|
|
20
|
+
if not isinstance(other, QuickHullPoint):
|
|
21
|
+
raise ValueError
|
|
22
|
+
are_coordinates_equal: bool = np.array_equal(
|
|
23
|
+
self.coordinates, other.coordinates
|
|
24
|
+
)
|
|
25
|
+
return are_coordinates_equal
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SubFacet:
|
|
29
|
+
def __init__(self, coordinates: PointND_Array) -> None:
|
|
30
|
+
self.coordinates = coordinates
|
|
31
|
+
self.points = frozenset(QuickHullPoint(c) for c in coordinates)
|
|
32
|
+
|
|
33
|
+
def __hash__(self) -> int:
|
|
34
|
+
return hash(self.points)
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other: object) -> bool:
|
|
37
|
+
if not isinstance(other, SubFacet):
|
|
38
|
+
raise ValueError
|
|
39
|
+
return self.points == other.points
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Facet:
|
|
43
|
+
def __init__(self, coordinates: PointND_Array, internal: PointND) -> None:
|
|
44
|
+
self.coordinates = coordinates
|
|
45
|
+
self.center: PointND = np.mean(coordinates, axis=0)
|
|
46
|
+
self.normal = self.compute_normal(internal)
|
|
47
|
+
self.subfacets = frozenset(
|
|
48
|
+
SubFacet(np.delete(self.coordinates, i, axis=0))
|
|
49
|
+
for i in range(self.coordinates.shape[0])
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def compute_normal(self, internal: PointND) -> PointND:
|
|
53
|
+
centered = self.coordinates - self.center
|
|
54
|
+
_, _, vh = np.linalg.svd(centered)
|
|
55
|
+
normal: PointND = vh[-1, :]
|
|
56
|
+
normal /= np.linalg.norm(normal)
|
|
57
|
+
|
|
58
|
+
# If the normal points towards the internal point, flip it!
|
|
59
|
+
if np.dot(normal, self.center - internal) < 0:
|
|
60
|
+
normal *= -1
|
|
61
|
+
|
|
62
|
+
return normal
|
|
63
|
+
|
|
64
|
+
def __hash__(self) -> int:
|
|
65
|
+
return hash(self.subfacets)
|
|
66
|
+
|
|
67
|
+
def __eq__(self, other: object) -> bool:
|
|
68
|
+
if not isinstance(other, Facet):
|
|
69
|
+
raise ValueError
|
|
70
|
+
return self.subfacets == other.subfacets
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Horizon:
|
|
74
|
+
def __init__(self) -> None:
|
|
75
|
+
self.facets: set[Facet] = set()
|
|
76
|
+
self.boundary: list[SubFacet] = []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class QuickHull:
|
|
80
|
+
"""
|
|
81
|
+
QuickHull algorithm for constructing a convex hull from a set of points.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
tolerance
|
|
86
|
+
A tolerance threshold for determining when points lie on the convex hull (default is 1e-5).
|
|
87
|
+
|
|
88
|
+
Attributes
|
|
89
|
+
----------
|
|
90
|
+
facets
|
|
91
|
+
List of facets considered.
|
|
92
|
+
removed
|
|
93
|
+
Set of internal facets that have been removed from the hull during the construction process.
|
|
94
|
+
outside
|
|
95
|
+
Dictionary mapping each facet to its outside points and eye point.
|
|
96
|
+
neighbors
|
|
97
|
+
Mapping of subfacets to their neighboring facets. Each subfacet links precisely two neighbors.
|
|
98
|
+
unclaimed
|
|
99
|
+
Points that have not yet been classified as inside or outside the current hull.
|
|
100
|
+
internal
|
|
101
|
+
An internal point (i.e., the center of the initial simplex) used as a reference during hull construction.
|
|
102
|
+
tolerance
|
|
103
|
+
The tolerance used to determine if points are considered outside the current hull.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, tolerance: float = 1e-5) -> None:
|
|
107
|
+
self.facets: list[Facet] = []
|
|
108
|
+
self.removed: set[Facet] = set()
|
|
109
|
+
self.outside: dict[Facet, tuple[PointND_Array | None, PointND | None]] = {}
|
|
110
|
+
self.neighbors: dict[SubFacet, set[Facet]] = {}
|
|
111
|
+
self.unclaimed: PointND_Array | None = None
|
|
112
|
+
self.internal: PointND | None = None
|
|
113
|
+
self.tolerance = tolerance
|
|
114
|
+
|
|
115
|
+
def initialize(self, points: PointND_Array) -> None:
|
|
116
|
+
# Sample Points
|
|
117
|
+
simplex = points[
|
|
118
|
+
np.random.choice(points.shape[0], points.shape[1] + 1, replace=False)
|
|
119
|
+
]
|
|
120
|
+
self.unclaimed = points
|
|
121
|
+
new_internal: PointND = np.mean(simplex, axis=0)
|
|
122
|
+
self.internal = new_internal
|
|
123
|
+
|
|
124
|
+
# Build Simplex
|
|
125
|
+
for c in range(simplex.shape[0]):
|
|
126
|
+
facet = Facet(np.delete(simplex, c, axis=0), internal=new_internal)
|
|
127
|
+
self.classify(facet)
|
|
128
|
+
self.facets.append(facet)
|
|
129
|
+
|
|
130
|
+
# Attach Neighbors
|
|
131
|
+
for f in self.facets:
|
|
132
|
+
for sf in f.subfacets:
|
|
133
|
+
self.neighbors.setdefault(sf, set()).add(f)
|
|
134
|
+
|
|
135
|
+
def classify(self, facet: Facet) -> None:
|
|
136
|
+
assert self.unclaimed is not None, (
|
|
137
|
+
"Call .initialize() before using .classify()."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if not self.unclaimed.size:
|
|
141
|
+
self.outside[facet] = (None, None)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Compute Projections
|
|
145
|
+
projections = (self.unclaimed - facet.center) @ facet.normal
|
|
146
|
+
arg = np.argmax(projections)
|
|
147
|
+
mask = projections > self.tolerance
|
|
148
|
+
|
|
149
|
+
# Identify Eye and Outside Set
|
|
150
|
+
eye = self.unclaimed[arg] if projections[arg] > self.tolerance else None
|
|
151
|
+
outside = self.unclaimed[mask]
|
|
152
|
+
self.outside[facet] = (outside, eye)
|
|
153
|
+
self.unclaimed = self.unclaimed[~mask]
|
|
154
|
+
|
|
155
|
+
def compute_horizon(self, eye: PointND, start_facet: Facet) -> Horizon:
|
|
156
|
+
horizon = Horizon()
|
|
157
|
+
self._recursive_horizon(eye, start_facet, horizon)
|
|
158
|
+
return horizon
|
|
159
|
+
|
|
160
|
+
def _recursive_horizon(self, eye: PointND, facet: Facet, horizon: Horizon) -> bool:
|
|
161
|
+
visible = np.dot(facet.normal, eye - facet.center) > 0
|
|
162
|
+
if not visible:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
# If the eye is visible from the facet:
|
|
166
|
+
# Label the facet as visible and cross each edge
|
|
167
|
+
horizon.facets.add(facet)
|
|
168
|
+
for subfacet in facet.subfacets:
|
|
169
|
+
neighbor = (self.neighbors[subfacet] - {facet}).pop()
|
|
170
|
+
# If the neighbor is not visible, then the edge shared must be on the boundary
|
|
171
|
+
if neighbor not in horizon.facets and not self._recursive_horizon(
|
|
172
|
+
eye, neighbor, horizon
|
|
173
|
+
):
|
|
174
|
+
horizon.boundary.append(subfacet)
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
def build(self, points: PointND_Array) -> None:
|
|
178
|
+
num, dim = points.shape
|
|
179
|
+
if (dim == 0) or (num < dim + 1):
|
|
180
|
+
raise ValueError("Not enough points supplied to build Convex Hull!")
|
|
181
|
+
if dim == 1:
|
|
182
|
+
raise ValueError("The Convex Hull of 1D data is its min-max!")
|
|
183
|
+
|
|
184
|
+
self.initialize(points)
|
|
185
|
+
|
|
186
|
+
# This helps the type checker.
|
|
187
|
+
assert self.unclaimed is not None
|
|
188
|
+
assert self.internal is not None
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
updated = False
|
|
192
|
+
for facet in self.facets:
|
|
193
|
+
if facet in self.removed:
|
|
194
|
+
continue
|
|
195
|
+
outside, eye = self.outside[facet]
|
|
196
|
+
if eye is not None:
|
|
197
|
+
updated = True
|
|
198
|
+
horizon = self.compute_horizon(eye, facet)
|
|
199
|
+
for f in horizon.facets:
|
|
200
|
+
points_to_append = self.outside[f][0]
|
|
201
|
+
# TODO: is this always true?
|
|
202
|
+
assert points_to_append is not None
|
|
203
|
+
self.unclaimed = np.vstack((self.unclaimed, points_to_append))
|
|
204
|
+
self.removed.add(f)
|
|
205
|
+
for sf in f.subfacets:
|
|
206
|
+
self.neighbors[sf].discard(f)
|
|
207
|
+
if self.neighbors[sf] == set():
|
|
208
|
+
del self.neighbors[sf]
|
|
209
|
+
for sf in horizon.boundary:
|
|
210
|
+
nf = Facet(
|
|
211
|
+
np.vstack((sf.coordinates, eye)), internal=self.internal
|
|
212
|
+
)
|
|
213
|
+
self.classify(nf)
|
|
214
|
+
self.facets.append(nf)
|
|
215
|
+
for nsf in nf.subfacets:
|
|
216
|
+
self.neighbors.setdefault(nsf, set()).add(nf)
|
|
217
|
+
if not updated:
|
|
218
|
+
break
|
manim/utils/rate_functions.py
CHANGED
|
@@ -83,7 +83,6 @@ There are primarily 3 kinds of standard easing functions:
|
|
|
83
83
|
self.wait()
|
|
84
84
|
"""
|
|
85
85
|
|
|
86
|
-
|
|
87
86
|
from __future__ import annotations
|
|
88
87
|
|
|
89
88
|
__all__ = [
|
|
@@ -106,21 +105,25 @@ __all__ = [
|
|
|
106
105
|
"exponential_decay",
|
|
107
106
|
]
|
|
108
107
|
|
|
109
|
-
import typing
|
|
110
108
|
from functools import wraps
|
|
111
109
|
from math import sqrt
|
|
110
|
+
from typing import Any, Protocol
|
|
112
111
|
|
|
113
112
|
import numpy as np
|
|
114
113
|
|
|
115
|
-
from
|
|
116
|
-
|
|
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: ...
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
# This is a decorator that makes sure any function it's used on will
|
|
120
123
|
# return 0 if t<0 and 1 if t>1.
|
|
121
|
-
def unit_interval(function):
|
|
124
|
+
def unit_interval(function: RateFunction) -> RateFunction:
|
|
122
125
|
@wraps(function)
|
|
123
|
-
def wrapper(t, *args, **kwargs):
|
|
126
|
+
def wrapper(t: float, *args: Any, **kwargs: Any) -> float:
|
|
124
127
|
if 0 <= t <= 1:
|
|
125
128
|
return function(t, *args, **kwargs)
|
|
126
129
|
elif t < 0:
|
|
@@ -133,9 +136,9 @@ def unit_interval(function):
|
|
|
133
136
|
|
|
134
137
|
# This is a decorator that makes sure any function it's used on will
|
|
135
138
|
# return 0 if t<0 or t>1.
|
|
136
|
-
def zero(function):
|
|
139
|
+
def zero(function: RateFunction) -> RateFunction:
|
|
137
140
|
@wraps(function)
|
|
138
|
-
def wrapper(t, *args, **kwargs):
|
|
141
|
+
def wrapper(t: float, *args: Any, **kwargs: Any) -> float:
|
|
139
142
|
if 0 <= t <= 1:
|
|
140
143
|
return function(t, *args, **kwargs)
|
|
141
144
|
else:
|
|
@@ -179,13 +182,12 @@ def smoothererstep(t: float) -> float:
|
|
|
179
182
|
The 1st, 2nd and 3rd derivatives (speed, acceleration and jerk) are zero at the endpoints.
|
|
180
183
|
https://en.wikipedia.org/wiki/Smoothstep
|
|
181
184
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
185
|
+
alpha: float = 0
|
|
186
|
+
if 0 < t < 1:
|
|
187
|
+
alpha = 35 * t**4 - 84 * t**5 + 70 * t**6 - 20 * t**7
|
|
188
|
+
elif t >= 1:
|
|
189
|
+
alpha = 1
|
|
190
|
+
return alpha
|
|
189
191
|
|
|
190
192
|
|
|
191
193
|
@unit_interval
|
|
@@ -200,7 +202,8 @@ def rush_from(t: float, inflection: float = 10.0) -> float:
|
|
|
200
202
|
|
|
201
203
|
@unit_interval
|
|
202
204
|
def slow_into(t: float) -> float:
|
|
203
|
-
|
|
205
|
+
val: float = np.sqrt(1 - (1 - t) * (1 - t))
|
|
206
|
+
return val
|
|
204
207
|
|
|
205
208
|
|
|
206
209
|
@unit_interval
|
|
@@ -219,7 +222,7 @@ def there_and_back(t: float, inflection: float = 10.0) -> float:
|
|
|
219
222
|
|
|
220
223
|
@zero
|
|
221
224
|
def there_and_back_with_pause(t: float, pause_ratio: float = 1.0 / 3) -> float:
|
|
222
|
-
a =
|
|
225
|
+
a = 2.0 / (1.0 - pause_ratio)
|
|
223
226
|
if t < 0.5 - pause_ratio / 2:
|
|
224
227
|
return smooth(a * t)
|
|
225
228
|
elif t < 0.5 + pause_ratio / 2:
|
|
@@ -232,40 +235,60 @@ def there_and_back_with_pause(t: float, pause_ratio: float = 1.0 / 3) -> float:
|
|
|
232
235
|
def running_start(
|
|
233
236
|
t: float,
|
|
234
237
|
pull_factor: float = -0.5,
|
|
235
|
-
) ->
|
|
236
|
-
|
|
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
|
+
)
|
|
237
258
|
|
|
238
259
|
|
|
239
260
|
def not_quite_there(
|
|
240
|
-
func:
|
|
261
|
+
func: RateFunction = smooth,
|
|
241
262
|
proportion: float = 0.7,
|
|
242
|
-
) ->
|
|
243
|
-
def result(t):
|
|
244
|
-
return proportion * func(t)
|
|
263
|
+
) -> RateFunction:
|
|
264
|
+
def result(t: float, *args: Any, **kwargs: Any) -> float:
|
|
265
|
+
return proportion * func(t, *args, **kwargs)
|
|
245
266
|
|
|
246
267
|
return result
|
|
247
268
|
|
|
248
269
|
|
|
249
270
|
@zero
|
|
250
271
|
def wiggle(t: float, wiggles: float = 2) -> float:
|
|
251
|
-
|
|
272
|
+
val: float = np.sin(wiggles * np.pi * t)
|
|
273
|
+
return there_and_back(t) * val
|
|
252
274
|
|
|
253
275
|
|
|
254
276
|
def squish_rate_func(
|
|
255
|
-
func:
|
|
277
|
+
func: RateFunction,
|
|
256
278
|
a: float = 0.4,
|
|
257
279
|
b: float = 0.6,
|
|
258
|
-
) ->
|
|
259
|
-
def result(t):
|
|
280
|
+
) -> RateFunction:
|
|
281
|
+
def result(t: float, *args: Any, **kwargs: Any) -> float:
|
|
260
282
|
if a == b:
|
|
261
283
|
return a
|
|
262
284
|
|
|
263
285
|
if t < a:
|
|
264
|
-
|
|
286
|
+
new_t = 0.0
|
|
265
287
|
elif t > b:
|
|
266
|
-
|
|
288
|
+
new_t = 1.0
|
|
267
289
|
else:
|
|
268
|
-
|
|
290
|
+
new_t = (t - a) / (b - a)
|
|
291
|
+
return func(new_t, *args, **kwargs)
|
|
269
292
|
|
|
270
293
|
return result
|
|
271
294
|
|
|
@@ -278,29 +301,37 @@ def squish_rate_func(
|
|
|
278
301
|
|
|
279
302
|
@unit_interval
|
|
280
303
|
def lingering(t: float) -> float:
|
|
281
|
-
|
|
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)
|
|
282
309
|
|
|
283
310
|
|
|
284
311
|
@unit_interval
|
|
285
312
|
def exponential_decay(t: float, half_life: float = 0.1) -> float:
|
|
286
313
|
# The half-life should be rather small to minimize
|
|
287
314
|
# the cut-off error at the end
|
|
288
|
-
|
|
315
|
+
val: float = 1 - np.exp(-t / half_life)
|
|
316
|
+
return val
|
|
289
317
|
|
|
290
318
|
|
|
291
319
|
@unit_interval
|
|
292
320
|
def ease_in_sine(t: float) -> float:
|
|
293
|
-
|
|
321
|
+
val: float = 1 - np.cos((t * np.pi) / 2)
|
|
322
|
+
return val
|
|
294
323
|
|
|
295
324
|
|
|
296
325
|
@unit_interval
|
|
297
326
|
def ease_out_sine(t: float) -> float:
|
|
298
|
-
|
|
327
|
+
val: float = np.sin((t * np.pi) / 2)
|
|
328
|
+
return val
|
|
299
329
|
|
|
300
330
|
|
|
301
331
|
@unit_interval
|
|
302
332
|
def ease_in_out_sine(t: float) -> float:
|
|
303
|
-
|
|
333
|
+
val: float = -(np.cos(np.pi * t) - 1) / 2
|
|
334
|
+
return val
|
|
304
335
|
|
|
305
336
|
|
|
306
337
|
@unit_interval
|
|
@@ -437,7 +468,8 @@ def ease_in_elastic(t: float) -> float:
|
|
|
437
468
|
elif t == 1:
|
|
438
469
|
return 1
|
|
439
470
|
else:
|
|
440
|
-
|
|
471
|
+
val: float = -pow(2, 10 * t - 10) * np.sin((t * 10 - 10.75) * c4)
|
|
472
|
+
return val
|
|
441
473
|
|
|
442
474
|
|
|
443
475
|
@unit_interval
|
|
@@ -448,7 +480,8 @@ def ease_out_elastic(t: float) -> float:
|
|
|
448
480
|
elif t == 1:
|
|
449
481
|
return 1
|
|
450
482
|
else:
|
|
451
|
-
|
|
483
|
+
val: float = pow(2, -10 * t) * np.sin((t * 10 - 0.75) * c4) + 1
|
|
484
|
+
return val
|
|
452
485
|
|
|
453
486
|
|
|
454
487
|
@unit_interval
|
|
@@ -459,9 +492,11 @@ def ease_in_out_elastic(t: float) -> float:
|
|
|
459
492
|
elif t == 1:
|
|
460
493
|
return 1
|
|
461
494
|
elif t < 0.5:
|
|
462
|
-
|
|
495
|
+
val: float = -(pow(2, 20 * t - 10) * np.sin((20 * t - 11.125) * c5)) / 2
|
|
496
|
+
return val
|
|
463
497
|
else:
|
|
464
|
-
|
|
498
|
+
val = (pow(2, -20 * t + 10) * np.sin((20 * t - 11.125) * c5)) / 2 + 1
|
|
499
|
+
return val
|
|
465
500
|
|
|
466
501
|
|
|
467
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"),
|