1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166from __future__ import annotations
import numpy as np
from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, DEFAULT_MOBJECT_COLOR
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.rate_functions import smooth
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, List, Iterable
from manimlib.typing import ManimColor, Vect3, Self
class AnimatedBoundary(VGroup):
def __init__(
self,
vmobject: VMobject,
colors: List[ManimColor] = [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
max_stroke_width: float = 3.0,
cycle_rate: float = 0.5,
back_and_forth: bool = True,
draw_rate_func: Callable[[float], float] = smooth,
fade_rate_func: Callable[[float], float] = smooth,
**kwargs
):
super().__init__(**kwargs)
self.vmobject: VMobject = vmobject
self.colors = colors
self.max_stroke_width = max_stroke_width
self.cycle_rate = cycle_rate
self.back_and_forth = back_and_forth
self.draw_rate_func = draw_rate_func
self.fade_rate_func = fade_rate_func
self.boundary_copies: list[VMobject] = [
vmobject.copy().set_style(
stroke_width=0,
fill_opacity=0
)
for x in range(2)
]
self.add(*self.boundary_copies)
self.total_time: float = 0
self.add_updater(
lambda m, dt: self.update_boundary_copies(dt)
)
def update_boundary_copies(self, dt: float) -> Self:
# Not actual time, but something which passes at
# an altered rate to make the implementation below
# cleaner
time = self.total_time * self.cycle_rate
growing, fading = self.boundary_copies
colors = self.colors
msw = self.max_stroke_width
vmobject = self.vmobject
index = int(time % len(colors))
alpha = time % 1
draw_alpha = self.draw_rate_func(alpha)
fade_alpha = self.fade_rate_func(alpha)
if self.back_and_forth and int(time) % 2 == 1:
bounds = (1 - draw_alpha, 1)
else:
bounds = (0, draw_alpha)
self.full_family_become_partial(growing, vmobject, *bounds)
growing.set_stroke(colors[index], width=msw)
if time >= 1:
self.full_family_become_partial(fading, vmobject, 0, 1)
fading.set_stroke(
color=colors[index - 1],
width=(1 - fade_alpha) * msw
)
self.total_time += dt
return self
def full_family_become_partial(
self,
mob1: VMobject,
mob2: VMobject,
a: float,
b: float
) -> Self:
family1 = mob1.family_members_with_points()
family2 = mob2.family_members_with_points()
for sm1, sm2 in zip(family1, family2):
sm1.pointwise_become_partial(sm2, a, b)
return self
class TracedPath(VMobject):
def __init__(
self,
traced_point_func: Callable[[], Vect3],
time_traced: float = np.inf,
time_per_anchor: float = 1.0 / 15,
stroke_width: float | Iterable[float] = 2.0,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
**kwargs
):
super().__init__(**kwargs)
self.traced_point_func = traced_point_func
self.time_traced = time_traced
self.time_per_anchor = time_per_anchor
self.time: float = 0
self.traced_points: list[np.ndarray] = []
self.add_updater(lambda m, dt: m.update_path(dt))
self.always.set_stroke(stroke_color, stroke_width)
def update_path(self, dt: float) -> Self:
if dt == 0:
return self
point = self.traced_point_func().copy()
self.traced_points.append(point)
if self.time_traced < np.inf:
n_relevant_points = int(self.time_traced / dt + 0.5)
n_tps = len(self.traced_points)
if n_tps < n_relevant_points:
points = self.traced_points + [point] * (n_relevant_points - n_tps)
else:
points = self.traced_points[n_tps - n_relevant_points:]
# Every now and then refresh the list
if n_tps > 10 * n_relevant_points:
self.traced_points = self.traced_points[-n_relevant_points:]
else:
points = self.traced_points
if points:
self.set_points_smoothly(points)
self.time += dt
return self
class TracingTail(TracedPath):
def __init__(
self,
mobject_or_func: Mobject | Callable[[], np.ndarray],
time_traced: float = 1.0,
stroke_width: float | Iterable[float] = (0, 3),
stroke_opacity: float | Iterable[float] = (0, 1),
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
**kwargs
):
if isinstance(mobject_or_func, Mobject):
func = mobject_or_func.get_center
else:
func = mobject_or_func
super().__init__(
func,
time_traced=time_traced,
stroke_width=stroke_width,
stroke_opacity=stroke_opacity,
stroke_color=stroke_color,
**kwargs
)
self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity))