pymations 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +24 -2
- package/THIRD-PARTY-NOTICES +217 -0
- package/engine/anim.js +8 -0
- package/engine/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/engine/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/engine/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/engine/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/engine/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/engine/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/engine/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/engine/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/engine/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/engine/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/engine/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/engine/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/engine/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/engine/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/engine/fonts/LICENSE_KATEX_FONTS.txt +106 -0
- package/engine/fonts/LICENSE_LIBERATION.txt +102 -0
- package/engine/fonts/LiberationSans-Bold.ttf +0 -0
- package/engine/fonts/LiberationSans-BoldItalic.ttf +0 -0
- package/engine/fonts/LiberationSans-Italic.ttf +0 -0
- package/engine/fonts/LiberationSans-Regular.ttf +0 -0
- package/engine/katex.min.css +1 -0
- package/engine/katex.min.js +1 -0
- package/engine/opentype.min.js +2 -0
- package/package.json +34 -4
- package/pyctures.py +895 -0
- package/pymations.js +1805 -0
- package/pymations.py +4346 -0
package/pymations.py
ADDED
|
@@ -0,0 +1,4346 @@
|
|
|
1
|
+
# GENERATED by circuitry/scripts/sync-pymations-lib.mjs from src/lib/python-canvas/canvas-anim-py.ts (version 15).
|
|
2
|
+
# Do not edit here: change it in Circuitry, then run the script.
|
|
3
|
+
# Pymations is MIT licensed; see LICENSE.
|
|
4
|
+
import sys, math, types, inspect
|
|
5
|
+
|
|
6
|
+
VERSION = "15"
|
|
7
|
+
_rt = sys.modules["pyctures"]
|
|
8
|
+
H_ANIM = 6
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------- run state
|
|
12
|
+
|
|
13
|
+
class _AnimState:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.next_id = 1 # 0 is the camera frame
|
|
16
|
+
self.next_uid = 1 # updater ids
|
|
17
|
+
self.reset_sent = False
|
|
18
|
+
self.cfg = {} # config values set this run
|
|
19
|
+
self.scene = None # the implicit scene of a bare script
|
|
20
|
+
self.capture = None # [builder target, ops] while .animate records
|
|
21
|
+
self.trace = None # _Trace while an updater is being traced
|
|
22
|
+
self.building = False # the Scene runner is recording construct()
|
|
23
|
+
self.rendered = set() # Scene classes the file ran itself (render())
|
|
24
|
+
self.told = set() # one-time notes already printed
|
|
25
|
+
self.section_sound = None # a section's voice-over: the section lasts until it ends
|
|
26
|
+
self.scenes_run = 0 # Scene classes started (transitions go between them)
|
|
27
|
+
self.anim_t = 0.0 # timeline time of this frame (tick payload)
|
|
28
|
+
self.anim_v = None # tracker id -> value at anim_t (tick payload), once sent
|
|
29
|
+
self.fallbacks = [] # updaters that run in Python every frame
|
|
30
|
+
self.canvases = [] # PycturesCanvas objects (drawn every frame)
|
|
31
|
+
self.objects = {} # id -> mobject made this run
|
|
32
|
+
self.last_anim_t = None # to notice a scrub back
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _st():
|
|
36
|
+
st = getattr(_rt.S, "anim", None)
|
|
37
|
+
if st is None:
|
|
38
|
+
st = _AnimState()
|
|
39
|
+
_rt.S.anim = st
|
|
40
|
+
return st
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _on_start(info):
|
|
44
|
+
_rt.S.anim = st = _AnimState()
|
|
45
|
+
_frame_info(st, info)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _frame_info(st, p):
|
|
49
|
+
if "anim_t" in p:
|
|
50
|
+
st.anim_t = float(p["anim_t"] or 0.0)
|
|
51
|
+
if "anim_v" in p:
|
|
52
|
+
st.anim_v = {int(k): v for k, v in (p["anim_v"] or {}).items()}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _before_frame(p):
|
|
56
|
+
"""A tick's timeline time and tracker values, before any frame code runs."""
|
|
57
|
+
_frame_info(_st(), p)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _on_frame(p):
|
|
61
|
+
"""Every frame (start and each tick): Python-run updaters and canvases."""
|
|
62
|
+
st = _st()
|
|
63
|
+
_frame_info(st, p)
|
|
64
|
+
if st.fallbacks and st.last_anim_t is not None and st.anim_t < st.last_anim_t:
|
|
65
|
+
# Scrubbed back: what Python-run dt updaters added no longer holds.
|
|
66
|
+
_call("live_clear")
|
|
67
|
+
for u in st.fallbacks:
|
|
68
|
+
u.last_t = None
|
|
69
|
+
st.last_anim_t = st.anim_t
|
|
70
|
+
for c in list(st.canvases):
|
|
71
|
+
c._redraw()
|
|
72
|
+
for u in list(st.fallbacks):
|
|
73
|
+
u.run(st)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _stays_live():
|
|
77
|
+
st = getattr(_rt.S, "anim", None)
|
|
78
|
+
return bool(st and (st.fallbacks or st.canvases))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _live_phase():
|
|
82
|
+
"""Commands now come from a Pyctures loop (draw(), await frame(), an
|
|
83
|
+
event), not from the scene being recorded: they change the live scene."""
|
|
84
|
+
st = _st()
|
|
85
|
+
return bool(getattr(_rt.S, "live", False)) and not st.building
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _tell(key, text):
|
|
89
|
+
st = _st()
|
|
90
|
+
if key not in st.told:
|
|
91
|
+
st.told.add(key)
|
|
92
|
+
print(text)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _reset_once():
|
|
96
|
+
"""The first engine call of a run resets the engine, so a file that never
|
|
97
|
+
animates never touches it (its page has no engine)."""
|
|
98
|
+
st = _st()
|
|
99
|
+
if not st.reset_sent:
|
|
100
|
+
st.reset_sent = True
|
|
101
|
+
_rt._emit("call", 0, H_ANIM, "reset", [{}])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _call(method, *args):
|
|
105
|
+
if _st().trace is not None:
|
|
106
|
+
raise _Untraceable("it calls %s()" % method)
|
|
107
|
+
_reset_once()
|
|
108
|
+
_rt._emit("call", 0, H_ANIM, method, [_j(a) for a in args])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _Untraceable(Exception):
|
|
112
|
+
"""An updater did something that cannot become a formula."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _read(target, what, args=None):
|
|
116
|
+
"""An awaitable engine value (engine.value), freed once it is read."""
|
|
117
|
+
_reset_once()
|
|
118
|
+
h = _rt._new_handle()
|
|
119
|
+
_rt._emit("call", h, H_ANIM, "value", [_j(target), what, _j(args)])
|
|
120
|
+
r = _rt._Read({"$h": h})
|
|
121
|
+
_rt.S.cmds.append(["free", 0, h])
|
|
122
|
+
return r
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------- values
|
|
126
|
+
#
|
|
127
|
+
# A value only the engine knows (c.get_center(), m.width, tracker.get_value(),
|
|
128
|
+
# the time) is an Expr: a formula the engine works out. Maths on it builds a
|
|
129
|
+
# bigger formula ({"$pt": tree} for points, {"$x": tree} for numbers, see
|
|
130
|
+
# "Contract v2" in pyctures-animation.md), so placement and updaters run in the
|
|
131
|
+
# browser. 'await e' reads the numbers; using one as a Python number (if, float,
|
|
132
|
+
# print) needs that await.
|
|
133
|
+
|
|
134
|
+
_LAZY_HINT = ("is worked out in the browser, so Python does not have its numbers yet. "
|
|
135
|
+
"Read them with p = await <expression> (in a Scene, write async def construct(self))")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _lazy_error(what):
|
|
139
|
+
tr = _st().trace
|
|
140
|
+
if tr is not None and not tr.real:
|
|
141
|
+
raise _Untraceable("it uses %s as a Python number" % what)
|
|
142
|
+
if tr is not None:
|
|
143
|
+
raise _rt.PreviewValueError(
|
|
144
|
+
what + " is worked out in the browser, so an updater that runs in Python cannot use it as a "
|
|
145
|
+
"number. Test a ValueTracker's value or pymations.time() instead, or keep the maths in "
|
|
146
|
+
"np.where(...) so the browser works it out")
|
|
147
|
+
raise _rt.PreviewValueError(what + " " + _LAZY_HINT)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _num(v, what="a number"):
|
|
151
|
+
"""A number argument: a float, or {"$x": …} when the engine works it out."""
|
|
152
|
+
if isinstance(v, Expr):
|
|
153
|
+
if v.shape != "s":
|
|
154
|
+
raise TypeError("expected %s, got a point" % what)
|
|
155
|
+
return v._enc()
|
|
156
|
+
try:
|
|
157
|
+
f = float(v)
|
|
158
|
+
except _Untraceable:
|
|
159
|
+
raise
|
|
160
|
+
except Exception:
|
|
161
|
+
raise TypeError("expected %s, got %r" % (what, v))
|
|
162
|
+
return f if f == f and f not in (float("inf"), float("-inf")) else 0.0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_sym(v):
|
|
166
|
+
return isinstance(v, Expr) or (isinstance(v, (list, tuple)) and any(isinstance(x, Expr) for x in v))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _j(v):
|
|
170
|
+
"""Plain JSON for the engine."""
|
|
171
|
+
if v is None or isinstance(v, (bool, str)):
|
|
172
|
+
return v
|
|
173
|
+
if isinstance(v, int):
|
|
174
|
+
return v
|
|
175
|
+
if isinstance(v, float):
|
|
176
|
+
return v if v == v and v not in (float("inf"), float("-inf")) else None
|
|
177
|
+
if isinstance(v, Expr):
|
|
178
|
+
return v._enc()
|
|
179
|
+
if isinstance(v, Mobject):
|
|
180
|
+
return v._ref()
|
|
181
|
+
if isinstance(v, _Rate):
|
|
182
|
+
return v.name
|
|
183
|
+
if isinstance(v, (list, tuple)):
|
|
184
|
+
if any(isinstance(x, Expr) for x in v):
|
|
185
|
+
return {"$pt": _tree(v)}
|
|
186
|
+
return [_j(x) for x in v]
|
|
187
|
+
if isinstance(v, dict):
|
|
188
|
+
return {str(k): _j(x) for k, x in v.items()}
|
|
189
|
+
if isinstance(v, type):
|
|
190
|
+
return v.__name__
|
|
191
|
+
if callable(v):
|
|
192
|
+
return None
|
|
193
|
+
tolist = getattr(v, "tolist", None) # numpy arrays and scalars
|
|
194
|
+
if tolist is not None:
|
|
195
|
+
return _j(tolist())
|
|
196
|
+
return str(v)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Vec(tuple):
|
|
200
|
+
"""A 3D vector (UP, LEFT …) that does arithmetic like a numpy array."""
|
|
201
|
+
__slots__ = ()
|
|
202
|
+
__array_ufunc__ = None # numpy hands its arithmetic with a Vec to us
|
|
203
|
+
|
|
204
|
+
def __new__(cls, x=0.0, y=0.0, z=0.0):
|
|
205
|
+
return tuple.__new__(cls, (float(x), float(y), float(z)))
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def of(v):
|
|
209
|
+
if isinstance(v, Vec):
|
|
210
|
+
return v
|
|
211
|
+
tolist = getattr(v, "tolist", None)
|
|
212
|
+
if tolist is not None:
|
|
213
|
+
v = tolist()
|
|
214
|
+
v = list(v)
|
|
215
|
+
if len(v) == 2:
|
|
216
|
+
v.append(0.0)
|
|
217
|
+
if len(v) != 3:
|
|
218
|
+
raise TypeError("a point needs 2 or 3 numbers, got %r" % (v,))
|
|
219
|
+
return Vec(_num(v[0]), _num(v[1]), _num(v[2]))
|
|
220
|
+
|
|
221
|
+
def _other(self, o):
|
|
222
|
+
if isinstance(o, Expr) or hasattr(type(o), "_t"): # symbolic, or a mobject
|
|
223
|
+
return None
|
|
224
|
+
if isinstance(o, (int, float)) and not isinstance(o, bool):
|
|
225
|
+
return Vec(o, o, o)
|
|
226
|
+
try:
|
|
227
|
+
return Vec.of(o)
|
|
228
|
+
except Exception:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def __add__(self, o):
|
|
232
|
+
o = self._other(o)
|
|
233
|
+
if o is None:
|
|
234
|
+
return NotImplemented
|
|
235
|
+
return Vec(self[0] + o[0], self[1] + o[1], self[2] + o[2])
|
|
236
|
+
|
|
237
|
+
__radd__ = __add__
|
|
238
|
+
|
|
239
|
+
def __sub__(self, o):
|
|
240
|
+
o = self._other(o)
|
|
241
|
+
if o is None:
|
|
242
|
+
return NotImplemented
|
|
243
|
+
return Vec(self[0] - o[0], self[1] - o[1], self[2] - o[2])
|
|
244
|
+
|
|
245
|
+
def __rsub__(self, o):
|
|
246
|
+
o = self._other(o)
|
|
247
|
+
if o is None:
|
|
248
|
+
return NotImplemented
|
|
249
|
+
return Vec(o[0] - self[0], o[1] - self[1], o[2] - self[2])
|
|
250
|
+
|
|
251
|
+
def __mul__(self, o):
|
|
252
|
+
o = self._other(o)
|
|
253
|
+
if o is None:
|
|
254
|
+
return NotImplemented
|
|
255
|
+
return Vec(self[0] * o[0], self[1] * o[1], self[2] * o[2])
|
|
256
|
+
|
|
257
|
+
__rmul__ = __mul__
|
|
258
|
+
|
|
259
|
+
def __truediv__(self, o):
|
|
260
|
+
o = self._other(o)
|
|
261
|
+
if o is None:
|
|
262
|
+
return NotImplemented
|
|
263
|
+
return Vec(self[0] / o[0], self[1] / o[1], self[2] / o[2])
|
|
264
|
+
|
|
265
|
+
def __neg__(self):
|
|
266
|
+
return Vec(-self[0], -self[1], -self[2])
|
|
267
|
+
|
|
268
|
+
def __pos__(self):
|
|
269
|
+
return self
|
|
270
|
+
|
|
271
|
+
def __abs__(self):
|
|
272
|
+
return math.sqrt(self[0] ** 2 + self[1] ** 2 + self[2] ** 2)
|
|
273
|
+
|
|
274
|
+
def tolist(self):
|
|
275
|
+
return list(self)
|
|
276
|
+
|
|
277
|
+
def __repr__(self):
|
|
278
|
+
return "Vec(%g, %g, %g)" % self
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _shape_of(v):
|
|
284
|
+
if isinstance(v, Expr):
|
|
285
|
+
return v.shape
|
|
286
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
287
|
+
return "s"
|
|
288
|
+
return "v"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
_UFUNCS = {
|
|
292
|
+
"add": "add", "subtract": "sub", "multiply": "mul", "true_divide": "div", "divide": "div",
|
|
293
|
+
"power": "pow", "remainder": "mod", "mod": "mod", "negative": "neg",
|
|
294
|
+
"sin": "sin", "cos": "cos", "tan": "tan", "arcsin": "asin", "arccos": "acos", "arctan": "atan",
|
|
295
|
+
"arctan2": "atan2", "sinh": "sinh", "cosh": "cosh", "tanh": "tanh", "exp": "exp", "log": "log",
|
|
296
|
+
"log10": "log10", "log2": "log2", "sqrt": "sqrt", "absolute": "abs", "fabs": "abs", "floor": "floor",
|
|
297
|
+
"ceil": "ceil", "rint": "round", "sign": "sign", "minimum": "min", "maximum": "max", "fmin": "min",
|
|
298
|
+
"fmax": "max", "hypot": "hypot", "degrees": "degrees", "radians": "radians", "rad2deg": "degrees",
|
|
299
|
+
"deg2rad": "radians", "less": "lt", "less_equal": "le", "greater": "gt", "greater_equal": "ge",
|
|
300
|
+
"equal": "eq", "not_equal": "ne",
|
|
301
|
+
}
|
|
302
|
+
# Our names → the engine's formula nodes (Contract v2, as the engine evaluates them).
|
|
303
|
+
_BINOPS = {"add": "+", "sub": "-", "mul": "*", "div": "/", "pow": "**", "mod": "%"}
|
|
304
|
+
_CMPS = {"lt": "<", "le": "<=", "gt": ">", "ge": ">=", "eq": "==", "ne": "!="}
|
|
305
|
+
_FN_NAMES = {"asin": "arcsin", "acos": "arccos", "atan": "arctan", "atan2": "arctan2", "degrees": "deg",
|
|
306
|
+
"radians": "rad"}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _fn(name, *args):
|
|
310
|
+
"""name(args) as a formula when any arg is symbolic."""
|
|
311
|
+
shape = "v" if any(_shape_of(a) == "v" for a in args) and name != "norm" else "s"
|
|
312
|
+
if name in _BINOPS:
|
|
313
|
+
return Expr([_BINOPS[name]] + [_tree(a) for a in args], shape)
|
|
314
|
+
if name == "neg":
|
|
315
|
+
return Expr(["neg", _tree(args[0])], shape)
|
|
316
|
+
if name in _CMPS:
|
|
317
|
+
return Expr(["cmp", _CMPS[name], _tree(args[0]), _tree(args[1])], shape)
|
|
318
|
+
if name == "hypot":
|
|
319
|
+
a, b = args
|
|
320
|
+
return _fn("sqrt", _fn("add", _fn("mul", a, a), _fn("mul", b, b)))
|
|
321
|
+
return Expr(["fn", _FN_NAMES.get(name, name)] + [_tree(a) for a in args], shape)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Expr:
|
|
325
|
+
"""A number or point the engine works out (c.get_center(), m.width,
|
|
326
|
+
tracker.get_value(), axes.c2p(1, 2), the time …), and maths on it.
|
|
327
|
+
'await e' gives its value."""
|
|
328
|
+
__slots__ = ("tree", "shape")
|
|
329
|
+
|
|
330
|
+
def __init__(self, tree, shape="v"):
|
|
331
|
+
self.tree = tree
|
|
332
|
+
self.shape = shape
|
|
333
|
+
|
|
334
|
+
def _enc(self):
|
|
335
|
+
return {"$pt" if self.shape == "v" else "$x": self.tree}
|
|
336
|
+
|
|
337
|
+
def __add__(self, o):
|
|
338
|
+
return _fn("add", self, o)
|
|
339
|
+
|
|
340
|
+
def __radd__(self, o):
|
|
341
|
+
return _fn("add", o, self)
|
|
342
|
+
|
|
343
|
+
def __sub__(self, o):
|
|
344
|
+
return _fn("sub", self, o)
|
|
345
|
+
|
|
346
|
+
def __rsub__(self, o):
|
|
347
|
+
return _fn("sub", o, self)
|
|
348
|
+
|
|
349
|
+
def __mul__(self, o):
|
|
350
|
+
return _fn("mul", self, o)
|
|
351
|
+
|
|
352
|
+
def __rmul__(self, o):
|
|
353
|
+
return _fn("mul", o, self)
|
|
354
|
+
|
|
355
|
+
def __truediv__(self, o):
|
|
356
|
+
return _fn("div", self, o)
|
|
357
|
+
|
|
358
|
+
def __rtruediv__(self, o):
|
|
359
|
+
return _fn("div", o, self)
|
|
360
|
+
|
|
361
|
+
def __pow__(self, o):
|
|
362
|
+
return _fn("pow", self, o)
|
|
363
|
+
|
|
364
|
+
def __rpow__(self, o):
|
|
365
|
+
return _fn("pow", o, self)
|
|
366
|
+
|
|
367
|
+
def __mod__(self, o):
|
|
368
|
+
return _fn("mod", self, o)
|
|
369
|
+
|
|
370
|
+
def __rmod__(self, o):
|
|
371
|
+
return _fn("mod", o, self)
|
|
372
|
+
|
|
373
|
+
def __neg__(self):
|
|
374
|
+
return _fn("neg", self)
|
|
375
|
+
|
|
376
|
+
def __pos__(self):
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
def __abs__(self):
|
|
380
|
+
return _fn("abs", self)
|
|
381
|
+
|
|
382
|
+
def __round__(self, n=None):
|
|
383
|
+
return _fn("round", self)
|
|
384
|
+
|
|
385
|
+
def __lt__(self, o):
|
|
386
|
+
return _fn("lt", self, o)
|
|
387
|
+
|
|
388
|
+
def __le__(self, o):
|
|
389
|
+
return _fn("le", self, o)
|
|
390
|
+
|
|
391
|
+
def __gt__(self, o):
|
|
392
|
+
return _fn("gt", self, o)
|
|
393
|
+
|
|
394
|
+
def __ge__(self, o):
|
|
395
|
+
return _fn("ge", self, o)
|
|
396
|
+
|
|
397
|
+
def __getitem__(self, i):
|
|
398
|
+
if self.shape != "v":
|
|
399
|
+
_lazy_error("this number")
|
|
400
|
+
if isinstance(i, slice):
|
|
401
|
+
return [self[k] for k in range(3)[i]]
|
|
402
|
+
return Expr(["idx", self.tree, int(i) % 3], "s")
|
|
403
|
+
|
|
404
|
+
def __iter__(self):
|
|
405
|
+
if self.shape != "v":
|
|
406
|
+
_lazy_error("this number")
|
|
407
|
+
return iter([self[0], self[1], self[2]])
|
|
408
|
+
|
|
409
|
+
def __len__(self):
|
|
410
|
+
if self.shape != "v":
|
|
411
|
+
_lazy_error("this number")
|
|
412
|
+
return 3
|
|
413
|
+
|
|
414
|
+
def _no(self, *a):
|
|
415
|
+
_lazy_error("this point" if self.shape == "v" else "this value")
|
|
416
|
+
|
|
417
|
+
__float__ = __int__ = __index__ = __bool__ = __complex__ = _no
|
|
418
|
+
|
|
419
|
+
def __format__(self, spec):
|
|
420
|
+
self._no()
|
|
421
|
+
|
|
422
|
+
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
423
|
+
name = _UFUNCS.get(ufunc.__name__)
|
|
424
|
+
if method != "__call__" or name is None or kwargs.get("out") is not None:
|
|
425
|
+
return NotImplemented
|
|
426
|
+
return _fn(name, *[_unarray(x) for x in inputs])
|
|
427
|
+
|
|
428
|
+
def __array_function__(self, func, types_, args, kwargs):
|
|
429
|
+
name = func.__name__
|
|
430
|
+
if name == "clip":
|
|
431
|
+
return _fn("clip", _unarray(args[0]), *[_unarray(a) for a in args[1:3]])
|
|
432
|
+
if name == "norm":
|
|
433
|
+
return _fn("norm", _unarray(args[0]))
|
|
434
|
+
if name == "where":
|
|
435
|
+
return Expr(["where", _tree(args[0]), _tree(args[1]), _tree(args[2])],
|
|
436
|
+
"v" if "v" in (_shape_of(args[1]), _shape_of(args[2])) else "s")
|
|
437
|
+
if name in ("array", "asarray"):
|
|
438
|
+
return args[0]
|
|
439
|
+
return NotImplemented
|
|
440
|
+
|
|
441
|
+
def __await__(self):
|
|
442
|
+
t = self.tree
|
|
443
|
+
if self.shape == "v":
|
|
444
|
+
v = yield from _read(0, "point", self._enc()).__await__()
|
|
445
|
+
return Vec.of(v)
|
|
446
|
+
if t[0] == "get" and len(t) == 3:
|
|
447
|
+
return (yield from _read(t[1], t[2]).__await__())
|
|
448
|
+
if t[0] == "val":
|
|
449
|
+
return (yield from _read(t[1], "value").__await__())
|
|
450
|
+
return (yield from _read(0, "number", self._enc()).__await__())
|
|
451
|
+
|
|
452
|
+
def __repr__(self):
|
|
453
|
+
return "<%s worked out in the browser>" % ("point" if self.shape == "v" else "value")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _unarray(v):
|
|
457
|
+
tolist = getattr(v, "tolist", None)
|
|
458
|
+
if tolist is not None and not isinstance(v, (Expr, Vec)):
|
|
459
|
+
v = tolist()
|
|
460
|
+
return v
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def Point(tree):
|
|
464
|
+
"""A point the engine works out, from its formula."""
|
|
465
|
+
return Expr(tree, "v")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _tree(v):
|
|
469
|
+
if isinstance(v, Expr):
|
|
470
|
+
return v.tree
|
|
471
|
+
if isinstance(v, Mobject):
|
|
472
|
+
return ["of", v._ref(), "center"]
|
|
473
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
474
|
+
return float(v)
|
|
475
|
+
v = _unarray(v)
|
|
476
|
+
v = list(v)
|
|
477
|
+
if any(isinstance(x, Expr) for x in v):
|
|
478
|
+
while len(v) < 3:
|
|
479
|
+
v.append(0.0)
|
|
480
|
+
return ["v", [_tree(x) for x in v[:3]]]
|
|
481
|
+
return ["v", list(Vec.of(v))]
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _P(v):
|
|
485
|
+
"""A point argument: [x, y, z], or {"$pt": …} when the engine works it out."""
|
|
486
|
+
if v is None:
|
|
487
|
+
return None
|
|
488
|
+
if isinstance(v, Expr):
|
|
489
|
+
if v.shape != "v":
|
|
490
|
+
raise TypeError("expected a point, got a number")
|
|
491
|
+
return v._enc()
|
|
492
|
+
if isinstance(v, Mobject):
|
|
493
|
+
return {"$pt": ["of", v._ref(), "center"]}
|
|
494
|
+
if _is_sym(_unarray(v)):
|
|
495
|
+
return {"$pt": _tree(v)}
|
|
496
|
+
return list(Vec.of(v))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _MP(v):
|
|
500
|
+
"""A mobject (its target) or a point."""
|
|
501
|
+
return v._embed() if isinstance(v, Mobject) else _P(v)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _coord(v):
|
|
505
|
+
"""A coordinate inside a formula: a number, or the formula of a symbolic one."""
|
|
506
|
+
if isinstance(v, Expr):
|
|
507
|
+
return v.tree
|
|
508
|
+
return _num(v, "a coordinate")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _var(name, shape="s"):
|
|
512
|
+
"""The time ["t"], an updater's step ["dt"], or a function's variable."""
|
|
513
|
+
if name in ("time", "dt"):
|
|
514
|
+
return Expr(["t"] if name == "time" else ["dt"], shape)
|
|
515
|
+
return Expr(["var", name], shape)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def time():
|
|
519
|
+
"""The animation's time in seconds: a formula inside an updater, the
|
|
520
|
+
timeline time of this frame inside a Pyctures loop."""
|
|
521
|
+
st = _st()
|
|
522
|
+
tr = st.trace
|
|
523
|
+
if tr is not None and not tr.real:
|
|
524
|
+
return _var("time")
|
|
525
|
+
if tr is not None or _live_phase():
|
|
526
|
+
return st.anim_t
|
|
527
|
+
return _var("time")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
_ANCHORS = {(0, 0): "center", (0, 1): "top", (0, -1): "bottom", (-1, 0): "left",
|
|
531
|
+
(1, 0): "right", (-1, 1): "UL", (1, 1): "UR", (-1, -1): "DL", (1, -1): "DR"}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _anchor(direction):
|
|
535
|
+
d = Vec.of(direction)
|
|
536
|
+
sx = (d[0] > 1e-9) - (d[0] < -1e-9)
|
|
537
|
+
sy = (d[1] > 1e-9) - (d[1] < -1e-9)
|
|
538
|
+
return _ANCHORS[(sx, sy)]
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---------------------------------------------------------------- constants
|
|
542
|
+
|
|
543
|
+
ORIGIN = Vec(0, 0, 0)
|
|
544
|
+
UP, DOWN, LEFT, RIGHT = Vec(0, 1, 0), Vec(0, -1, 0), Vec(-1, 0, 0), Vec(1, 0, 0)
|
|
545
|
+
IN, OUT = Vec(0, 0, -1), Vec(0, 0, 1)
|
|
546
|
+
UL, UR, DL, DR = UP + LEFT, UP + RIGHT, DOWN + LEFT, DOWN + RIGHT
|
|
547
|
+
X_AXIS, Y_AXIS, Z_AXIS = Vec(1, 0, 0), Vec(0, 1, 0), Vec(0, 0, 1)
|
|
548
|
+
PI = math.pi
|
|
549
|
+
TAU = 2 * math.pi
|
|
550
|
+
DEGREES = TAU / 360
|
|
551
|
+
SMALL_BUFF = 0.1
|
|
552
|
+
MED_SMALL_BUFF = 0.25
|
|
553
|
+
MED_LARGE_BUFF = 0.5
|
|
554
|
+
LARGE_BUFF = 1.0
|
|
555
|
+
DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF
|
|
556
|
+
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF
|
|
557
|
+
DEFAULT_STROKE_WIDTH = 4
|
|
558
|
+
DEFAULT_DOT_RADIUS = 0.08
|
|
559
|
+
DEFAULT_SMALL_DOT_RADIUS = 0.04
|
|
560
|
+
DEFAULT_ARROW_TIP_LENGTH = 0.35
|
|
561
|
+
DEFAULT_FONT_SIZE = 48
|
|
562
|
+
SCALE_FACTOR_PER_FONT_POINT = 1 / 960
|
|
563
|
+
DEFAULT_DASH_LENGTH = 0.05
|
|
564
|
+
DEFAULT_WAIT_TIME = 1.0
|
|
565
|
+
NORMAL, BOLD, ITALIC, OBLIQUE = "NORMAL", "BOLD", "ITALIC", "OBLIQUE"
|
|
566
|
+
|
|
567
|
+
_COLORS = dict(
|
|
568
|
+
WHITE="#FFFFFF", BLACK="#000000",
|
|
569
|
+
GRAY_A="#DDDDDD", GRAY_B="#BBBBBB", GRAY_C="#888888", GRAY_D="#444444", GRAY_E="#222222",
|
|
570
|
+
LIGHTER_GRAY="#DDDDDD", LIGHT_GRAY="#BBBBBB", GRAY="#888888", DARK_GRAY="#444444", DARKER_GRAY="#222222",
|
|
571
|
+
PURE_RED="#FF0000", PURE_GREEN="#00FF00", PURE_BLUE="#0000FF",
|
|
572
|
+
PURE_CYAN="#00FFFF", PURE_MAGENTA="#FF00FF", PURE_YELLOW="#FFFF00",
|
|
573
|
+
BLUE_A="#C7E9F1", BLUE_B="#9CDCEB", BLUE_C="#58C4DD", BLUE_D="#29ABCA", BLUE_E="#236B8E",
|
|
574
|
+
BLUE="#58C4DD", DARK_BLUE="#236B8E",
|
|
575
|
+
TEAL_A="#ACEAD7", TEAL_B="#76DDC0", TEAL_C="#5CD0B3", TEAL_D="#55C1A7", TEAL_E="#49A88F", TEAL="#5CD0B3",
|
|
576
|
+
GREEN_A="#C9E2AE", GREEN_B="#A6CF8C", GREEN_C="#83C167", GREEN_D="#77B05D", GREEN_E="#699C52",
|
|
577
|
+
GREEN="#83C167",
|
|
578
|
+
YELLOW_A="#FFF1B6", YELLOW_B="#FFEA94", YELLOW_C="#F7D96F", YELLOW_D="#F4D345", YELLOW_E="#E8C11C",
|
|
579
|
+
YELLOW="#F7D96F",
|
|
580
|
+
GOLD_A="#F7C797", GOLD_B="#F9B775", GOLD_C="#F0AC5F", GOLD_D="#E1A158", GOLD_E="#C78D46", GOLD="#F0AC5F",
|
|
581
|
+
RED_A="#F7A1A3", RED_B="#FF8080", RED_C="#FC6255", RED_D="#E65A4C", RED_E="#CF5044", RED="#FC6255",
|
|
582
|
+
MAROON_A="#ECABC1", MAROON_B="#EC92AB", MAROON_C="#C55F73", MAROON_D="#A24D61", MAROON_E="#94424F",
|
|
583
|
+
MAROON="#C55F73",
|
|
584
|
+
PURPLE_A="#CAA3E8", PURPLE_B="#B189C6", PURPLE_C="#9A72AC", PURPLE_D="#715582", PURPLE_E="#644172",
|
|
585
|
+
PURPLE="#9A72AC",
|
|
586
|
+
PINK="#D147BD", LIGHT_PINK="#DC75CD", ORANGE="#FF862F",
|
|
587
|
+
LIGHT_BROWN="#CD853F", DARK_BROWN="#8B4513", GRAY_BROWN="#736357",
|
|
588
|
+
LOGO_WHITE="#ECE7E2", LOGO_GREEN="#87C2A5", LOGO_BLUE="#525893", LOGO_RED="#E07A5F", LOGO_BLACK="#343434",
|
|
589
|
+
)
|
|
590
|
+
for _k in list(_COLORS):
|
|
591
|
+
if "GRAY" in _k:
|
|
592
|
+
_COLORS[_k.replace("GRAY", "GREY")] = _COLORS[_k]
|
|
593
|
+
globals().update(_COLORS)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _color(c):
|
|
597
|
+
if c is None:
|
|
598
|
+
return None
|
|
599
|
+
if isinstance(c, str):
|
|
600
|
+
return _COLORS.get(c.upper(), c) if not c.startswith("#") else c
|
|
601
|
+
if isinstance(c, (list, tuple)) and c and not isinstance(c[0], (int, float)):
|
|
602
|
+
return [_color(x) for x in c]
|
|
603
|
+
if isinstance(c, (list, tuple)) and len(c) in (3, 4):
|
|
604
|
+
r, g, b = (max(0, min(255, int(round(float(x) * 255)))) for x in c[:3])
|
|
605
|
+
return "#%02X%02X%02X" % (r, g, b)
|
|
606
|
+
return str(c)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _rgb(c):
|
|
610
|
+
c = _color(c)
|
|
611
|
+
if not (isinstance(c, str) and c.startswith("#") and len(c) == 7):
|
|
612
|
+
raise ValueError("expected a colour like '#FC6255', got %r" % (c,))
|
|
613
|
+
return [int(c[i:i + 2], 16) / 255.0 for i in (1, 3, 5)]
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def rgb_to_color(rgb):
|
|
617
|
+
return _color(list(rgb))
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def color_to_rgb(color):
|
|
621
|
+
return _rgb(color)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def interpolate_color(a, b, alpha):
|
|
625
|
+
ra, rb = _rgb(a), _rgb(b)
|
|
626
|
+
return rgb_to_color([x + (y - x) * alpha for x, y in zip(ra, rb)])
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def color_gradient(colors, length):
|
|
630
|
+
colors = [_color(c) for c in colors]
|
|
631
|
+
if length <= 1 or len(colors) == 1:
|
|
632
|
+
return colors[:1] * max(1, length)
|
|
633
|
+
out = []
|
|
634
|
+
for i in range(length):
|
|
635
|
+
t = i / (length - 1) * (len(colors) - 1)
|
|
636
|
+
k = min(int(t), len(colors) - 2)
|
|
637
|
+
out.append(interpolate_color(colors[k], colors[k + 1], t - k))
|
|
638
|
+
return out
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def average_color(*colors):
|
|
642
|
+
rgbs = [_rgb(c) for c in colors]
|
|
643
|
+
return rgb_to_color([sum(x) / len(rgbs) for x in zip(*rgbs)])
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def ManimColor(value):
|
|
647
|
+
return _color(value)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ---------------------------------------------------------------- rate functions
|
|
651
|
+
|
|
652
|
+
class _Rate:
|
|
653
|
+
"""A named rate function: callable here, sent to the engine by name."""
|
|
654
|
+
__slots__ = ("name", "fn")
|
|
655
|
+
|
|
656
|
+
def __init__(self, name, fn):
|
|
657
|
+
self.name = name
|
|
658
|
+
self.fn = fn
|
|
659
|
+
|
|
660
|
+
def __call__(self, t, *a, **k):
|
|
661
|
+
return self.fn(t, *a, **k)
|
|
662
|
+
|
|
663
|
+
def __repr__(self):
|
|
664
|
+
return "<rate function %s>" % self.name
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _rate(v):
|
|
668
|
+
"""A rate_func argument: its name, or 65 samples of a Python function."""
|
|
669
|
+
if v is None:
|
|
670
|
+
return None
|
|
671
|
+
if isinstance(v, _Rate):
|
|
672
|
+
return v.name
|
|
673
|
+
if isinstance(v, str):
|
|
674
|
+
return v
|
|
675
|
+
if callable(v):
|
|
676
|
+
return {"samples": [_num(v(i / 64.0)) for i in range(65)]}
|
|
677
|
+
raise TypeError("rate_func must be a rate function such as smooth or linear, got %r" % (v,))
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _sigmoid(x):
|
|
681
|
+
return 1.0 / (1.0 + math.exp(-x))
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _smooth(t, inflection=10.0):
|
|
685
|
+
e = _sigmoid(-inflection / 2.0)
|
|
686
|
+
return min(max((_sigmoid(inflection * (t - 0.5)) - e) / (1 - 2 * e), 0.0), 1.0)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _there_and_back(t, inflection=10.0):
|
|
690
|
+
return _smooth(2 * t if t < 0.5 else 2 * (1 - t), inflection)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _there_and_back_with_pause(t, pause_ratio=1.0 / 3):
|
|
694
|
+
a = 1.0 / pause_ratio
|
|
695
|
+
if t < 0.5 - pause_ratio / 2:
|
|
696
|
+
return _smooth(a * t)
|
|
697
|
+
if t < 0.5 + pause_ratio / 2:
|
|
698
|
+
return 1.0
|
|
699
|
+
return _smooth(a - a * t)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _running_start(t, pull_factor=-0.5):
|
|
703
|
+
# A cubic Bezier through 0, 0, pull, pull, 1, 1, 1 evaluated at t.
|
|
704
|
+
pts = [0, 0, pull_factor, pull_factor, 1, 1, 1]
|
|
705
|
+
n = len(pts) - 1
|
|
706
|
+
return sum(math.comb(n, k) * (1 - t) ** (n - k) * t ** k * p for k, p in enumerate(pts))
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _bounce_out(t):
|
|
710
|
+
n, d = 7.5625, 2.75
|
|
711
|
+
if t < 1 / d:
|
|
712
|
+
return n * t * t
|
|
713
|
+
if t < 2 / d:
|
|
714
|
+
t -= 1.5 / d
|
|
715
|
+
return n * t * t + 0.75
|
|
716
|
+
if t < 2.5 / d:
|
|
717
|
+
t -= 2.25 / d
|
|
718
|
+
return n * t * t + 0.9375
|
|
719
|
+
t -= 2.625 / d
|
|
720
|
+
return n * t * t + 0.984375
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
_c1 = 1.70158
|
|
724
|
+
_c2 = _c1 * 1.525
|
|
725
|
+
_c3 = _c1 + 1
|
|
726
|
+
_c4 = TAU / 3
|
|
727
|
+
_c5 = TAU / 4.5
|
|
728
|
+
|
|
729
|
+
_RATES = dict(
|
|
730
|
+
linear=lambda t: t,
|
|
731
|
+
smooth=_smooth,
|
|
732
|
+
smoothstep=lambda t: 0.0 if t <= 0 else (1.0 if t >= 1 else 3 * t ** 2 - 2 * t ** 3),
|
|
733
|
+
smootherstep=lambda t: 0.0 if t <= 0 else (1.0 if t >= 1 else 6 * t ** 5 - 15 * t ** 4 + 10 * t ** 3),
|
|
734
|
+
smoothererstep=lambda t: 0.0 if t <= 0 else (1.0 if t >= 1 else
|
|
735
|
+
35 * t ** 4 - 84 * t ** 5 + 70 * t ** 6 - 20 * t ** 7),
|
|
736
|
+
rush_into=lambda t, inflection=10.0: 2 * _smooth(t / 2.0, inflection),
|
|
737
|
+
rush_from=lambda t, inflection=10.0: 2 * _smooth(t / 2.0 + 0.5, inflection) - 1,
|
|
738
|
+
slow_into=lambda t: math.sqrt(max(0.0, 1 - (1 - t) * (1 - t))),
|
|
739
|
+
double_smooth=lambda t: 0.5 * _smooth(2 * t) if t < 0.5 else 0.5 * (1 + _smooth(2 * t - 1)),
|
|
740
|
+
there_and_back=_there_and_back,
|
|
741
|
+
there_and_back_with_pause=_there_and_back_with_pause,
|
|
742
|
+
running_start=_running_start,
|
|
743
|
+
wiggle=lambda t, wiggles=2: _there_and_back(t) * math.sin(wiggles * PI * t),
|
|
744
|
+
lingering=lambda t: _smooth(min(1.0, t / 0.8)),
|
|
745
|
+
exponential_decay=lambda t, half_life=0.1: 1 - math.exp(-t / half_life),
|
|
746
|
+
ease_in_sine=lambda t: 1 - math.cos(t * PI / 2),
|
|
747
|
+
ease_out_sine=lambda t: math.sin(t * PI / 2),
|
|
748
|
+
ease_in_out_sine=lambda t: -(math.cos(PI * t) - 1) / 2,
|
|
749
|
+
ease_in_quad=lambda t: t * t,
|
|
750
|
+
ease_out_quad=lambda t: 1 - (1 - t) * (1 - t),
|
|
751
|
+
ease_in_out_quad=lambda t: 2 * t * t if t < 0.5 else 1 - (-2 * t + 2) ** 2 / 2,
|
|
752
|
+
ease_in_cubic=lambda t: t ** 3,
|
|
753
|
+
ease_out_cubic=lambda t: 1 - (1 - t) ** 3,
|
|
754
|
+
ease_in_out_cubic=lambda t: 4 * t ** 3 if t < 0.5 else 1 - (-2 * t + 2) ** 3 / 2,
|
|
755
|
+
ease_in_quart=lambda t: t ** 4,
|
|
756
|
+
ease_out_quart=lambda t: 1 - (1 - t) ** 4,
|
|
757
|
+
ease_in_out_quart=lambda t: 8 * t ** 4 if t < 0.5 else 1 - (-2 * t + 2) ** 4 / 2,
|
|
758
|
+
ease_in_quint=lambda t: t ** 5,
|
|
759
|
+
ease_out_quint=lambda t: 1 - (1 - t) ** 5,
|
|
760
|
+
ease_in_out_quint=lambda t: 16 * t ** 5 if t < 0.5 else 1 - (-2 * t + 2) ** 5 / 2,
|
|
761
|
+
ease_in_expo=lambda t: 0.0 if t == 0 else 2 ** (10 * t - 10),
|
|
762
|
+
ease_out_expo=lambda t: 1.0 if t == 1 else 1 - 2 ** (-10 * t),
|
|
763
|
+
ease_in_out_expo=lambda t: (0.0 if t == 0 else 1.0 if t == 1 else
|
|
764
|
+
2 ** (20 * t - 10) / 2 if t < 0.5 else (2 - 2 ** (-20 * t + 10)) / 2),
|
|
765
|
+
ease_in_circ=lambda t: 1 - math.sqrt(max(0.0, 1 - t * t)),
|
|
766
|
+
ease_out_circ=lambda t: math.sqrt(max(0.0, 1 - (t - 1) ** 2)),
|
|
767
|
+
ease_in_out_circ=lambda t: ((1 - math.sqrt(max(0.0, 1 - (2 * t) ** 2))) / 2 if t < 0.5 else
|
|
768
|
+
(math.sqrt(max(0.0, 1 - (-2 * t + 2) ** 2)) + 1) / 2),
|
|
769
|
+
ease_in_back=lambda t: _c3 * t ** 3 - _c1 * t * t,
|
|
770
|
+
ease_out_back=lambda t: 1 + _c3 * (t - 1) ** 3 + _c1 * (t - 1) ** 2,
|
|
771
|
+
ease_in_out_back=lambda t: (((2 * t) ** 2 * ((_c2 + 1) * 2 * t - _c2)) / 2 if t < 0.5 else
|
|
772
|
+
((2 * t - 2) ** 2 * ((_c2 + 1) * (t * 2 - 2) + _c2) + 2) / 2),
|
|
773
|
+
ease_in_elastic=lambda t: (t if t in (0, 1) else
|
|
774
|
+
-(2 ** (10 * t - 10)) * math.sin((t * 10 - 10.75) * _c4)),
|
|
775
|
+
ease_out_elastic=lambda t: (t if t in (0, 1) else
|
|
776
|
+
2 ** (-10 * t) * math.sin((t * 10 - 0.75) * _c4) + 1),
|
|
777
|
+
ease_in_out_elastic=lambda t: (t if t in (0, 1) else
|
|
778
|
+
-(2 ** (20 * t - 10) * math.sin((20 * t - 11.125) * _c5)) / 2 if t < 0.5 else
|
|
779
|
+
(2 ** (-20 * t + 10) * math.sin((20 * t - 11.125) * _c5)) / 2 + 1),
|
|
780
|
+
ease_in_bounce=lambda t: 1 - _bounce_out(1 - t),
|
|
781
|
+
ease_out_bounce=_bounce_out,
|
|
782
|
+
ease_in_out_bounce=lambda t: ((1 - _bounce_out(1 - 2 * t)) / 2 if t < 0.5 else
|
|
783
|
+
(1 + _bounce_out(2 * t - 1)) / 2),
|
|
784
|
+
)
|
|
785
|
+
rate_functions = types.SimpleNamespace()
|
|
786
|
+
for _k, _f in _RATES.items():
|
|
787
|
+
globals()[_k] = _r = _Rate(_k, _f)
|
|
788
|
+
setattr(rate_functions, _k, _r)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
# ---------------------------------------------------------------- config
|
|
792
|
+
|
|
793
|
+
def _frame_size():
|
|
794
|
+
cfg = _st().cfg
|
|
795
|
+
h = cfg.get("frame_height", 8.0)
|
|
796
|
+
return cfg.get("frame_width", h * 16.0 / 9.0), h
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
class _Config:
|
|
800
|
+
"""config(background=…, frame_height=…) or config.background_color = …"""
|
|
801
|
+
_ENGINE = {"background": "background", "background_color": "background",
|
|
802
|
+
"frame_height": "frame_height", "frame_width": "frame_width"}
|
|
803
|
+
_DEFAULTS = {"background_color": BLACK, "frame_height": 8.0, "pixel_width": 1920,
|
|
804
|
+
"pixel_height": 1080, "frame_rate": 60, "transition": "none", "transition_time": 0.5}
|
|
805
|
+
|
|
806
|
+
def __call__(self, **kw):
|
|
807
|
+
for k, v in kw.items():
|
|
808
|
+
self[k] = v
|
|
809
|
+
return self
|
|
810
|
+
|
|
811
|
+
def __setitem__(self, k, v):
|
|
812
|
+
st = _st()
|
|
813
|
+
if k == "background":
|
|
814
|
+
k = "background_color"
|
|
815
|
+
st.cfg[k] = v
|
|
816
|
+
key = self._ENGINE.get(k)
|
|
817
|
+
if key is not None:
|
|
818
|
+
_call("config", {key: _color(v) if key == "background" else _num(v)})
|
|
819
|
+
|
|
820
|
+
def __getitem__(self, k):
|
|
821
|
+
cfg = _st().cfg
|
|
822
|
+
if k in cfg:
|
|
823
|
+
return cfg[k]
|
|
824
|
+
w, h = _frame_size()
|
|
825
|
+
extra = {"frame_width": w, "frame_x_radius": w / 2, "frame_y_radius": h / 2,
|
|
826
|
+
"background": cfg.get("background_color", BLACK)}
|
|
827
|
+
if k in extra:
|
|
828
|
+
return extra[k]
|
|
829
|
+
return self._DEFAULTS.get(k)
|
|
830
|
+
|
|
831
|
+
def __setattr__(self, k, v):
|
|
832
|
+
self[k] = v
|
|
833
|
+
|
|
834
|
+
def __getattr__(self, k):
|
|
835
|
+
if k.startswith("__"):
|
|
836
|
+
raise AttributeError(k)
|
|
837
|
+
return self[k]
|
|
838
|
+
|
|
839
|
+
def __repr__(self):
|
|
840
|
+
return "config(%s)" % ", ".join("%s=%r" % kv for kv in _st().cfg.items())
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
config = _Config()
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
# ---------------------------------------------------------------- mobjects
|
|
847
|
+
|
|
848
|
+
_STYLE = ("color", "fill_color", "fill_opacity", "stroke_color", "stroke_width",
|
|
849
|
+
"stroke_opacity", "opacity", "z_index", "background_stroke_color",
|
|
850
|
+
"background_stroke_width", "sheen_factor")
|
|
851
|
+
_VSTYLE = {"color": WHITE, "fill_opacity": 0.0, "stroke_width": DEFAULT_STROKE_WIDTH}
|
|
852
|
+
_FILLED = {"color": WHITE, "fill_opacity": 1.0, "stroke_width": 0}
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _flat(items):
|
|
856
|
+
out = []
|
|
857
|
+
for m in items:
|
|
858
|
+
if isinstance(m, Mobject):
|
|
859
|
+
out.append(m)
|
|
860
|
+
elif isinstance(m, (list, tuple, types.GeneratorType)):
|
|
861
|
+
out.extend(_flat(m))
|
|
862
|
+
else:
|
|
863
|
+
raise TypeError("expected mobjects, such as Circle() or Text('Hi'), got %r" % (m,))
|
|
864
|
+
return out
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
class Mobject:
|
|
868
|
+
"""Anything on the stage. Methods return the mobject, so they chain."""
|
|
869
|
+
|
|
870
|
+
def __init__(self, **kwargs):
|
|
871
|
+
self._make("group", {}, kwargs, {})
|
|
872
|
+
|
|
873
|
+
# -- identity
|
|
874
|
+
|
|
875
|
+
def _make(self, kind, geom, kwargs, style=_VSTYLE, n=None, kids=None, extra=()):
|
|
876
|
+
st = _st()
|
|
877
|
+
self._path = None
|
|
878
|
+
self._kids = kids
|
|
879
|
+
self._n = n
|
|
880
|
+
self._tmpl = None
|
|
881
|
+
props = dict(style)
|
|
882
|
+
for k, v in kwargs.items():
|
|
883
|
+
if v is None or (callable(v) and not isinstance(v, (_Rate, type))):
|
|
884
|
+
continue
|
|
885
|
+
props[k] = _color(v) if k.endswith("color") and not isinstance(v, Expr) else v
|
|
886
|
+
props.update(geom)
|
|
887
|
+
self.z_index = props.get("z_index", 0)
|
|
888
|
+
tr = st.trace
|
|
889
|
+
if tr is not None:
|
|
890
|
+
# Made inside an updater: a TEMPLATE the engine builds every frame.
|
|
891
|
+
tr.next_key += 1
|
|
892
|
+
self._id = None
|
|
893
|
+
self._tmpl = {"key": tr.next_key, "kind": kind, "props": props, "ops": []}
|
|
894
|
+
return
|
|
895
|
+
self._id = st.next_id
|
|
896
|
+
st.next_id += 1
|
|
897
|
+
st.objects[self._id] = self
|
|
898
|
+
_call("make", self._id, kind, props, *extra)
|
|
899
|
+
|
|
900
|
+
def _t(self):
|
|
901
|
+
"""This object as the target of a command."""
|
|
902
|
+
tm = self.__dict__.get("_tmpl")
|
|
903
|
+
if tm is not None:
|
|
904
|
+
return {"tmpl": tm["key"]}
|
|
905
|
+
return self._id if self._path is None else {"id": self._id, "path": list(self._path)}
|
|
906
|
+
|
|
907
|
+
_ref = _t
|
|
908
|
+
|
|
909
|
+
def _embed(self):
|
|
910
|
+
"""This object as an argument: a template travels whole."""
|
|
911
|
+
tm = self.__dict__.get("_tmpl")
|
|
912
|
+
return tm if tm is not None else self._t()
|
|
913
|
+
|
|
914
|
+
def _op(self, name, args):
|
|
915
|
+
st = _st()
|
|
916
|
+
tm = self.__dict__.get("_tmpl")
|
|
917
|
+
if tm is not None:
|
|
918
|
+
tm["ops"].append([name, args])
|
|
919
|
+
return self
|
|
920
|
+
cap = st.capture
|
|
921
|
+
if cap is not None:
|
|
922
|
+
op = [name, args]
|
|
923
|
+
if cap[0] != self._t():
|
|
924
|
+
op.append(self._t())
|
|
925
|
+
cap[1].append(op)
|
|
926
|
+
return self
|
|
927
|
+
tr = st.trace
|
|
928
|
+
if tr is not None:
|
|
929
|
+
tr.ops.append([name, args, self._t()])
|
|
930
|
+
return self
|
|
931
|
+
if _live_phase():
|
|
932
|
+
# From a Pyctures loop: a change to the live scene, on top of the timeline.
|
|
933
|
+
_live_op(name, args, self._t())
|
|
934
|
+
return self
|
|
935
|
+
_call("op", self._t(), name, args)
|
|
936
|
+
return self
|
|
937
|
+
|
|
938
|
+
@property
|
|
939
|
+
def animate(self):
|
|
940
|
+
return _Animate(self)
|
|
941
|
+
|
|
942
|
+
def __bool__(self):
|
|
943
|
+
return True
|
|
944
|
+
|
|
945
|
+
def __repr__(self):
|
|
946
|
+
return "<%s #%s>" % (type(self).__name__, self._t())
|
|
947
|
+
|
|
948
|
+
# -- parts
|
|
949
|
+
|
|
950
|
+
def _count(self):
|
|
951
|
+
if self._kids is not None:
|
|
952
|
+
return len(self._kids)
|
|
953
|
+
return self._n
|
|
954
|
+
|
|
955
|
+
def __len__(self):
|
|
956
|
+
n = self._count()
|
|
957
|
+
if n is None:
|
|
958
|
+
raise TypeError("the parts of a %s are only known in the browser; index them "
|
|
959
|
+
"directly, such as m[0]" % type(self).__name__)
|
|
960
|
+
return n
|
|
961
|
+
|
|
962
|
+
def __iter__(self):
|
|
963
|
+
return iter([self[i] for i in range(len(self))])
|
|
964
|
+
|
|
965
|
+
def __getitem__(self, i):
|
|
966
|
+
if isinstance(i, slice):
|
|
967
|
+
return VGroup(*[self[k] for k in range(len(self))[i]])
|
|
968
|
+
if self._kids is not None:
|
|
969
|
+
return self._kids[i]
|
|
970
|
+
i = int(i)
|
|
971
|
+
if i < 0:
|
|
972
|
+
i += len(self)
|
|
973
|
+
if self._n is not None and not 0 <= i < self._n:
|
|
974
|
+
raise IndexError("%s has %d parts; there is no part %d" % (type(self).__name__, self._n, i))
|
|
975
|
+
return _Part(self, i)
|
|
976
|
+
|
|
977
|
+
@property
|
|
978
|
+
def submobjects(self):
|
|
979
|
+
return list(self)
|
|
980
|
+
|
|
981
|
+
def copy(self):
|
|
982
|
+
st = _st()
|
|
983
|
+
m = object.__new__(_Copy if isinstance(self, _Part) else type(self))
|
|
984
|
+
m.__dict__.update(self.__dict__)
|
|
985
|
+
m.__dict__.pop("updaters", None)
|
|
986
|
+
tr = st.trace
|
|
987
|
+
if tr is not None:
|
|
988
|
+
# Inside an updater: a template that copies the object every frame.
|
|
989
|
+
tr.next_key += 1
|
|
990
|
+
m._id, m._path, m._ptmpl = None, None, None
|
|
991
|
+
m._tmpl = {"key": tr.next_key, "kind": "copy", "props": {"mobject": self._embed()}, "ops": []}
|
|
992
|
+
return m
|
|
993
|
+
m._id = st.next_id
|
|
994
|
+
st.next_id += 1
|
|
995
|
+
m._path = None
|
|
996
|
+
if self._kids is not None:
|
|
997
|
+
m._n, m._kids = len(self._kids), None
|
|
998
|
+
_call("copy", m._id, self._t())
|
|
999
|
+
return m
|
|
1000
|
+
|
|
1001
|
+
def generate_target(self):
|
|
1002
|
+
self.target = self.copy()
|
|
1003
|
+
return self.target
|
|
1004
|
+
|
|
1005
|
+
def get_family(self):
|
|
1006
|
+
return [self] + [f for k in (self._kids or ()) for f in k.get_family()]
|
|
1007
|
+
|
|
1008
|
+
# -- reading back (lazy)
|
|
1009
|
+
|
|
1010
|
+
def get_center(self):
|
|
1011
|
+
return Point(["of", self._ref(), "center"])
|
|
1012
|
+
|
|
1013
|
+
def get_top(self):
|
|
1014
|
+
return Point(["of", self._ref(), "top"])
|
|
1015
|
+
|
|
1016
|
+
def get_bottom(self):
|
|
1017
|
+
return Point(["of", self._ref(), "bottom"])
|
|
1018
|
+
|
|
1019
|
+
def get_left(self):
|
|
1020
|
+
return Point(["of", self._ref(), "left"])
|
|
1021
|
+
|
|
1022
|
+
def get_right(self):
|
|
1023
|
+
return Point(["of", self._ref(), "right"])
|
|
1024
|
+
|
|
1025
|
+
def get_start(self):
|
|
1026
|
+
return Point(["of", self._ref(), "start"])
|
|
1027
|
+
|
|
1028
|
+
def get_end(self):
|
|
1029
|
+
return Point(["of", self._ref(), "end"])
|
|
1030
|
+
|
|
1031
|
+
def get_corner(self, direction):
|
|
1032
|
+
return Point(["of", self._ref(), _anchor(direction)])
|
|
1033
|
+
|
|
1034
|
+
get_critical_point = get_edge_center = get_boundary_point = get_corner
|
|
1035
|
+
get_center_of_mass = get_center
|
|
1036
|
+
|
|
1037
|
+
def get_x(self):
|
|
1038
|
+
return Expr(["get", self._ref(), "x"], "s")
|
|
1039
|
+
|
|
1040
|
+
def get_y(self):
|
|
1041
|
+
return Expr(["get", self._ref(), "y"], "s")
|
|
1042
|
+
|
|
1043
|
+
def get_z(self):
|
|
1044
|
+
return Expr(["get", self._ref(), "z"], "s")
|
|
1045
|
+
|
|
1046
|
+
def get_width(self):
|
|
1047
|
+
return Expr(["get", self._ref(), "width"], "s")
|
|
1048
|
+
|
|
1049
|
+
def get_height(self):
|
|
1050
|
+
return Expr(["get", self._ref(), "height"], "s")
|
|
1051
|
+
|
|
1052
|
+
def get_color(self):
|
|
1053
|
+
return Expr(["get", self._ref(), "color"], "s")
|
|
1054
|
+
|
|
1055
|
+
@property
|
|
1056
|
+
def width(self):
|
|
1057
|
+
return self.get_width()
|
|
1058
|
+
|
|
1059
|
+
@width.setter
|
|
1060
|
+
def width(self, value):
|
|
1061
|
+
self.scale_to_fit_width(value)
|
|
1062
|
+
|
|
1063
|
+
@property
|
|
1064
|
+
def height(self):
|
|
1065
|
+
return self.get_height()
|
|
1066
|
+
|
|
1067
|
+
@height.setter
|
|
1068
|
+
def height(self, value):
|
|
1069
|
+
self.scale_to_fit_height(value)
|
|
1070
|
+
|
|
1071
|
+
# -- placement
|
|
1072
|
+
|
|
1073
|
+
def shift(self, *vectors):
|
|
1074
|
+
if not vectors:
|
|
1075
|
+
return self
|
|
1076
|
+
total = vectors[0]
|
|
1077
|
+
for v in vectors[1:]:
|
|
1078
|
+
total = (total + v) if _is_sym(total) or _is_sym(v) else Vec.of(total) + v
|
|
1079
|
+
return self._op("shift", {"vectors": [_P(total)]})
|
|
1080
|
+
|
|
1081
|
+
def move_to(self, point_or_mobject, aligned_edge=ORIGIN, coor_mask=(1, 1, 1)):
|
|
1082
|
+
return self._op("move_to", {"point_or_mobject": _MP(point_or_mobject), "aligned_edge": _P(aligned_edge),
|
|
1083
|
+
"coor_mask": _P(coor_mask)})
|
|
1084
|
+
|
|
1085
|
+
def center(self):
|
|
1086
|
+
return self.move_to(ORIGIN)
|
|
1087
|
+
|
|
1088
|
+
def next_to(self, mobject_or_point, direction=RIGHT, buff=DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
|
|
1089
|
+
aligned_edge=ORIGIN, **kwargs):
|
|
1090
|
+
return self._op("next_to", {"mobject_or_point": _MP(mobject_or_point), "direction": _P(direction),
|
|
1091
|
+
"buff": _num(buff), "aligned_edge": _P(aligned_edge)})
|
|
1092
|
+
|
|
1093
|
+
def to_edge(self, edge=LEFT, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
|
|
1094
|
+
return self._op("to_edge", {"edge": _P(edge), "buff": _num(buff)})
|
|
1095
|
+
|
|
1096
|
+
def to_corner(self, corner=DL, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
|
|
1097
|
+
return self._op("to_corner", {"corner": _P(corner), "buff": _num(buff)})
|
|
1098
|
+
|
|
1099
|
+
def align_to(self, mobject_or_point, direction=ORIGIN, **kwargs):
|
|
1100
|
+
return self._op("align_to", {"mobject_or_point": _MP(mobject_or_point), "direction": _P(direction)})
|
|
1101
|
+
|
|
1102
|
+
def arrange(self, direction=RIGHT, buff=DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, center=True,
|
|
1103
|
+
aligned_edge=ORIGIN, **kwargs):
|
|
1104
|
+
return self._op("arrange", {"direction": _P(direction), "buff": _num(buff), "center": bool(center),
|
|
1105
|
+
"aligned_edge": _P(aligned_edge)})
|
|
1106
|
+
|
|
1107
|
+
def arrange_in_grid(self, rows=None, cols=None, buff=MED_SMALL_BUFF, cell_alignment=ORIGIN,
|
|
1108
|
+
flow_order="rd", **kwargs):
|
|
1109
|
+
b = [_num(x) for x in buff] if isinstance(buff, (list, tuple)) else _num(buff)
|
|
1110
|
+
return self._op("arrange_in_grid", {"rows": rows, "cols": cols, "buff": b,
|
|
1111
|
+
"cell_alignment": _P(cell_alignment), "flow_order": flow_order})
|
|
1112
|
+
|
|
1113
|
+
def scale(self, scale_factor, about_point=None, about_edge=None, **kwargs):
|
|
1114
|
+
return self._op("scale", {"scale_factor": _num(scale_factor), "about_point": _P(about_point),
|
|
1115
|
+
"about_edge": _P(about_edge)})
|
|
1116
|
+
|
|
1117
|
+
def rotate(self, angle, axis=OUT, about_point=None, **kwargs):
|
|
1118
|
+
return self._op("rotate", {"angle": _num(angle), "axis": _P(axis), "about_point": _P(about_point)})
|
|
1119
|
+
|
|
1120
|
+
def flip(self, axis=UP, **kwargs):
|
|
1121
|
+
return self._op("flip", {"axis": _P(axis)})
|
|
1122
|
+
|
|
1123
|
+
def stretch(self, factor, dim, **kwargs):
|
|
1124
|
+
return self._op("stretch", {"factor": _num(factor), "dim": int(dim)})
|
|
1125
|
+
|
|
1126
|
+
def set_x(self, x, direction=ORIGIN):
|
|
1127
|
+
return self._op("set_x", {"x": _num(x)})
|
|
1128
|
+
|
|
1129
|
+
def set_y(self, y, direction=ORIGIN):
|
|
1130
|
+
return self._op("set_y", {"y": _num(y)})
|
|
1131
|
+
|
|
1132
|
+
def set_z(self, z, direction=ORIGIN):
|
|
1133
|
+
return self._op("set_z", {"z": _num(z)})
|
|
1134
|
+
|
|
1135
|
+
def scale_to_fit_width(self, width, **kwargs):
|
|
1136
|
+
return self._op("scale_to_fit_width", {"width": _num(width)})
|
|
1137
|
+
|
|
1138
|
+
def scale_to_fit_height(self, height, **kwargs):
|
|
1139
|
+
return self._op("scale_to_fit_height", {"height": _num(height)})
|
|
1140
|
+
|
|
1141
|
+
def stretch_to_fit_width(self, width, **kwargs):
|
|
1142
|
+
return self._op("stretch_to_fit_width", {"width": _num(width)})
|
|
1143
|
+
|
|
1144
|
+
def stretch_to_fit_height(self, height, **kwargs):
|
|
1145
|
+
return self._op("stretch_to_fit_height", {"height": _num(height)})
|
|
1146
|
+
|
|
1147
|
+
set_width, set_height = scale_to_fit_width, scale_to_fit_height
|
|
1148
|
+
|
|
1149
|
+
def match_width(self, mobject, **kwargs):
|
|
1150
|
+
return self._op("match_width", {"mobject": mobject._embed()})
|
|
1151
|
+
|
|
1152
|
+
def match_height(self, mobject, **kwargs):
|
|
1153
|
+
return self._op("match_height", {"mobject": mobject._embed()})
|
|
1154
|
+
|
|
1155
|
+
# -- style
|
|
1156
|
+
|
|
1157
|
+
def set_color(self, color=YELLOW_C, family=True):
|
|
1158
|
+
return self._op("set_color", {"color": _color(color)})
|
|
1159
|
+
|
|
1160
|
+
def set_color_by_gradient(self, *colors):
|
|
1161
|
+
return self._op("set_color", {"color": [_color(c) for c in _flat_colors(colors)]})
|
|
1162
|
+
|
|
1163
|
+
def set_fill(self, color=None, opacity=None, family=True):
|
|
1164
|
+
args = {}
|
|
1165
|
+
if color is not None:
|
|
1166
|
+
args["color"] = _color(color)
|
|
1167
|
+
if opacity is not None:
|
|
1168
|
+
args["opacity"] = _num(opacity)
|
|
1169
|
+
return self._op("set_fill", args)
|
|
1170
|
+
|
|
1171
|
+
def set_stroke(self, color=None, width=None, opacity=None, background=False, family=True):
|
|
1172
|
+
args = {}
|
|
1173
|
+
if color is not None:
|
|
1174
|
+
args["color"] = _color(color)
|
|
1175
|
+
if width is not None:
|
|
1176
|
+
args["width"] = _num(width)
|
|
1177
|
+
if opacity is not None:
|
|
1178
|
+
args["opacity"] = _num(opacity)
|
|
1179
|
+
return self._op("set_stroke", args)
|
|
1180
|
+
|
|
1181
|
+
def set_opacity(self, opacity, family=True):
|
|
1182
|
+
return self._op("set_opacity", {"opacity": _num(opacity)})
|
|
1183
|
+
|
|
1184
|
+
def fade(self, darkness=0.5, family=True):
|
|
1185
|
+
return self._op("fade", {"darkness": _num(darkness)})
|
|
1186
|
+
|
|
1187
|
+
def set_z_index(self, z_index_value, family=True):
|
|
1188
|
+
self.z_index = z_index_value
|
|
1189
|
+
return self._op("set_z_index", {"z_index": _num(z_index_value)})
|
|
1190
|
+
|
|
1191
|
+
def set(self, **kwargs):
|
|
1192
|
+
for k, v in kwargs.items():
|
|
1193
|
+
if k == "color":
|
|
1194
|
+
self.set_color(v)
|
|
1195
|
+
elif k in ("width", "height"):
|
|
1196
|
+
getattr(self, "scale_to_fit_" + k)(v)
|
|
1197
|
+
elif k == "opacity":
|
|
1198
|
+
self.set_opacity(v)
|
|
1199
|
+
elif k in ("fill_color", "fill_opacity"):
|
|
1200
|
+
self.set_fill(**{k[5:]: v})
|
|
1201
|
+
elif k in ("stroke_color", "stroke_width", "stroke_opacity"):
|
|
1202
|
+
self.set_stroke(**{k[7:]: v})
|
|
1203
|
+
elif k == "z_index":
|
|
1204
|
+
self.set_z_index(v)
|
|
1205
|
+
else:
|
|
1206
|
+
setattr(self, k, v)
|
|
1207
|
+
return self
|
|
1208
|
+
|
|
1209
|
+
def become(self, mobject, **kwargs):
|
|
1210
|
+
self._n = mobject._count()
|
|
1211
|
+
return self._op("become", {"mobject": mobject._embed()})
|
|
1212
|
+
|
|
1213
|
+
def save_state(self):
|
|
1214
|
+
return self._op("save_state", {})
|
|
1215
|
+
|
|
1216
|
+
def restore(self):
|
|
1217
|
+
return self._op("restore", {})
|
|
1218
|
+
|
|
1219
|
+
# -- groups
|
|
1220
|
+
|
|
1221
|
+
def add(self, *mobjects):
|
|
1222
|
+
mobs = _flat(mobjects)
|
|
1223
|
+
if self._kids is not None:
|
|
1224
|
+
self._kids.extend(m for m in mobs if m not in self._kids)
|
|
1225
|
+
return self._op("add", {"mobjects": [m._embed() for m in mobs]})
|
|
1226
|
+
|
|
1227
|
+
def remove(self, *mobjects):
|
|
1228
|
+
mobs = _flat(mobjects)
|
|
1229
|
+
if self._kids is not None:
|
|
1230
|
+
self._kids[:] = [k for k in self._kids if k not in mobs]
|
|
1231
|
+
return self._op("remove", {"mobjects": [m._embed() for m in mobs]})
|
|
1232
|
+
|
|
1233
|
+
# -- not yet
|
|
1234
|
+
|
|
1235
|
+
def add_updater(self, update_function, index=None, call_updater=False):
|
|
1236
|
+
"""Run update_function(m) — or (m, dt) — every frame. It is traced once
|
|
1237
|
+
into a formula the browser runs; if it cannot be, it runs in Python."""
|
|
1238
|
+
_add_updater(self, update_function)
|
|
1239
|
+
return self
|
|
1240
|
+
|
|
1241
|
+
def remove_updater(self, update_function):
|
|
1242
|
+
ups = self.__dict__.get("updaters") or []
|
|
1243
|
+
for entry in list(ups):
|
|
1244
|
+
if entry[0] is update_function:
|
|
1245
|
+
ups.remove(entry)
|
|
1246
|
+
self._stop_updates(entry[1])
|
|
1247
|
+
return self
|
|
1248
|
+
|
|
1249
|
+
def _stop_updates(self, entries):
|
|
1250
|
+
for target, uid in entries:
|
|
1251
|
+
if target is None:
|
|
1252
|
+
uid.active = False # a Python-run updater
|
|
1253
|
+
else:
|
|
1254
|
+
_call("remove_updater", target, uid)
|
|
1255
|
+
|
|
1256
|
+
def clear_updaters(self, recursive=True):
|
|
1257
|
+
for _fn, entries in self.__dict__.get("updaters") or []:
|
|
1258
|
+
for target, uid in entries:
|
|
1259
|
+
if target is None:
|
|
1260
|
+
uid.active = False
|
|
1261
|
+
elif target != self._t():
|
|
1262
|
+
_call("remove_updater", target, uid)
|
|
1263
|
+
self.__dict__["updaters"] = []
|
|
1264
|
+
_call("clear_updaters", self._t())
|
|
1265
|
+
return self
|
|
1266
|
+
|
|
1267
|
+
def suspend_updating(self, recursive=True):
|
|
1268
|
+
_call("suspend_updating", self._t())
|
|
1269
|
+
return self
|
|
1270
|
+
|
|
1271
|
+
def resume_updating(self, recursive=True):
|
|
1272
|
+
_call("resume_updating", self._t())
|
|
1273
|
+
return self
|
|
1274
|
+
|
|
1275
|
+
def set_shadow(self, blur=0.15, offset=(0.05, -0.05), color=BLACK, opacity=0.5):
|
|
1276
|
+
"""A soft shadow (blur and offset in scene units)."""
|
|
1277
|
+
return self._op("set_shadow", {"blur": _num(blur), "offset": [_num(offset[0]), _num(offset[1])],
|
|
1278
|
+
"color": _color(color), "opacity": _num(opacity)})
|
|
1279
|
+
|
|
1280
|
+
def get_updaters(self):
|
|
1281
|
+
return [entry[0] for entry in self.__dict__.get("updaters") or []]
|
|
1282
|
+
|
|
1283
|
+
def has_updaters(self):
|
|
1284
|
+
return bool(self.__dict__.get("updaters"))
|
|
1285
|
+
|
|
1286
|
+
def point_from_proportion(self, alpha):
|
|
1287
|
+
return Point(["get", self._ref(), "point_from_proportion", _coord(alpha)])
|
|
1288
|
+
|
|
1289
|
+
def __getattr__(self, name):
|
|
1290
|
+
if name.startswith("__") or name.startswith("_"):
|
|
1291
|
+
raise AttributeError(name)
|
|
1292
|
+
raise AttributeError("%s has no %s() in Pymations yet" % (type(self).__name__, name))
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _flat_colors(colors):
|
|
1296
|
+
out = []
|
|
1297
|
+
for c in colors:
|
|
1298
|
+
if isinstance(c, (list, tuple)) and c and isinstance(c[0], str):
|
|
1299
|
+
out.extend(c)
|
|
1300
|
+
else:
|
|
1301
|
+
out.append(c)
|
|
1302
|
+
return out
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
class _Part(Mobject):
|
|
1306
|
+
"""A part of a mobject the engine owns (eq[1], text[0], copy[2])."""
|
|
1307
|
+
|
|
1308
|
+
def __init__(self, parent, index):
|
|
1309
|
+
self._id = parent._id
|
|
1310
|
+
self._path = (list(parent._path) if parent._path else []) + [index]
|
|
1311
|
+
self._kids = None
|
|
1312
|
+
self._ptmpl = parent.__dict__.get("_tmpl") or parent.__dict__.get("_ptmpl")
|
|
1313
|
+
parts = parent.__dict__.get("_parts")
|
|
1314
|
+
self._n = None
|
|
1315
|
+
if parts is not None and index < len(parts):
|
|
1316
|
+
self.tex = parts[index]
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _t(self):
|
|
1320
|
+
if self._ptmpl is not None:
|
|
1321
|
+
return {"tmpl": self._ptmpl["key"], "path": list(self._path)}
|
|
1322
|
+
return Mobject._t(self)
|
|
1323
|
+
|
|
1324
|
+
_ref = _embed = _t
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
class _Copy(Mobject):
|
|
1328
|
+
pass
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
class VMobject(Mobject):
|
|
1332
|
+
def __init__(self, fill_color=None, fill_opacity=0.0, stroke_color=None, stroke_width=DEFAULT_STROKE_WIDTH,
|
|
1333
|
+
**kwargs):
|
|
1334
|
+
kwargs.update(fill_color=fill_color, fill_opacity=fill_opacity, stroke_color=stroke_color,
|
|
1335
|
+
stroke_width=stroke_width)
|
|
1336
|
+
self._make("vmobject", {"points": [], "smooth": False}, kwargs, n=0)
|
|
1337
|
+
|
|
1338
|
+
def set_points_as_corners(self, points):
|
|
1339
|
+
return self._op("set_points", {"points": [_P(p) for p in points], "smooth": False})
|
|
1340
|
+
|
|
1341
|
+
def set_points_smoothly(self, points):
|
|
1342
|
+
return self._op("set_points", {"points": [_P(p) for p in points], "smooth": True})
|
|
1343
|
+
|
|
1344
|
+
def get_length(self):
|
|
1345
|
+
return Expr(["get", self._ref(), "length"], "s")
|
|
1346
|
+
|
|
1347
|
+
def get_angle(self):
|
|
1348
|
+
return Expr(["get", self._ref(), "angle"], "s")
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
class Group(Mobject):
|
|
1352
|
+
_kind = "group"
|
|
1353
|
+
|
|
1354
|
+
def __init__(self, *mobjects, **kwargs):
|
|
1355
|
+
kids = _flat(mobjects)
|
|
1356
|
+
self._make(self._kind, {"children": [m._embed() for m in kids]}, kwargs, {}, kids=kids)
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
class VGroup(Group, VMobject):
|
|
1360
|
+
_kind = "group"
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
class Circle(VMobject):
|
|
1364
|
+
def __init__(self, radius=1.0, color=RED, **kwargs):
|
|
1365
|
+
self._make("circle", {"radius": _num(radius)}, dict(kwargs, color=color), n=0)
|
|
1366
|
+
|
|
1367
|
+
def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2):
|
|
1368
|
+
return self._op("surround", {"mobject": mobject._embed(), "buffer_factor": _num(buffer_factor)})
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
class Ellipse(Circle):
|
|
1372
|
+
def __init__(self, width=2.0, height=1.0, color=RED, **kwargs):
|
|
1373
|
+
self._make("ellipse", {"width": _num(width), "height": _num(height)}, dict(kwargs, color=color), n=0)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
class Dot(VMobject):
|
|
1377
|
+
def __init__(self, point=ORIGIN, radius=DEFAULT_DOT_RADIUS, stroke_width=0, fill_opacity=1.0,
|
|
1378
|
+
color=WHITE, **kwargs):
|
|
1379
|
+
self._make("dot", {"point": _P(point), "radius": _num(radius)},
|
|
1380
|
+
dict(kwargs, stroke_width=stroke_width, fill_opacity=fill_opacity, color=color), n=0)
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
class SmallDot(Dot):
|
|
1384
|
+
def __init__(self, radius=DEFAULT_SMALL_DOT_RADIUS, **kwargs):
|
|
1385
|
+
Dot.__init__(self, radius=radius, **kwargs)
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
class Rectangle(VMobject):
|
|
1389
|
+
def __init__(self, color=WHITE, height=2.0, width=4.0, **kwargs):
|
|
1390
|
+
self._make("rectangle", {"width": _num(width), "height": _num(height)}, dict(kwargs, color=color), n=0)
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
class Square(Rectangle):
|
|
1394
|
+
def __init__(self, side_length=2.0, color=WHITE, **kwargs):
|
|
1395
|
+
self._make("square", {"side_length": _num(side_length)}, dict(kwargs, color=color), n=0)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
class RoundedRectangle(Rectangle):
|
|
1399
|
+
def __init__(self, corner_radius=0.5, color=WHITE, height=2.0, width=4.0, **kwargs):
|
|
1400
|
+
self._make("rounded_rectangle", {"width": _num(width), "height": _num(height),
|
|
1401
|
+
"corner_radius": _num(corner_radius)}, dict(kwargs, color=color), n=0)
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
class Line(VMobject):
|
|
1405
|
+
_kind = "line"
|
|
1406
|
+
|
|
1407
|
+
def __init__(self, start=LEFT, end=RIGHT, buff=0, path_arc=None, **kwargs):
|
|
1408
|
+
geom = {"start": _MP(start), "end": _MP(end), "buff": _num(buff),
|
|
1409
|
+
"path_arc": _num(path_arc) if path_arc else 0.0}
|
|
1410
|
+
geom.update(self._extra(kwargs))
|
|
1411
|
+
self._make(self._kind, geom, kwargs, n=0)
|
|
1412
|
+
|
|
1413
|
+
def _extra(self, kwargs):
|
|
1414
|
+
return {}
|
|
1415
|
+
|
|
1416
|
+
def put_start_and_end_on(self, start, end):
|
|
1417
|
+
return self._op("put_start_and_end_on", {"start": _P(start), "end": _P(end)})
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
class DashedLine(Line):
|
|
1421
|
+
_kind = "dashed_line"
|
|
1422
|
+
|
|
1423
|
+
def _extra(self, kwargs):
|
|
1424
|
+
return {"dash_length": _num(kwargs.pop("dash_length", DEFAULT_DASH_LENGTH)),
|
|
1425
|
+
"dashed_ratio": _num(kwargs.pop("dashed_ratio", 0.5))}
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
class Arrow(Line):
|
|
1429
|
+
_kind = "arrow"
|
|
1430
|
+
|
|
1431
|
+
def __init__(self, start=LEFT, end=RIGHT, buff=MED_SMALL_BUFF, stroke_width=6, **kwargs):
|
|
1432
|
+
Line.__init__(self, start, end, buff=buff, stroke_width=stroke_width, **kwargs)
|
|
1433
|
+
|
|
1434
|
+
def _extra(self, kwargs):
|
|
1435
|
+
return {"max_tip_length_to_length_ratio": _num(kwargs.pop("max_tip_length_to_length_ratio", 0.25)),
|
|
1436
|
+
"max_stroke_width_to_length_ratio": _num(kwargs.pop("max_stroke_width_to_length_ratio", 5)),
|
|
1437
|
+
"tip_length": _num(kwargs.pop("tip_length", DEFAULT_ARROW_TIP_LENGTH))}
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
class DoubleArrow(Arrow):
|
|
1441
|
+
_kind = "double_arrow"
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
class Vector(Arrow):
|
|
1445
|
+
def __init__(self, direction=RIGHT, buff=0, **kwargs):
|
|
1446
|
+
Arrow.__init__(self, ORIGIN, direction, buff=buff, **kwargs)
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
class Arc(VMobject):
|
|
1450
|
+
def __init__(self, radius=1.0, start_angle=0, angle=TAU / 4, arc_center=ORIGIN, **kwargs):
|
|
1451
|
+
self._make("arc", {"radius": _num(radius), "start_angle": _num(start_angle), "angle": _num(angle),
|
|
1452
|
+
"arc_center": _P(arc_center)}, kwargs, n=0)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
class Polygon(VMobject):
|
|
1456
|
+
def __init__(self, *vertices, color=BLUE, **kwargs):
|
|
1457
|
+
self._make("polygon", {"vertices": [_P(v) for v in vertices]}, dict(kwargs, color=color), n=0)
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
class RegularPolygon(Polygon):
|
|
1461
|
+
def __init__(self, n=6, radius=1.0, start_angle=None, color=BLUE, **kwargs):
|
|
1462
|
+
if start_angle is None:
|
|
1463
|
+
start_angle = (n % 2) * 90 * DEGREES
|
|
1464
|
+
self._make("regular_polygon", {"n": int(n), "radius": _num(radius), "start_angle": _num(start_angle)},
|
|
1465
|
+
dict(kwargs, color=color), n=0)
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
class Triangle(RegularPolygon):
|
|
1469
|
+
def __init__(self, **kwargs):
|
|
1470
|
+
RegularPolygon.__init__(self, n=3, **kwargs)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
class Star(Polygon):
|
|
1474
|
+
def __init__(self, n=5, outer_radius=1.0, inner_radius=None, density=2, start_angle=TAU / 4,
|
|
1475
|
+
color=BLUE, **kwargs):
|
|
1476
|
+
if inner_radius is None:
|
|
1477
|
+
inner_radius = (outer_radius * math.cos(TAU * density / 2 / n)
|
|
1478
|
+
/ math.cos(TAU * (density - 1) / 2 / n))
|
|
1479
|
+
self._make("star", {"n": int(n), "outer_radius": _num(outer_radius), "inner_radius": _num(inner_radius),
|
|
1480
|
+
"start_angle": _num(start_angle)}, dict(kwargs, color=color), n=0)
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
class Annulus(VMobject):
|
|
1484
|
+
def __init__(self, inner_radius=1, outer_radius=2, fill_opacity=1, stroke_width=0, color=WHITE, **kwargs):
|
|
1485
|
+
self._make("annulus", {"inner_radius": _num(inner_radius), "outer_radius": _num(outer_radius)},
|
|
1486
|
+
dict(kwargs, fill_opacity=fill_opacity, stroke_width=stroke_width, color=color), n=0)
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
class Text(VMobject):
|
|
1490
|
+
def __init__(self, text, font_size=DEFAULT_FONT_SIZE, font="", weight=NORMAL, slant=NORMAL,
|
|
1491
|
+
line_spacing=-1, t2c=None, letter_spacing=0, **kwargs):
|
|
1492
|
+
text = str(text)
|
|
1493
|
+
self.text = text
|
|
1494
|
+
# font: a family name, or a font file beside the .py ("Brand.ttf"), sent as written.
|
|
1495
|
+
geom = {"text": text, "font_size": _num(font_size), "font": font, "weight": weight,
|
|
1496
|
+
"slant": slant, "line_spacing": _num(line_spacing), "letter_spacing": _num(letter_spacing)}
|
|
1497
|
+
if t2c:
|
|
1498
|
+
geom["t2c"] = {str(k): _color(v) for k, v in t2c.items()}
|
|
1499
|
+
self._make("text", geom, kwargs, _FILLED, n=sum(1 for ch in text if not ch.isspace()))
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def _split_tex(strings, isolate):
|
|
1504
|
+
"""Manim's part rule: each argument is a part, {{ … }} marks parts too
|
|
1505
|
+
("{{ a }} + {{ b }}" is a, +, b), and every substring to isolate becomes
|
|
1506
|
+
its own part. Parts are sent trimmed, so get_part_by_tex("a") finds one."""
|
|
1507
|
+
pieces = []
|
|
1508
|
+
for s in strings:
|
|
1509
|
+
s = str(s)
|
|
1510
|
+
if "{{" in s:
|
|
1511
|
+
for chunk in s.replace("}}", "{{").split("{{"):
|
|
1512
|
+
if chunk.strip():
|
|
1513
|
+
pieces.append(chunk.strip())
|
|
1514
|
+
elif s.strip():
|
|
1515
|
+
pieces.append(s.strip())
|
|
1516
|
+
isolate = sorted({str(x) for x in isolate if x}, key=len, reverse=True)
|
|
1517
|
+
if not isolate:
|
|
1518
|
+
return pieces
|
|
1519
|
+
out = []
|
|
1520
|
+
for p in pieces:
|
|
1521
|
+
i, start = 0, 0
|
|
1522
|
+
while i < len(p):
|
|
1523
|
+
hit = next((x for x in isolate if p.startswith(x, i)), None)
|
|
1524
|
+
if hit:
|
|
1525
|
+
if p[start:i].strip():
|
|
1526
|
+
out.append(p[start:i].strip())
|
|
1527
|
+
out.append(hit)
|
|
1528
|
+
i += len(hit)
|
|
1529
|
+
start = i
|
|
1530
|
+
else:
|
|
1531
|
+
i += 1
|
|
1532
|
+
if p[start:].strip():
|
|
1533
|
+
out.append(p[start:].strip())
|
|
1534
|
+
return out
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
class MathTex(VMobject):
|
|
1538
|
+
_kind = "mathtex"
|
|
1539
|
+
_sep = " "
|
|
1540
|
+
|
|
1541
|
+
def __init__(self, *tex_strings, arg_separator=None, substrings_to_isolate=None, tex_to_color_map=None,
|
|
1542
|
+
font_size=DEFAULT_FONT_SIZE, tex_environment=None, **kwargs):
|
|
1543
|
+
cmap = dict(tex_to_color_map or {})
|
|
1544
|
+
parts = _split_tex(tex_strings, list(substrings_to_isolate or []) + list(cmap))
|
|
1545
|
+
self._parts = parts
|
|
1546
|
+
self.tex_string = (self._sep if arg_separator is None else arg_separator).join(parts)
|
|
1547
|
+
geom = {"tex_strings": parts, "font_size": _num(font_size),
|
|
1548
|
+
"arg_separator": self._sep if arg_separator is None else arg_separator}
|
|
1549
|
+
if tex_environment is not None:
|
|
1550
|
+
# How lines (\\) are laid out: "flushleft", "center", "align*", …; absent = the default.
|
|
1551
|
+
geom["tex_environment"] = str(tex_environment)
|
|
1552
|
+
self._make(self._kind, geom, kwargs, _FILLED, n=len(parts))
|
|
1553
|
+
for tex, col in cmap.items():
|
|
1554
|
+
self.set_color_by_tex(tex, col)
|
|
1555
|
+
|
|
1556
|
+
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
|
|
1557
|
+
f = (lambda s: s) if case_sensitive else (lambda s: s.lower())
|
|
1558
|
+
hits = [i for i, p in enumerate(self._parts)
|
|
1559
|
+
if (f(tex) in f(p) if substring else f(tex) == f(p))]
|
|
1560
|
+
return VGroup(*[self[i] for i in hits])
|
|
1561
|
+
|
|
1562
|
+
def get_part_by_tex(self, tex, **kwargs):
|
|
1563
|
+
for i, p in enumerate(self._parts):
|
|
1564
|
+
if tex in p:
|
|
1565
|
+
return self[i]
|
|
1566
|
+
return None
|
|
1567
|
+
|
|
1568
|
+
def index_of_part_by_tex(self, tex, **kwargs):
|
|
1569
|
+
for i, p in enumerate(self._parts):
|
|
1570
|
+
if tex in p:
|
|
1571
|
+
return i
|
|
1572
|
+
return -1
|
|
1573
|
+
|
|
1574
|
+
index_of_part = index_of_part_by_tex
|
|
1575
|
+
|
|
1576
|
+
def set_color_by_tex(self, tex, color, **kwargs):
|
|
1577
|
+
for i, p in enumerate(self._parts):
|
|
1578
|
+
if tex in p:
|
|
1579
|
+
self[i].set_color(color)
|
|
1580
|
+
return self
|
|
1581
|
+
|
|
1582
|
+
def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs):
|
|
1583
|
+
for tex, col in texs_to_color_map.items():
|
|
1584
|
+
self.set_color_by_tex(tex, col)
|
|
1585
|
+
return self
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
class Tex(MathTex):
|
|
1589
|
+
_kind = "tex"
|
|
1590
|
+
_sep = ""
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
class Title(Tex):
|
|
1594
|
+
"""A Tex at the top, underlined (the underline is its last part)."""
|
|
1595
|
+
|
|
1596
|
+
def __init__(self, *text_parts, include_underline=True, match_underline_width_to_text=False,
|
|
1597
|
+
underline_buff=MED_SMALL_BUFF, **kwargs):
|
|
1598
|
+
Tex.__init__(self, *text_parts, **kwargs)
|
|
1599
|
+
self.to_edge(UP)
|
|
1600
|
+
if include_underline:
|
|
1601
|
+
w, _h = _frame_size()
|
|
1602
|
+
self.underline = Line(LEFT, RIGHT).next_to(self, DOWN, buff=underline_buff)
|
|
1603
|
+
if match_underline_width_to_text:
|
|
1604
|
+
self.underline.match_width(self)
|
|
1605
|
+
else:
|
|
1606
|
+
self.underline.scale_to_fit_width(w - 2)
|
|
1607
|
+
self.add(self.underline)
|
|
1608
|
+
self._n += 1
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def _number_tex(number, num_decimal_places, include_sign):
|
|
1612
|
+
s = ("%+." if include_sign else "%.") + str(int(num_decimal_places)) + "f"
|
|
1613
|
+
return s % number
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
def _real_number(v):
|
|
1617
|
+
return isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
def _decimal_text(number, num_decimal_places, include_sign, group_with_commas):
|
|
1621
|
+
s = ("{:+," if include_sign else "{:,") if group_with_commas else ("{:+" if include_sign else "{:")
|
|
1622
|
+
return (s + ".%df}" % int(num_decimal_places)).format(number)
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
class DecimalNumber(VMobject):
|
|
1626
|
+
"""A number drawn digit by digit, so changing it is cheap."""
|
|
1627
|
+
|
|
1628
|
+
def __init__(self, number=0, num_decimal_places=2, mob_class=None, include_sign=False,
|
|
1629
|
+
group_with_commas=True, digit_buff_per_font_unit=0.001, show_ellipsis=False, unit=None,
|
|
1630
|
+
unit_buff_per_font_unit=0, include_background_rectangle=False, edge_to_fix=LEFT,
|
|
1631
|
+
font_size=DEFAULT_FONT_SIZE, stroke_width=0, fill_opacity=1.0, **kwargs):
|
|
1632
|
+
self.number = number
|
|
1633
|
+
self.num_decimal_places = num_decimal_places
|
|
1634
|
+
n = None
|
|
1635
|
+
if _real_number(number):
|
|
1636
|
+
text = _decimal_text(number, num_decimal_places, include_sign, group_with_commas)
|
|
1637
|
+
n = len(text) + (1 if unit else 0) + (3 if show_ellipsis else 0)
|
|
1638
|
+
self._make("decimal_number", {
|
|
1639
|
+
"number": _num(number), "num_decimal_places": int(num_decimal_places),
|
|
1640
|
+
"include_sign": bool(include_sign), "group_with_commas": bool(group_with_commas),
|
|
1641
|
+
"show_ellipsis": bool(show_ellipsis), "unit": unit, "edge_to_fix": _P(edge_to_fix),
|
|
1642
|
+
"include_background_rectangle": bool(include_background_rectangle), "font_size": _num(font_size),
|
|
1643
|
+
}, dict(kwargs, stroke_width=stroke_width, fill_opacity=fill_opacity), _FILLED, n=n)
|
|
1644
|
+
|
|
1645
|
+
def set_value(self, number):
|
|
1646
|
+
self.number = number
|
|
1647
|
+
return self._op("set_value", {"value": _num(number)})
|
|
1648
|
+
|
|
1649
|
+
def get_value(self):
|
|
1650
|
+
if _real_number(self.number) and _st().trace is None:
|
|
1651
|
+
return self.number
|
|
1652
|
+
return Expr(["get", self._ref(), "value"], "s")
|
|
1653
|
+
|
|
1654
|
+
def increment_value(self, delta_t=1):
|
|
1655
|
+
if _real_number(self.number) and _real_number(delta_t):
|
|
1656
|
+
self.number += delta_t
|
|
1657
|
+
return self._op("increment_value", {"value": _num(delta_t)})
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
class Integer(DecimalNumber):
|
|
1661
|
+
def __init__(self, number=0, num_decimal_places=0, **kwargs):
|
|
1662
|
+
DecimalNumber.__init__(self, number, num_decimal_places=num_decimal_places, **kwargs)
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
class ValueTracker(Mobject):
|
|
1666
|
+
"""A number on the timeline: animate it, and have updaters follow it."""
|
|
1667
|
+
|
|
1668
|
+
def __init__(self, value=0, **kwargs):
|
|
1669
|
+
self._value = self._initial = float(value) if _real_number(value) else 0.0
|
|
1670
|
+
self._make("value_tracker", {"value": _num(value)}, kwargs, {}, n=0)
|
|
1671
|
+
|
|
1672
|
+
def get_value(self):
|
|
1673
|
+
st = _st()
|
|
1674
|
+
tr = st.trace
|
|
1675
|
+
real = (tr is not None and tr.real) or (tr is None and _live_phase())
|
|
1676
|
+
if real and self.__dict__.get("_tmpl") is None:
|
|
1677
|
+
if st.anim_v is None: # no frame from the page yet: the timeline's start
|
|
1678
|
+
return self._initial if st.trace is not None else self._value
|
|
1679
|
+
return st.anim_v.get(self._id, self._value)
|
|
1680
|
+
return Expr(["val", self._ref()], "s")
|
|
1681
|
+
|
|
1682
|
+
def set_value(self, value):
|
|
1683
|
+
if _real_number(value):
|
|
1684
|
+
self._value = float(value)
|
|
1685
|
+
return self._op("set_value", {"value": _num(value)})
|
|
1686
|
+
|
|
1687
|
+
def increment_value(self, d_value):
|
|
1688
|
+
if _real_number(d_value):
|
|
1689
|
+
self._value += d_value
|
|
1690
|
+
return self._op("increment_value", {"value": _num(d_value)})
|
|
1691
|
+
|
|
1692
|
+
def __iadd__(self, d_value):
|
|
1693
|
+
return self.increment_value(d_value)
|
|
1694
|
+
|
|
1695
|
+
def __isub__(self, d_value):
|
|
1696
|
+
return self.increment_value(-d_value)
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
class Variable(VGroup):
|
|
1700
|
+
"""label = value, with a tracker (var.tracker) that the value follows."""
|
|
1701
|
+
|
|
1702
|
+
def __init__(self, var, label, var_type=None, num_decimal_places=2, **kwargs):
|
|
1703
|
+
var_type = var_type or DecimalNumber
|
|
1704
|
+
label = MathTex(label) if isinstance(label, str) else label
|
|
1705
|
+
# As Manim's: the label carries its "=", so the parts are [label, value].
|
|
1706
|
+
self.label = VGroup(label, MathTex("=")).arrange(RIGHT)
|
|
1707
|
+
self.tracker = ValueTracker(var)
|
|
1708
|
+
if var_type is Integer or (isinstance(var_type, type) and issubclass(var_type, Integer)):
|
|
1709
|
+
self.value = var_type(var, **kwargs)
|
|
1710
|
+
else:
|
|
1711
|
+
self.value = var_type(var, num_decimal_places=num_decimal_places, **kwargs)
|
|
1712
|
+
VGroup.__init__(self, self.label, self.value)
|
|
1713
|
+
self.arrange(RIGHT)
|
|
1714
|
+
tracker = self.tracker
|
|
1715
|
+
self.value.add_updater(lambda v: v.set_value(tracker.get_value()))
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
# ---------------------------------------------------------------- updaters
|
|
1719
|
+
#
|
|
1720
|
+
# An updater is TRACED: called once with symbolic values (tracker values,
|
|
1721
|
+
# positions, the time, dt), so its ops become formulas the engine runs every
|
|
1722
|
+
# frame — scrubbing and recording stay exact and Python stays idle. One that
|
|
1723
|
+
# cannot be traced (an if on a value, math.sin, a call we cannot follow) runs in
|
|
1724
|
+
# Python every frame instead: the runtime stays live, and its ops are sent as
|
|
1725
|
+
# live overrides at the frame's timeline time (anim_t in the tick payload).
|
|
1726
|
+
|
|
1727
|
+
_RELATIVE_OPS = ("shift", "rotate", "scale", "stretch", "increment_value", "rotate_about_origin")
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
class _Trace:
|
|
1731
|
+
__slots__ = ("ops", "next_key", "real")
|
|
1732
|
+
|
|
1733
|
+
def __init__(self, real):
|
|
1734
|
+
self.ops = [] # [name, args, target] on existing objects
|
|
1735
|
+
self.next_key = 0 # templates made inside the traced function
|
|
1736
|
+
self.real = real # Python fallback: trackers and time are real numbers
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
def _run_trace(fn, args, real=False):
|
|
1740
|
+
st = _st()
|
|
1741
|
+
prev = st.trace
|
|
1742
|
+
tr = st.trace = _Trace(real)
|
|
1743
|
+
try:
|
|
1744
|
+
return tr, fn(*args), None
|
|
1745
|
+
except Exception as e: # noqa: BLE001 — any failure means: run it in Python instead
|
|
1746
|
+
if real:
|
|
1747
|
+
raise
|
|
1748
|
+
return tr, None, e
|
|
1749
|
+
finally:
|
|
1750
|
+
st.trace = prev
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
def _takes_dt(fn):
|
|
1754
|
+
try:
|
|
1755
|
+
params = [p for p in inspect.signature(fn).parameters.values()
|
|
1756
|
+
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]
|
|
1757
|
+
except (TypeError, ValueError):
|
|
1758
|
+
return False
|
|
1759
|
+
return len(params) >= 2 or any(p.name == "dt" for p in params)
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def _fn_line(fn):
|
|
1763
|
+
code = getattr(fn, "__code__", None)
|
|
1764
|
+
if code is not None and code.co_filename == _rt.USER_FILE:
|
|
1765
|
+
return code.co_firstlineno
|
|
1766
|
+
return _rt._user_line()
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def _new_uid():
|
|
1770
|
+
st = _st()
|
|
1771
|
+
uid = st.next_uid
|
|
1772
|
+
st.next_uid += 1
|
|
1773
|
+
return uid
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def _wire(v):
|
|
1777
|
+
"""A template ({key, kind, props, ops}) as the engine takes it: [kind, props, ops],
|
|
1778
|
+
nested wherever one appears. A reference to another template cannot be sent."""
|
|
1779
|
+
if isinstance(v, dict):
|
|
1780
|
+
if "kind" in v and "key" in v:
|
|
1781
|
+
return [v["kind"], _wire(v["props"]), [[op[0], _wire(op[1])] for op in v["ops"]]]
|
|
1782
|
+
if "tmpl" in v:
|
|
1783
|
+
raise _Untraceable("one object it makes refers to another one it makes")
|
|
1784
|
+
return {k: _wire(x) for k, x in v.items()}
|
|
1785
|
+
if isinstance(v, (list, tuple)):
|
|
1786
|
+
if any(isinstance(x, Expr) for x in v):
|
|
1787
|
+
return _j(v) # a point with a formula in it: {"$pt": …}
|
|
1788
|
+
return [_wire(x) for x in v]
|
|
1789
|
+
if isinstance(v, (Expr, Mobject)):
|
|
1790
|
+
return _wire(_j(v))
|
|
1791
|
+
return v
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def _per_frame(args, name, k):
|
|
1795
|
+
"""A per-frame relative op as a rate: its amount times k = 60·dt (Manim's 60 fps)."""
|
|
1796
|
+
def times(v):
|
|
1797
|
+
v = _j(v)
|
|
1798
|
+
if isinstance(v, dict):
|
|
1799
|
+
key = "$pt" if "$pt" in v else "$x"
|
|
1800
|
+
return {key: ["*", v[key], k]}
|
|
1801
|
+
if isinstance(v, list):
|
|
1802
|
+
return {"$pt": ["*", v, k]}
|
|
1803
|
+
return {"$x": ["*", v, k]}
|
|
1804
|
+
|
|
1805
|
+
def power(v):
|
|
1806
|
+
v = _j(v)
|
|
1807
|
+
return {"$x": ["**", v.get("$x") if isinstance(v, dict) else v, k]}
|
|
1808
|
+
|
|
1809
|
+
a = dict(args)
|
|
1810
|
+
if name == "shift":
|
|
1811
|
+
a["vectors"] = [times(v) for v in a.get("vectors", [])]
|
|
1812
|
+
elif name in ("rotate", "rotate_about_origin"):
|
|
1813
|
+
a["angle"] = times(a["angle"])
|
|
1814
|
+
elif name == "increment_value":
|
|
1815
|
+
a["value"] = times(a["value"])
|
|
1816
|
+
elif name == "scale":
|
|
1817
|
+
a["scale_factor"] = power(a["scale_factor"])
|
|
1818
|
+
elif name == "stretch":
|
|
1819
|
+
a["factor"] = power(a["factor"])
|
|
1820
|
+
return a
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
def _by_target(ops):
|
|
1824
|
+
out = {}
|
|
1825
|
+
for name, args, target in ops:
|
|
1826
|
+
out.setdefault(repr(target), (target, []))[1].append([name, args])
|
|
1827
|
+
return list(out.values())
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def _add_updater(mob, fn):
|
|
1831
|
+
if mob.__dict__.get("_tmpl") is not None or _st().trace is not None:
|
|
1832
|
+
raise _Untraceable("it adds an updater")
|
|
1833
|
+
dt = _takes_dt(fn)
|
|
1834
|
+
tr, result, err = _run_trace(fn, (mob, _var("dt")) if dt else (mob,))
|
|
1835
|
+
entries = []
|
|
1836
|
+
if err is None:
|
|
1837
|
+
ops = tr.ops
|
|
1838
|
+
kind = "dt" if dt else "op"
|
|
1839
|
+
if not dt and any(op[0] in _RELATIVE_OPS for op in ops):
|
|
1840
|
+
k = ["*", ["dt"], float(config.frame_rate or 60)]
|
|
1841
|
+
ops = [[name, _per_frame(args, name, k) if name in _RELATIVE_OPS else args, target]
|
|
1842
|
+
for name, args, target in ops]
|
|
1843
|
+
kind = "dt"
|
|
1844
|
+
try:
|
|
1845
|
+
groups = [(target, _wire(o)) for target, o in _by_target(ops)] or [(mob._t(), [])]
|
|
1846
|
+
except _Untraceable as e:
|
|
1847
|
+
err = e
|
|
1848
|
+
else:
|
|
1849
|
+
for target, o in groups:
|
|
1850
|
+
uid = _new_uid()
|
|
1851
|
+
_call("add_updater", target, {"uid": uid, "kind": kind, "ops": o})
|
|
1852
|
+
entries.append((target, uid))
|
|
1853
|
+
if err is not None:
|
|
1854
|
+
u = _PyUpdater("dt" if dt else "op", fn, mob)
|
|
1855
|
+
_fallback(u)
|
|
1856
|
+
entries.append((None, u))
|
|
1857
|
+
mob.__dict__.setdefault("updaters", []).append((fn, entries))
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def _fallback(u):
|
|
1861
|
+
_tell("fb%d" % id(u), "Note: the updater on line %d runs in Python on every frame, so the program "
|
|
1862
|
+
"keeps running." % _fn_line(u.fn))
|
|
1863
|
+
_st().fallbacks.append(u)
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def _live_op(name, args, target, accumulate=False):
|
|
1867
|
+
"""One change on top of the timeline, at whatever time is drawn."""
|
|
1868
|
+
if (name == "set_value" and not accumulate and isinstance(target, int)
|
|
1869
|
+
and isinstance(_st().objects.get(target), ValueTracker)):
|
|
1870
|
+
_call("live_set", target, _wire(args.get("value")))
|
|
1871
|
+
return
|
|
1872
|
+
if accumulate:
|
|
1873
|
+
_call("live_op", target, name, _wire(args), {"accumulate": True})
|
|
1874
|
+
else:
|
|
1875
|
+
_call("live_op", target, name, _wire(args))
|
|
1876
|
+
|
|
1877
|
+
|
|
1878
|
+
class _PyUpdater:
|
|
1879
|
+
"""An updater run in Python each frame; its ops become live overrides."""
|
|
1880
|
+
|
|
1881
|
+
def __init__(self, kind, fn, mob):
|
|
1882
|
+
self.kind, self.fn, self.mob = kind, fn, mob
|
|
1883
|
+
self.last_t = None
|
|
1884
|
+
self.active = True
|
|
1885
|
+
|
|
1886
|
+
def run(self, st):
|
|
1887
|
+
if not self.active:
|
|
1888
|
+
return
|
|
1889
|
+
if self.kind == "dt":
|
|
1890
|
+
t = st.anim_t
|
|
1891
|
+
last, self.last_t = self.last_t, t
|
|
1892
|
+
if last is None or t <= last:
|
|
1893
|
+
return
|
|
1894
|
+
tr, _r, _e = _run_trace(self.fn, (self.mob, t - last), real=True)
|
|
1895
|
+
for name, args, target in tr.ops:
|
|
1896
|
+
_live_op(name, args, target, accumulate=True)
|
|
1897
|
+
return
|
|
1898
|
+
if self.kind == "redraw":
|
|
1899
|
+
tr, result, _e = _run_trace(self.fn, (), real=True)
|
|
1900
|
+
if isinstance(result, Mobject):
|
|
1901
|
+
_live_op("become", {"mobject": result._embed()}, self.mob._t())
|
|
1902
|
+
else:
|
|
1903
|
+
tr, _r, _e = _run_trace(self.fn, (self.mob,), real=True)
|
|
1904
|
+
for name, args, target in tr.ops:
|
|
1905
|
+
_live_op(name, args, target)
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def always_redraw(func):
|
|
1909
|
+
"""A mobject rebuilt from func() every frame."""
|
|
1910
|
+
st = _st()
|
|
1911
|
+
tr, result, err = _run_trace(func, ())
|
|
1912
|
+
if err is None and isinstance(result, Mobject) and result.__dict__.get("_tmpl") is not None:
|
|
1913
|
+
try:
|
|
1914
|
+
make = _wire(result._tmpl)
|
|
1915
|
+
except _Untraceable as e:
|
|
1916
|
+
err = e
|
|
1917
|
+
else:
|
|
1918
|
+
result._tmpl = None
|
|
1919
|
+
result._id = st.next_id
|
|
1920
|
+
st.next_id += 1
|
|
1921
|
+
st.objects[result._id] = result
|
|
1922
|
+
uid = _new_uid()
|
|
1923
|
+
_call("add_updater", result._id, {"uid": uid, "kind": "redraw", "make": make})
|
|
1924
|
+
result.__dict__["updaters"] = [(func, [(result._id, uid)])]
|
|
1925
|
+
return result
|
|
1926
|
+
if err is None and not isinstance(result, Mobject):
|
|
1927
|
+
raise TypeError("always_redraw needs a function that returns a mobject, such as "
|
|
1928
|
+
"lambda: Line(a.get_center(), b.get_center())")
|
|
1929
|
+
holder = VGroup()
|
|
1930
|
+
u = _PyUpdater("redraw", func, holder)
|
|
1931
|
+
_fallback(u)
|
|
1932
|
+
holder.__dict__["updaters"] = [(func, [(None, u)])]
|
|
1933
|
+
return holder
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
def always(method, *args, **kwargs):
|
|
1937
|
+
"""always(m.next_to, other, UP): keep calling the method every frame."""
|
|
1938
|
+
mob = method.__self__
|
|
1939
|
+
mob.add_updater(lambda m: method(*args, **kwargs))
|
|
1940
|
+
return mob
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
def f_always(method, *arg_generators, **kwargs):
|
|
1944
|
+
"""f_always(m.set_x, lambda: t.get_value()): the arguments are worked out every frame."""
|
|
1945
|
+
mob = method.__self__
|
|
1946
|
+
mob.add_updater(lambda m: method(*[g() for g in arg_generators], **kwargs))
|
|
1947
|
+
return mob
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
def always_shift(mobject, direction=RIGHT, rate=0.1):
|
|
1951
|
+
return mobject.add_updater(lambda m, dt: m.shift(dt * rate * direction))
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
def always_rotate(mobject, rate=20 * DEGREES, **kwargs):
|
|
1955
|
+
return mobject.add_updater(lambda m, dt: m.rotate(dt * rate, **kwargs))
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
class TracedPath(VMobject):
|
|
1959
|
+
"""The path a point leaves behind: TracedPath(dot.get_center)."""
|
|
1960
|
+
|
|
1961
|
+
def __init__(self, traced_point_func, stroke_width=2, stroke_color=WHITE, dissipating_time=None, **kwargs):
|
|
1962
|
+
tr, point, err = _run_trace(traced_point_func, ())
|
|
1963
|
+
if err is not None or not _is_sym(point):
|
|
1964
|
+
raise TypeError("TracedPath needs a point it can follow, such as TracedPath(dot.get_center)")
|
|
1965
|
+
self._make("traced_path", {"point": _P(point), "dissipating_time": dissipating_time},
|
|
1966
|
+
dict(kwargs, stroke_width=stroke_width, stroke_color=stroke_color), n=0)
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
# ---------------------------------------------------------------- Pyctures inside a scene
|
|
1970
|
+
|
|
1971
|
+
class PycturesCanvas(Mobject):
|
|
1972
|
+
"""A picture drawn with Pyctures' ctx every frame, placed in the scene:
|
|
1973
|
+
draw(ctx, t) draws in pixels (resolution per scene unit); t is the time."""
|
|
1974
|
+
|
|
1975
|
+
def __init__(self, width=4.0, height=3.0, draw=None, resolution=100, **kwargs):
|
|
1976
|
+
w, h = float(width), float(height)
|
|
1977
|
+
ratio = ((_rt.S.ns or {}).get("pixel_ratio", 1) or 1)
|
|
1978
|
+
self._lw, self._lh = max(1, int(round(w * resolution))), max(1, int(round(h * resolution)))
|
|
1979
|
+
pw, ph = max(1, int(round(self._lw * ratio))), max(1, int(round(self._lh * ratio)))
|
|
1980
|
+
self._ratio = ratio
|
|
1981
|
+
self._draw_fn = draw
|
|
1982
|
+
self._make("pyctures_canvas", {"width": w, "height": h, "px_width": pw, "px_height": ph}, kwargs, {}, n=0)
|
|
1983
|
+
# The engine makes the canvas element; Python draws on it through a handle.
|
|
1984
|
+
self._canvas = _rt.JsRef(H_ANIM).canvas_for(self._id)
|
|
1985
|
+
self._ctx = self._canvas.getContext("2d")
|
|
1986
|
+
_st().canvases.append(self)
|
|
1987
|
+
|
|
1988
|
+
@property
|
|
1989
|
+
def ctx(self):
|
|
1990
|
+
return self._ctx
|
|
1991
|
+
|
|
1992
|
+
def _redraw(self):
|
|
1993
|
+
if self._draw_fn is None:
|
|
1994
|
+
return
|
|
1995
|
+
g = self._ctx
|
|
1996
|
+
g.setTransform(self._ratio, 0, 0, self._ratio, 0, 0)
|
|
1997
|
+
g.clearRect(0, 0, self._lw, self._lh)
|
|
1998
|
+
self._draw_fn(g, _st().anim_t)
|
|
1999
|
+
|
|
2000
|
+
|
|
2001
|
+
def _style_dict(d):
|
|
2002
|
+
"""A config dict (axis_config={"color": GREEN}): colours as '#RRGGBB', the rest as JSON."""
|
|
2003
|
+
return {str(k): _color(v) if str(k).endswith("color") else _j(v) for k, v in dict(d or {}).items()}
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
def _range3(r, default):
|
|
2007
|
+
r = list(default if r is None else r)
|
|
2008
|
+
if len(r) == 2:
|
|
2009
|
+
r.append(1.0)
|
|
2010
|
+
return [_num(x) for x in r]
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
class _Curve(VMobject):
|
|
2014
|
+
"""A curve sampled in Python once (axes.plot, ParametricFunction, get_area)."""
|
|
2015
|
+
|
|
2016
|
+
def __init__(self, points, axes=None, function=None, closed=False, x_range=None, **kwargs):
|
|
2017
|
+
self._func = function
|
|
2018
|
+
self._axes = axes
|
|
2019
|
+
self._x_range = x_range
|
|
2020
|
+
# Pen-up gaps (None) split the curve into segments.
|
|
2021
|
+
segs, seg = [], []
|
|
2022
|
+
for p in points:
|
|
2023
|
+
if p is None:
|
|
2024
|
+
if seg:
|
|
2025
|
+
segs.append(seg)
|
|
2026
|
+
seg = []
|
|
2027
|
+
else:
|
|
2028
|
+
seg.append(p)
|
|
2029
|
+
if seg:
|
|
2030
|
+
segs.append(seg)
|
|
2031
|
+
if axes is not None:
|
|
2032
|
+
# In the axes' own coordinates; the engine places them on the axes.
|
|
2033
|
+
geom = {"segments": segs, "axes": axes._ref()}
|
|
2034
|
+
if closed:
|
|
2035
|
+
geom.update(closed=True, use_smoothing=False)
|
|
2036
|
+
self._make("graph", geom, kwargs, n=0)
|
|
2037
|
+
else:
|
|
2038
|
+
self._make("vmobject", {"subpaths": segs, "smooth": not closed, "closed": bool(closed)}, kwargs, n=0)
|
|
2039
|
+
|
|
2040
|
+
def underlying_function(self, x):
|
|
2041
|
+
return self._func(x)
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
def _sample(fn, t0, t1, step, to_point):
|
|
2045
|
+
span = t1 - t0
|
|
2046
|
+
if span <= 0:
|
|
2047
|
+
return []
|
|
2048
|
+
step = min(abs(step) if step else span, span / 200.0)
|
|
2049
|
+
count = min(4000, int(math.ceil(span / step)))
|
|
2050
|
+
pts, pen = [], False
|
|
2051
|
+
for i in range(count + 1):
|
|
2052
|
+
t = t0 + span * i / count
|
|
2053
|
+
try:
|
|
2054
|
+
p = to_point(fn, t)
|
|
2055
|
+
# A symbolic value (a tracker inside the function) is worked out by the
|
|
2056
|
+
# engine, every frame when the curve is redrawn.
|
|
2057
|
+
ok = all(isinstance(v, Expr) or (v == v and abs(v) < 1e7) for v in p)
|
|
2058
|
+
except (ArithmeticError, ValueError):
|
|
2059
|
+
ok = False
|
|
2060
|
+
if ok:
|
|
2061
|
+
pts.append(p)
|
|
2062
|
+
pen = True
|
|
2063
|
+
elif pen:
|
|
2064
|
+
pts.append(None)
|
|
2065
|
+
pen = False
|
|
2066
|
+
while pts and pts[-1] is None:
|
|
2067
|
+
pts.pop()
|
|
2068
|
+
return pts
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
def _xy(fn, x):
|
|
2072
|
+
y = fn(x)
|
|
2073
|
+
if isinstance(y, Expr):
|
|
2074
|
+
if y.shape != "s":
|
|
2075
|
+
raise TypeError("a plotted function must return a number")
|
|
2076
|
+
return [float(x), y, 0.0]
|
|
2077
|
+
return [float(x), float(y), 0.0]
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
def _xyz(fn, t):
|
|
2081
|
+
p = _unarray(fn(t))
|
|
2082
|
+
if _is_sym(p):
|
|
2083
|
+
p = list(p)
|
|
2084
|
+
return (p + [0.0, 0.0])[:3] if len(p) < 3 else p[:3]
|
|
2085
|
+
return list(Vec.of(p))
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
class ParametricFunction(_Curve):
|
|
2089
|
+
def __init__(self, function, t_range=(0, 1), color=YELLOW, **kwargs):
|
|
2090
|
+
t = _range3(t_range, (0, 1, 0.01))
|
|
2091
|
+
_Curve.__init__(self, _sample(function, t[0], t[1], t[2] if len(t_range) > 2 else None, _xyz),
|
|
2092
|
+
function=function, color=color, **kwargs)
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
class FunctionGraph(ParametricFunction):
|
|
2096
|
+
def __init__(self, function, x_range=None, color=YELLOW, **kwargs):
|
|
2097
|
+
w, _h = _frame_size()
|
|
2098
|
+
r = _range3(x_range, (-w / 2, w / 2, 0.01))
|
|
2099
|
+
_Curve.__init__(self, _sample(function, r[0], r[1], r[2], _xy), function=function, color=color,
|
|
2100
|
+
x_range=(r[0], r[1]), **kwargs)
|
|
2101
|
+
|
|
2102
|
+
|
|
2103
|
+
class NumberLine(VMobject):
|
|
2104
|
+
def __init__(self, x_range=None, length=None, unit_size=1, include_numbers=False, include_tip=False,
|
|
2105
|
+
**kwargs):
|
|
2106
|
+
w, _h = _frame_size()
|
|
2107
|
+
half = float(round(w / 2))
|
|
2108
|
+
r = _range3(x_range, (-half, half, 1))
|
|
2109
|
+
self.x_range = r
|
|
2110
|
+
self._make("number_line", {"x_range": r, "length": _num(length) if length else None,
|
|
2111
|
+
"unit_size": _num(unit_size), "include_numbers": bool(include_numbers),
|
|
2112
|
+
"include_tip": bool(include_tip)}, kwargs, n=None)
|
|
2113
|
+
|
|
2114
|
+
def number_to_point(self, number):
|
|
2115
|
+
return Point(["c2p", self._ref(), _coord(number), 0.0])
|
|
2116
|
+
|
|
2117
|
+
n2p = number_to_point
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
class Axes(VMobject):
|
|
2121
|
+
_kind = "axes"
|
|
2122
|
+
|
|
2123
|
+
def __init__(self, x_range=None, y_range=None, x_length=None, y_length=None, axis_config=None,
|
|
2124
|
+
x_axis_config=None, y_axis_config=None, tips=True, **kwargs):
|
|
2125
|
+
w, h = _frame_size()
|
|
2126
|
+
self.x_range = _range3(x_range, self._default_range(w))
|
|
2127
|
+
self.y_range = _range3(y_range, self._default_range(h))
|
|
2128
|
+
geom = {"x_range": self.x_range, "y_range": self.y_range,
|
|
2129
|
+
"x_length": _num(x_length) if x_length is not None else self._length(round(w) - 2, self.x_range),
|
|
2130
|
+
"y_length": _num(y_length) if y_length is not None else self._length(round(h) - 2, self.y_range),
|
|
2131
|
+
"tips": bool(tips), "axis_config": _style_dict(axis_config),
|
|
2132
|
+
"x_axis_config": _style_dict(x_axis_config), "y_axis_config": _style_dict(y_axis_config)}
|
|
2133
|
+
geom.update(self._extra(kwargs))
|
|
2134
|
+
self._make(self._kind, geom, kwargs, n=4 if isinstance(self, NumberPlane) else 2)
|
|
2135
|
+
|
|
2136
|
+
def _length(self, frame_length, r):
|
|
2137
|
+
return float(frame_length)
|
|
2138
|
+
|
|
2139
|
+
def _default_range(self, frame_length):
|
|
2140
|
+
# Whole units: x -7..7 and y -4..4 in the 16:9 frame.
|
|
2141
|
+
half = float(round(frame_length / 2))
|
|
2142
|
+
return (-half, half, 1)
|
|
2143
|
+
|
|
2144
|
+
def _extra(self, kwargs):
|
|
2145
|
+
return {}
|
|
2146
|
+
|
|
2147
|
+
_axis_index = (0, 1) # where the x and y axes sit among the parts
|
|
2148
|
+
|
|
2149
|
+
@property
|
|
2150
|
+
def x_axis(self):
|
|
2151
|
+
return self[self._axis_index[0]]
|
|
2152
|
+
|
|
2153
|
+
@property
|
|
2154
|
+
def y_axis(self):
|
|
2155
|
+
return self[self._axis_index[1]]
|
|
2156
|
+
|
|
2157
|
+
def get_axes(self):
|
|
2158
|
+
return VGroup(self.x_axis, self.y_axis)
|
|
2159
|
+
|
|
2160
|
+
def coords_to_point(self, *coords):
|
|
2161
|
+
if len(coords) == 1:
|
|
2162
|
+
coords = tuple(_unarray(coords[0]))
|
|
2163
|
+
x = _coord(coords[0])
|
|
2164
|
+
y = _coord(coords[1]) if len(coords) > 1 else 0.0
|
|
2165
|
+
return Point(["c2p", self._ref(), x, y])
|
|
2166
|
+
|
|
2167
|
+
c2p = coords_to_point
|
|
2168
|
+
|
|
2169
|
+
def plot(self, function, x_range=None, color=None, **kwargs):
|
|
2170
|
+
r = list(x_range) if x_range is not None else list(self.x_range)
|
|
2171
|
+
r = _range3(r, self.x_range)
|
|
2172
|
+
step = r[2] if (x_range is not None and len(x_range) > 2) else None
|
|
2173
|
+
kw = dict(kwargs)
|
|
2174
|
+
kw["color"] = color if color is not None else kw.get("color", YELLOW)
|
|
2175
|
+
g = _Curve(_sample(function, r[0], r[1], step, _xy), axes=self, function=function,
|
|
2176
|
+
x_range=(r[0], r[1]), **kw)
|
|
2177
|
+
g.color = kw["color"]
|
|
2178
|
+
return g
|
|
2179
|
+
|
|
2180
|
+
get_graph = plot
|
|
2181
|
+
|
|
2182
|
+
def plot_parametric_curve(self, function, t_range=(0, 1), color=YELLOW, **kwargs):
|
|
2183
|
+
t = _range3(t_range, (0, 1, 0.01))
|
|
2184
|
+
return _Curve(_sample(function, t[0], t[1], t[2] if len(t_range) > 2 else None, _xyz),
|
|
2185
|
+
axes=self, function=function, color=color, **kwargs)
|
|
2186
|
+
|
|
2187
|
+
def input_to_graph_point(self, x, graph):
|
|
2188
|
+
fn = getattr(graph, "_func", None)
|
|
2189
|
+
if fn is None:
|
|
2190
|
+
raise TypeError("input_to_graph_point needs a graph made with axes.plot(...)")
|
|
2191
|
+
return self.c2p(x, fn(x))
|
|
2192
|
+
|
|
2193
|
+
i2gp = input_to_graph_point
|
|
2194
|
+
|
|
2195
|
+
def get_graph_label(self, graph, label="f(x)", x_val=None, direction=RIGHT, buff=MED_SMALL_BUFF,
|
|
2196
|
+
color=None, dot=False, dot_config=None):
|
|
2197
|
+
m = label if isinstance(label, Mobject) else MathTex(label)
|
|
2198
|
+
if x_val is None:
|
|
2199
|
+
x_val = self.x_range[1]
|
|
2200
|
+
m.next_to(self.i2gp(x_val, graph), direction, buff=buff)
|
|
2201
|
+
m.set_color(color if color is not None else getattr(graph, "color", YELLOW))
|
|
2202
|
+
return m
|
|
2203
|
+
|
|
2204
|
+
def get_x_axis_label(self, label, edge=UR, direction=UR, buff=SMALL_BUFF, **kwargs):
|
|
2205
|
+
m = label if isinstance(label, Mobject) else MathTex(label)
|
|
2206
|
+
return m.next_to(self.x_axis.get_corner(edge), direction, buff=buff)
|
|
2207
|
+
|
|
2208
|
+
def get_y_axis_label(self, label, edge=UR, direction=UR, buff=SMALL_BUFF, **kwargs):
|
|
2209
|
+
m = label if isinstance(label, Mobject) else MathTex(label)
|
|
2210
|
+
return m.next_to(self.y_axis.get_corner(edge), direction, buff=buff)
|
|
2211
|
+
|
|
2212
|
+
def get_axis_labels(self, x_label="x", y_label="y"):
|
|
2213
|
+
return VGroup(self.get_x_axis_label(x_label), self.get_y_axis_label(y_label))
|
|
2214
|
+
|
|
2215
|
+
def add_coordinates(self, *axes_numbers, num_decimal_places=None, **kwargs):
|
|
2216
|
+
return self._op("add_coordinates", {"num_decimal_places": num_decimal_places})
|
|
2217
|
+
|
|
2218
|
+
# -- helpers built here from the plotted function (Python knows it)
|
|
2219
|
+
|
|
2220
|
+
def _graph_fn(self, graph, what):
|
|
2221
|
+
fn = getattr(graph, "_func", None)
|
|
2222
|
+
if fn is None:
|
|
2223
|
+
raise TypeError("%s needs a graph made with axes.plot(...)" % what)
|
|
2224
|
+
return fn
|
|
2225
|
+
|
|
2226
|
+
def get_area(self, graph, x_range=None, color=(BLUE, GREEN), opacity=0.3, bounded_graph=None, **kwargs):
|
|
2227
|
+
f = self._graph_fn(graph, "get_area")
|
|
2228
|
+
a, b = x_range if x_range is not None else (graph._x_range or self.x_range[:2])
|
|
2229
|
+
g = getattr(bounded_graph, "_func", None) or (lambda x: 0.0)
|
|
2230
|
+
top = [p for p in _sample(f, a, b, None, _xy) if p is not None]
|
|
2231
|
+
bottom = [p for p in _sample(g, a, b, None, _xy) if p is not None]
|
|
2232
|
+
area = _Curve(top + bottom[::-1], axes=self, closed=True, fill_opacity=opacity, stroke_width=0,
|
|
2233
|
+
color=list(color) if isinstance(color, (list, tuple)) else color, **kwargs)
|
|
2234
|
+
return area
|
|
2235
|
+
|
|
2236
|
+
def get_riemann_rectangles(self, graph, x_range=None, dx=0.1, input_sample_type="left", stroke_width=1,
|
|
2237
|
+
stroke_color=BLACK, fill_opacity=1, color=(BLUE, GREEN), show_signed_area=True,
|
|
2238
|
+
bounded_graph=None, blend=False, width_scale_factor=1.001):
|
|
2239
|
+
f = self._graph_fn(graph, "get_riemann_rectangles")
|
|
2240
|
+
g = getattr(bounded_graph, "_func", None) or (lambda x: 0.0)
|
|
2241
|
+
a, b = x_range if x_range is not None else self.x_range[:2]
|
|
2242
|
+
n = max(1, int(round((b - a) / dx)))
|
|
2243
|
+
colors = color_gradient(color if isinstance(color, (list, tuple)) else [color], n)
|
|
2244
|
+
rects = []
|
|
2245
|
+
for i in range(n):
|
|
2246
|
+
x0 = a + i * dx
|
|
2247
|
+
x1 = x0 + dx * width_scale_factor
|
|
2248
|
+
xs = {"left": x0, "right": x0 + dx, "center": x0 + dx / 2}.get(input_sample_type, x0)
|
|
2249
|
+
y = f(xs)
|
|
2250
|
+
base = g(xs)
|
|
2251
|
+
pts = [[x0, base, 0.0], [x1, base, 0.0], [x1, y, 0.0], [x0, y, 0.0]]
|
|
2252
|
+
rects.append(_Curve(pts, axes=self, closed=True, fill_opacity=fill_opacity, stroke_width=stroke_width,
|
|
2253
|
+
stroke_color=stroke_color, fill_color=colors[i], color=colors[i]))
|
|
2254
|
+
return VGroup(*rects)
|
|
2255
|
+
|
|
2256
|
+
def get_vertical_line(self, point, line_config=None, line_func=None, **kwargs):
|
|
2257
|
+
line_func = line_func or DashedLine
|
|
2258
|
+
foot = Point(["v", [["idx", _tree(point), 0], ["idx", ["c2p", self._ref(), 0.0, 0.0], 1], 0.0]])
|
|
2259
|
+
return line_func(point, foot, **dict(line_config or {}, **kwargs))
|
|
2260
|
+
|
|
2261
|
+
def get_horizontal_line(self, point, line_config=None, line_func=None, **kwargs):
|
|
2262
|
+
line_func = line_func or DashedLine
|
|
2263
|
+
foot = Point(["v", [["idx", ["c2p", self._ref(), 0.0, 0.0], 0], ["idx", _tree(point), 1], 0.0]])
|
|
2264
|
+
return line_func(point, foot, **dict(line_config or {}, **kwargs))
|
|
2265
|
+
|
|
2266
|
+
def get_T_label(self, x_val, graph, label=None, label_color=None, triangle_size=MED_SMALL_BUFF,
|
|
2267
|
+
triangle_color=WHITE, line_func=None, line_color=YELLOW):
|
|
2268
|
+
line_func = line_func or Line
|
|
2269
|
+
tri = Triangle(color=triangle_color, fill_opacity=1).scale_to_fit_height(triangle_size)
|
|
2270
|
+
tri.move_to(self.c2p(x_val, 0), UP)
|
|
2271
|
+
parts = [tri]
|
|
2272
|
+
if label is not None:
|
|
2273
|
+
lab = label if isinstance(label, Mobject) else MathTex(label)
|
|
2274
|
+
lab.next_to(tri, DOWN)
|
|
2275
|
+
if label_color is not None:
|
|
2276
|
+
lab.set_color(label_color)
|
|
2277
|
+
parts.append(lab)
|
|
2278
|
+
parts.append(line_func(self.c2p(x_val, 0), self.i2gp(x_val, graph), color=line_color))
|
|
2279
|
+
return VGroup(*parts)
|
|
2280
|
+
|
|
2281
|
+
def get_secant_slope_group(self, x, graph, dx=None, dx_line_color=YELLOW, dy_line_color=None,
|
|
2282
|
+
dx_label=None, dy_label=None, include_secant_line=True, secant_line_color=GREEN,
|
|
2283
|
+
secant_line_length=10):
|
|
2284
|
+
f = self._graph_fn(graph, "get_secant_slope_group")
|
|
2285
|
+
dx = dx if dx is not None else (self.x_range[1] - self.x_range[0]) / 10
|
|
2286
|
+
p1, p2 = self.c2p(x, f(x)), self.c2p(x + dx, f(x + dx))
|
|
2287
|
+
corner = self.c2p(x + dx, f(x))
|
|
2288
|
+
dx_line = Line(p1, corner, color=dx_line_color)
|
|
2289
|
+
dy_line = Line(corner, p2, color=dy_line_color or getattr(graph, "color", YELLOW))
|
|
2290
|
+
parts = [dx_line, dy_line]
|
|
2291
|
+
if dx_label is not None:
|
|
2292
|
+
parts.append((dx_label if isinstance(dx_label, Mobject) else MathTex(dx_label)).next_to(dx_line, DOWN))
|
|
2293
|
+
if dy_label is not None:
|
|
2294
|
+
parts.append((dy_label if isinstance(dy_label, Mobject) else MathTex(dy_label)).next_to(dy_line, RIGHT))
|
|
2295
|
+
if include_secant_line:
|
|
2296
|
+
secant = Line(p1, p2, color=secant_line_color)
|
|
2297
|
+
secant.scale(secant_line_length / secant.get_length())
|
|
2298
|
+
parts.append(secant)
|
|
2299
|
+
return VGroup(*parts)
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
class ThreeDAxes(Axes):
|
|
2303
|
+
def __init__(self, x_range=None, y_range=None, z_range=None, x_length=None, y_length=None,
|
|
2304
|
+
z_length=None, **kwargs):
|
|
2305
|
+
_tell("3d", "Note: 3D is not shown yet, so this scene plays in 2D.")
|
|
2306
|
+
Axes.__init__(self, x_range, y_range, x_length, y_length, **kwargs)
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
class NumberPlane(Axes):
|
|
2310
|
+
"""Parts: background lines, faded lines, x axis, y axis (as Manim's)."""
|
|
2311
|
+
_kind = "number_plane"
|
|
2312
|
+
_axis_index = (2, 3)
|
|
2313
|
+
|
|
2314
|
+
def __init__(self, x_range=None, y_range=None, x_length=None, y_length=None,
|
|
2315
|
+
background_line_style=None, **kwargs):
|
|
2316
|
+
kwargs["background_line_style"] = background_line_style
|
|
2317
|
+
Axes.__init__(self, x_range, y_range, x_length, y_length, **kwargs)
|
|
2318
|
+
|
|
2319
|
+
def _length(self, frame_length, r):
|
|
2320
|
+
return r[1] - r[0]
|
|
2321
|
+
|
|
2322
|
+
def _default_range(self, frame_length):
|
|
2323
|
+
# A plane covers the whole frame.
|
|
2324
|
+
return (-frame_length / 2, frame_length / 2, 1)
|
|
2325
|
+
|
|
2326
|
+
def _extra(self, kwargs):
|
|
2327
|
+
return {"background_line_style": _style_dict(kwargs.pop("background_line_style", None))}
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
class SurroundingRectangle(RoundedRectangle):
|
|
2331
|
+
def __init__(self, mobject, color=YELLOW, buff=SMALL_BUFF, corner_radius=0.0, **kwargs):
|
|
2332
|
+
self._make("surrounding_rectangle", {"mobject": mobject._embed(), "buff": _num(buff),
|
|
2333
|
+
"corner_radius": _num(corner_radius)},
|
|
2334
|
+
dict(kwargs, color=color), n=0)
|
|
2335
|
+
|
|
2336
|
+
|
|
2337
|
+
class BackgroundRectangle(SurroundingRectangle):
|
|
2338
|
+
def __init__(self, mobject, color=None, stroke_width=0, stroke_opacity=0, fill_opacity=0.75, buff=0,
|
|
2339
|
+
**kwargs):
|
|
2340
|
+
if color is None:
|
|
2341
|
+
color = config.background_color
|
|
2342
|
+
self._make("background_rectangle", {"mobject": mobject._embed(), "buff": _num(buff)},
|
|
2343
|
+
dict(kwargs, color=color, stroke_width=stroke_width, stroke_opacity=stroke_opacity,
|
|
2344
|
+
fill_opacity=fill_opacity), n=0)
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
class Brace(VMobject):
|
|
2348
|
+
def __init__(self, mobject, direction=DOWN, buff=0.2, **kwargs):
|
|
2349
|
+
self.direction = Vec.of(direction)
|
|
2350
|
+
self._make("brace", {"mobject": mobject._embed(), "direction": _P(direction), "buff": _num(buff)},
|
|
2351
|
+
kwargs, _FILLED, n=0)
|
|
2352
|
+
|
|
2353
|
+
def put_at_tip(self, mob, use_next_to=True, buff=MED_SMALL_BUFF, **kwargs):
|
|
2354
|
+
return mob.next_to(self, self.direction, buff=buff)
|
|
2355
|
+
|
|
2356
|
+
def get_text(self, *text, **kwargs):
|
|
2357
|
+
"""A Tex label at the tip (Manim's: TeX type at the default size, not Text)."""
|
|
2358
|
+
return self.put_at_tip(Tex(*text), **kwargs)
|
|
2359
|
+
|
|
2360
|
+
def get_tex(self, *tex, **kwargs):
|
|
2361
|
+
"""A MathTex label at the tip."""
|
|
2362
|
+
return self.put_at_tip(MathTex(*tex), **kwargs)
|
|
2363
|
+
|
|
2364
|
+
|
|
2365
|
+
class BraceLabel(VGroup):
|
|
2366
|
+
def __init__(self, obj, text, brace_direction=DOWN, label_constructor=MathTex, font_size=DEFAULT_FONT_SIZE,
|
|
2367
|
+
buff=0.2, **kwargs):
|
|
2368
|
+
self.brace = Brace(obj, brace_direction, buff=buff)
|
|
2369
|
+
self.label = label_constructor(text, font_size=font_size)
|
|
2370
|
+
self.brace.put_at_tip(self.label)
|
|
2371
|
+
VGroup.__init__(self, self.brace, self.label, **kwargs)
|
|
2372
|
+
|
|
2373
|
+
|
|
2374
|
+
class BraceText(BraceLabel):
|
|
2375
|
+
def __init__(self, obj, text, label_constructor=Text, **kwargs):
|
|
2376
|
+
BraceLabel.__init__(self, obj, text, label_constructor=label_constructor, **kwargs)
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
class CameraFrame(Mobject):
|
|
2380
|
+
"""The camera frame (MovingCameraScene): target 0, never made."""
|
|
2381
|
+
|
|
2382
|
+
def __init__(self):
|
|
2383
|
+
self._id = 0
|
|
2384
|
+
self._path = None
|
|
2385
|
+
self._kids = None
|
|
2386
|
+
self._n = 0
|
|
2387
|
+
|
|
2388
|
+
def _t(self):
|
|
2389
|
+
return 0
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
class _Camera:
|
|
2393
|
+
def __init__(self):
|
|
2394
|
+
self.frame = CameraFrame()
|
|
2395
|
+
|
|
2396
|
+
@property
|
|
2397
|
+
def background_color(self):
|
|
2398
|
+
return config.background_color
|
|
2399
|
+
|
|
2400
|
+
@background_color.setter
|
|
2401
|
+
def background_color(self, value):
|
|
2402
|
+
config.background_color = value
|
|
2403
|
+
|
|
2404
|
+
|
|
2405
|
+
camera = _Camera()
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
# ---------------------------------------------------------------- .animate
|
|
2409
|
+
|
|
2410
|
+
class _Animate:
|
|
2411
|
+
"""m.animate.shift(UP).scale(2): records the ops, played as one animation."""
|
|
2412
|
+
|
|
2413
|
+
def __init__(self, mob):
|
|
2414
|
+
self._mob = mob
|
|
2415
|
+
self._ops = []
|
|
2416
|
+
self._kw = {}
|
|
2417
|
+
|
|
2418
|
+
def __call__(self, **kwargs):
|
|
2419
|
+
self._kw.update(kwargs)
|
|
2420
|
+
return self
|
|
2421
|
+
|
|
2422
|
+
def __getattr__(self, name):
|
|
2423
|
+
if name.startswith("_"):
|
|
2424
|
+
raise AttributeError(name)
|
|
2425
|
+
fn = getattr(self._mob, name)
|
|
2426
|
+
if not callable(fn):
|
|
2427
|
+
raise AttributeError("%s.animate.%s is not something that can be animated" % (
|
|
2428
|
+
type(self._mob).__name__, name))
|
|
2429
|
+
|
|
2430
|
+
def record(*args, **kwargs):
|
|
2431
|
+
st = _st()
|
|
2432
|
+
prev = st.capture
|
|
2433
|
+
st.capture = (self._mob._t(), self._ops)
|
|
2434
|
+
try:
|
|
2435
|
+
fn(*args, **kwargs)
|
|
2436
|
+
finally:
|
|
2437
|
+
st.capture = prev
|
|
2438
|
+
return self
|
|
2439
|
+
return record
|
|
2440
|
+
|
|
2441
|
+
def _anim(self):
|
|
2442
|
+
a = {"kind": "animate", "target": self._mob._t(), "args": {"ops": self._ops}}
|
|
2443
|
+
_timing(a, self._kw)
|
|
2444
|
+
return a
|
|
2445
|
+
|
|
2446
|
+
def _added(self):
|
|
2447
|
+
return [self._mob]
|
|
2448
|
+
|
|
2449
|
+
def _removed(self):
|
|
2450
|
+
return []
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
# ---------------------------------------------------------------- animations
|
|
2454
|
+
|
|
2455
|
+
def _timing(d, kw):
|
|
2456
|
+
for k in ("run_time", "lag_ratio"):
|
|
2457
|
+
if kw.get(k) is not None:
|
|
2458
|
+
d[k] = _num(kw[k])
|
|
2459
|
+
if kw.get("rate_func") is not None:
|
|
2460
|
+
d["rate_func"] = _rate(kw["rate_func"])
|
|
2461
|
+
return d
|
|
2462
|
+
|
|
2463
|
+
|
|
2464
|
+
class Animation:
|
|
2465
|
+
kind = "animation"
|
|
2466
|
+
|
|
2467
|
+
def __init__(self, mobject=None, *args, run_time=None, rate_func=None, lag_ratio=None, **kwargs):
|
|
2468
|
+
# Positional arguments after the mobject are the animation's own
|
|
2469
|
+
# (Circumscribe(m, Circle), Indicate(m, 1.5)), as in Manim.
|
|
2470
|
+
self.mobject = mobject
|
|
2471
|
+
self._kw = {"run_time": run_time, "rate_func": rate_func, "lag_ratio": lag_ratio}
|
|
2472
|
+
self._args = self._make_args(*args, **kwargs)
|
|
2473
|
+
|
|
2474
|
+
def _make_args(self, **kwargs):
|
|
2475
|
+
return {}
|
|
2476
|
+
|
|
2477
|
+
def _anim(self):
|
|
2478
|
+
t = self.mobject._t() if isinstance(self.mobject, Mobject) else None
|
|
2479
|
+
return _timing({"kind": self.kind, "target": t, "args": self._args}, self._kw)
|
|
2480
|
+
|
|
2481
|
+
def _added(self):
|
|
2482
|
+
return [self.mobject] if isinstance(self.mobject, Mobject) else []
|
|
2483
|
+
|
|
2484
|
+
def _removed(self):
|
|
2485
|
+
return []
|
|
2486
|
+
|
|
2487
|
+
@property
|
|
2488
|
+
def run_time(self):
|
|
2489
|
+
return self._kw.get("run_time") or 1.0
|
|
2490
|
+
|
|
2491
|
+
def set_run_time(self, run_time):
|
|
2492
|
+
self._kw["run_time"] = run_time
|
|
2493
|
+
return self
|
|
2494
|
+
|
|
2495
|
+
def set_rate_func(self, rate_func):
|
|
2496
|
+
self._kw["rate_func"] = rate_func
|
|
2497
|
+
return self
|
|
2498
|
+
|
|
2499
|
+
|
|
2500
|
+
class _Remover(Animation):
|
|
2501
|
+
def _removed(self):
|
|
2502
|
+
return [self.mobject]
|
|
2503
|
+
|
|
2504
|
+
|
|
2505
|
+
class Create(Animation):
|
|
2506
|
+
kind = "create"
|
|
2507
|
+
|
|
2508
|
+
|
|
2509
|
+
class ShowCreation(Create):
|
|
2510
|
+
pass
|
|
2511
|
+
|
|
2512
|
+
|
|
2513
|
+
class Uncreate(_Remover):
|
|
2514
|
+
kind = "uncreate"
|
|
2515
|
+
|
|
2516
|
+
|
|
2517
|
+
class Write(Animation):
|
|
2518
|
+
kind = "write"
|
|
2519
|
+
|
|
2520
|
+
def _make_args(self, reverse=False, **kwargs):
|
|
2521
|
+
return {"reverse": bool(reverse)}
|
|
2522
|
+
|
|
2523
|
+
|
|
2524
|
+
class Unwrite(_Remover):
|
|
2525
|
+
kind = "unwrite"
|
|
2526
|
+
|
|
2527
|
+
def _make_args(self, reverse=True, **kwargs):
|
|
2528
|
+
return {"reverse": bool(reverse)}
|
|
2529
|
+
|
|
2530
|
+
|
|
2531
|
+
class DrawBorderThenFill(Animation):
|
|
2532
|
+
kind = "draw_border_then_fill"
|
|
2533
|
+
|
|
2534
|
+
|
|
2535
|
+
class _Fade(Animation):
|
|
2536
|
+
def __init__(self, *mobjects, shift=None, target_position=None, scale=1, **kwargs):
|
|
2537
|
+
mobs = _flat(mobjects)
|
|
2538
|
+
self._many = mobs if len(mobs) > 1 else None
|
|
2539
|
+
self._fade_kw = {"shift": shift, "target_position": target_position, "scale": scale}
|
|
2540
|
+
Animation.__init__(self, mobs[0] if mobs else None, **kwargs)
|
|
2541
|
+
|
|
2542
|
+
def _make_args(self, **kwargs):
|
|
2543
|
+
args = {}
|
|
2544
|
+
kw = self._fade_kw
|
|
2545
|
+
if kw["shift"] is not None:
|
|
2546
|
+
args["shift"] = _P(kw["shift"])
|
|
2547
|
+
if kw["target_position"] is not None:
|
|
2548
|
+
args["target_position"] = _P(kw["target_position"])
|
|
2549
|
+
if kw["scale"] != 1:
|
|
2550
|
+
args["scale"] = _num(kw["scale"])
|
|
2551
|
+
return args
|
|
2552
|
+
|
|
2553
|
+
def _anim(self):
|
|
2554
|
+
if self._many is None:
|
|
2555
|
+
return Animation._anim(self)
|
|
2556
|
+
items = [{"kind": self.kind, "target": m._t(), "args": self._args} for m in self._many]
|
|
2557
|
+
return _timing({"kind": "group", "items": items}, self._kw)
|
|
2558
|
+
|
|
2559
|
+
def _added(self):
|
|
2560
|
+
return self._many or Animation._added(self)
|
|
2561
|
+
|
|
2562
|
+
|
|
2563
|
+
class FadeIn(_Fade):
|
|
2564
|
+
kind = "fade_in"
|
|
2565
|
+
|
|
2566
|
+
|
|
2567
|
+
class FadeOut(_Fade):
|
|
2568
|
+
kind = "fade_out"
|
|
2569
|
+
|
|
2570
|
+
def _removed(self):
|
|
2571
|
+
return self._added()
|
|
2572
|
+
|
|
2573
|
+
|
|
2574
|
+
class Transform(Animation):
|
|
2575
|
+
kind = "transform"
|
|
2576
|
+
|
|
2577
|
+
def __init__(self, mobject, target_mobject=None, path_arc=0, **kwargs):
|
|
2578
|
+
self.target_mobject = target_mobject
|
|
2579
|
+
if not isinstance(target_mobject, Mobject):
|
|
2580
|
+
raise TypeError("%s needs two mobjects: %s(a, b)" % (type(self).__name__, type(self).__name__))
|
|
2581
|
+
Animation.__init__(self, mobject, path_arc=path_arc, **kwargs)
|
|
2582
|
+
|
|
2583
|
+
def _make_args(self, path_arc=0, **kwargs):
|
|
2584
|
+
return {"target_mobject": self.target_mobject._embed(), "path_arc": _num(path_arc)}
|
|
2585
|
+
|
|
2586
|
+
|
|
2587
|
+
class ReplacementTransform(Transform):
|
|
2588
|
+
kind = "replacement_transform"
|
|
2589
|
+
|
|
2590
|
+
def _added(self):
|
|
2591
|
+
return [self.mobject, self.target_mobject]
|
|
2592
|
+
|
|
2593
|
+
def _removed(self):
|
|
2594
|
+
return [self.mobject]
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
class TransformFromCopy(Transform):
|
|
2598
|
+
kind = "transform_from_copy"
|
|
2599
|
+
|
|
2600
|
+
def _added(self):
|
|
2601
|
+
return [self.target_mobject]
|
|
2602
|
+
|
|
2603
|
+
|
|
2604
|
+
class TransformMatchingTex(ReplacementTransform):
|
|
2605
|
+
kind = "transform_matching_tex"
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
class TransformMatchingShapes(ReplacementTransform):
|
|
2609
|
+
kind = "transform_matching_shapes"
|
|
2610
|
+
|
|
2611
|
+
|
|
2612
|
+
class MoveToTarget(Transform):
|
|
2613
|
+
def __init__(self, mobject, **kwargs):
|
|
2614
|
+
target = getattr(mobject, "target", None)
|
|
2615
|
+
if target is None:
|
|
2616
|
+
raise TypeError("MoveToTarget needs m.generate_target() first")
|
|
2617
|
+
Transform.__init__(self, mobject, target, **kwargs)
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
class Restore(Animation):
|
|
2621
|
+
kind = "restore"
|
|
2622
|
+
|
|
2623
|
+
|
|
2624
|
+
class GrowFromCenter(Animation):
|
|
2625
|
+
kind = "grow_from_center"
|
|
2626
|
+
|
|
2627
|
+
|
|
2628
|
+
class GrowFromPoint(Animation):
|
|
2629
|
+
kind = "grow_from_point"
|
|
2630
|
+
|
|
2631
|
+
def __init__(self, mobject, point, **kwargs):
|
|
2632
|
+
self._point = point
|
|
2633
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
2634
|
+
|
|
2635
|
+
def _make_args(self, **kwargs):
|
|
2636
|
+
return {"point": _P(self._point)}
|
|
2637
|
+
|
|
2638
|
+
|
|
2639
|
+
class GrowFromEdge(Animation):
|
|
2640
|
+
kind = "grow_from_edge"
|
|
2641
|
+
|
|
2642
|
+
def __init__(self, mobject, edge, **kwargs):
|
|
2643
|
+
self._edge = edge
|
|
2644
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
2645
|
+
|
|
2646
|
+
def _make_args(self, **kwargs):
|
|
2647
|
+
return {"edge": _P(self._edge)}
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
class GrowArrow(Animation):
|
|
2651
|
+
kind = "grow_arrow"
|
|
2652
|
+
|
|
2653
|
+
|
|
2654
|
+
class SpinInFromNothing(Animation):
|
|
2655
|
+
kind = "spin_in_from_nothing"
|
|
2656
|
+
|
|
2657
|
+
def _make_args(self, angle=PI / 2, **kwargs):
|
|
2658
|
+
return {"angle": _num(angle)}
|
|
2659
|
+
|
|
2660
|
+
|
|
2661
|
+
class Indicate(Animation):
|
|
2662
|
+
kind = "indicate"
|
|
2663
|
+
|
|
2664
|
+
def _make_args(self, scale_factor=1.2, color=PURE_YELLOW, **kwargs):
|
|
2665
|
+
return {"scale_factor": _num(scale_factor), "color": _color(color)}
|
|
2666
|
+
|
|
2667
|
+
|
|
2668
|
+
class Circumscribe(Animation):
|
|
2669
|
+
kind = "circumscribe"
|
|
2670
|
+
|
|
2671
|
+
def _make_args(self, shape=None, fade_in=False, fade_out=False, time_width=0.3, buff=SMALL_BUFF,
|
|
2672
|
+
color=YELLOW, stroke_width=DEFAULT_STROKE_WIDTH, **kwargs):
|
|
2673
|
+
name = getattr(shape, "__name__", "Rectangle") if shape is not None else "Rectangle"
|
|
2674
|
+
return {"shape": "Circle" if name in ("Circle", "Ellipse") else "Rectangle", "color": _color(color),
|
|
2675
|
+
"buff": _num(buff), "fade_in": bool(fade_in), "fade_out": bool(fade_out),
|
|
2676
|
+
"time_width": _num(time_width), "stroke_width": _num(stroke_width)}
|
|
2677
|
+
|
|
2678
|
+
|
|
2679
|
+
class Flash(Animation):
|
|
2680
|
+
kind = "flash"
|
|
2681
|
+
|
|
2682
|
+
def __init__(self, point, line_length=0.2, num_lines=12, flash_radius=0.1, line_stroke_width=3,
|
|
2683
|
+
color=YELLOW, time_width=1, **kwargs):
|
|
2684
|
+
self._flash = {"point": _P(point), "color": _color(color), "line_length": _num(line_length),
|
|
2685
|
+
"num_lines": int(num_lines), "flash_radius": _num(flash_radius),
|
|
2686
|
+
"line_stroke_width": _num(line_stroke_width), "time_width": _num(time_width)}
|
|
2687
|
+
Animation.__init__(self, None, **kwargs)
|
|
2688
|
+
|
|
2689
|
+
def _make_args(self, **kwargs):
|
|
2690
|
+
return self._flash
|
|
2691
|
+
|
|
2692
|
+
|
|
2693
|
+
class Wiggle(Animation):
|
|
2694
|
+
kind = "wiggle"
|
|
2695
|
+
|
|
2696
|
+
def _make_args(self, scale_value=1.1, rotation_angle=0.01 * TAU, n_wiggles=6, **kwargs):
|
|
2697
|
+
return {"scale_value": _num(scale_value), "rotation_angle": _num(rotation_angle),
|
|
2698
|
+
"n_wiggles": int(n_wiggles)}
|
|
2699
|
+
|
|
2700
|
+
|
|
2701
|
+
class Rotate(Animation):
|
|
2702
|
+
kind = "rotate"
|
|
2703
|
+
|
|
2704
|
+
def __init__(self, mobject, angle=PI, axis=OUT, about_point=None, **kwargs):
|
|
2705
|
+
self._rot = {"angle": _num(angle), "axis": _P(axis), "about_point": _P(about_point)}
|
|
2706
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
2707
|
+
|
|
2708
|
+
def _make_args(self, **kwargs):
|
|
2709
|
+
return self._rot
|
|
2710
|
+
|
|
2711
|
+
|
|
2712
|
+
class Rotating(Rotate):
|
|
2713
|
+
"""A steady turn: a full turn over 5 s at constant speed, unless told otherwise."""
|
|
2714
|
+
|
|
2715
|
+
def __init__(self, mobject, axis=OUT, radians=TAU, about_point=None, about_edge=None, run_time=5,
|
|
2716
|
+
rate_func=linear, **kwargs):
|
|
2717
|
+
Rotate.__init__(self, mobject, radians, axis, about_point, run_time=run_time, rate_func=rate_func,
|
|
2718
|
+
**kwargs)
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
class MoveAlongPath(Animation):
|
|
2722
|
+
kind = "move_along_path"
|
|
2723
|
+
|
|
2724
|
+
def __init__(self, mobject, path, **kwargs):
|
|
2725
|
+
self._path_mob = path
|
|
2726
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
2727
|
+
|
|
2728
|
+
def _make_args(self, **kwargs):
|
|
2729
|
+
return {"path": self._path_mob._t()}
|
|
2730
|
+
|
|
2731
|
+
|
|
2732
|
+
class Wait(Animation):
|
|
2733
|
+
kind = "wait"
|
|
2734
|
+
|
|
2735
|
+
def __init__(self, run_time=1, **kwargs):
|
|
2736
|
+
Animation.__init__(self, None, run_time=run_time, **kwargs)
|
|
2737
|
+
|
|
2738
|
+
|
|
2739
|
+
def _as_anim(a, where="play()"):
|
|
2740
|
+
if isinstance(a, (Animation, _Animate)):
|
|
2741
|
+
return a
|
|
2742
|
+
if isinstance(a, Mobject):
|
|
2743
|
+
raise TypeError("%s takes animations, such as Create(m) or m.animate.shift(UP); got a %s"
|
|
2744
|
+
% (where, type(a).__name__))
|
|
2745
|
+
raise TypeError("%s takes animations, such as Create(m) or m.animate.shift(UP); got %r" % (where, a))
|
|
2746
|
+
|
|
2747
|
+
|
|
2748
|
+
class AnimationGroup(Animation):
|
|
2749
|
+
kind = "group"
|
|
2750
|
+
|
|
2751
|
+
def __init__(self, *animations, group=None, run_time=None, rate_func=None, lag_ratio=None, **kwargs):
|
|
2752
|
+
items = []
|
|
2753
|
+
for a in animations:
|
|
2754
|
+
if isinstance(a, (list, tuple, types.GeneratorType)):
|
|
2755
|
+
items.extend(_as_anim(x, type(self).__name__ + "()") for x in a)
|
|
2756
|
+
else:
|
|
2757
|
+
items.append(_as_anim(a, type(self).__name__ + "()"))
|
|
2758
|
+
self.animations = items
|
|
2759
|
+
Animation.__init__(self, None, run_time=run_time, rate_func=rate_func,
|
|
2760
|
+
lag_ratio=self._lag(lag_ratio))
|
|
2761
|
+
|
|
2762
|
+
def _lag(self, lag_ratio):
|
|
2763
|
+
return lag_ratio
|
|
2764
|
+
|
|
2765
|
+
def _anim(self):
|
|
2766
|
+
return _timing({"kind": self.kind, "items": [a._anim() for a in self.animations]}, self._kw)
|
|
2767
|
+
|
|
2768
|
+
def _added(self):
|
|
2769
|
+
return [m for a in self.animations for m in a._added()]
|
|
2770
|
+
|
|
2771
|
+
def _removed(self):
|
|
2772
|
+
return [m for a in self.animations for m in a._removed()]
|
|
2773
|
+
|
|
2774
|
+
|
|
2775
|
+
class Succession(AnimationGroup):
|
|
2776
|
+
kind = "succession"
|
|
2777
|
+
|
|
2778
|
+
|
|
2779
|
+
class LaggedStart(AnimationGroup):
|
|
2780
|
+
kind = "lagged"
|
|
2781
|
+
|
|
2782
|
+
def _lag(self, lag_ratio):
|
|
2783
|
+
return 0.05 if lag_ratio is None else lag_ratio
|
|
2784
|
+
|
|
2785
|
+
|
|
2786
|
+
def LaggedStartMap(AnimationClass, mobject, arg_creator=None, run_time=2, **kwargs):
|
|
2787
|
+
anims = [AnimationClass(*(arg_creator(m) if arg_creator else (m,))) for m in mobject]
|
|
2788
|
+
return LaggedStart(*anims, run_time=run_time, **kwargs)
|
|
2789
|
+
|
|
2790
|
+
|
|
2791
|
+
# ---------------------------------------------------------------- more geometry
|
|
2792
|
+
|
|
2793
|
+
class Angle(VMobject):
|
|
2794
|
+
def __init__(self, line1, line2, radius=None, quadrant=(1, 1), other_angle=False, dot=False,
|
|
2795
|
+
dot_radius=None, dot_distance=0.55, dot_color=WHITE, elbow=False, **kwargs):
|
|
2796
|
+
self._make("angle", {"line1": line1._embed(), "line2": line2._embed(),
|
|
2797
|
+
"radius": None if radius is None else _num(radius), "quadrant": list(quadrant),
|
|
2798
|
+
"other_angle": bool(other_angle), "dot": bool(dot),
|
|
2799
|
+
"dot_radius": None if dot_radius is None else _num(dot_radius),
|
|
2800
|
+
"dot_distance": _num(dot_distance), "dot_color": _color(dot_color),
|
|
2801
|
+
"elbow": bool(elbow)}, kwargs, n=None)
|
|
2802
|
+
|
|
2803
|
+
def get_value(self, degrees=False):
|
|
2804
|
+
v = Expr(["get", self._ref(), "value"], "s")
|
|
2805
|
+
return v / DEGREES if degrees else v
|
|
2806
|
+
|
|
2807
|
+
@staticmethod
|
|
2808
|
+
def from_three_points(A, B, C, **kwargs):
|
|
2809
|
+
return Angle(Line(B, A), Line(B, C), **kwargs)
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
class RightAngle(Angle):
|
|
2813
|
+
def __init__(self, line1, line2, length=None, **kwargs):
|
|
2814
|
+
self._make("right_angle", {"line1": line1._embed(), "line2": line2._embed(),
|
|
2815
|
+
"length": None if length is None else _num(length)}, kwargs, n=None)
|
|
2816
|
+
|
|
2817
|
+
|
|
2818
|
+
class AnnularSector(Arc):
|
|
2819
|
+
def __init__(self, inner_radius=1, outer_radius=2, angle=TAU / 4, start_angle=0, fill_opacity=1,
|
|
2820
|
+
stroke_width=0, color=WHITE, arc_center=ORIGIN, **kwargs):
|
|
2821
|
+
self._make("annular_sector", {"inner_radius": _num(inner_radius), "outer_radius": _num(outer_radius),
|
|
2822
|
+
"angle": _num(angle), "start_angle": _num(start_angle),
|
|
2823
|
+
"arc_center": _P(arc_center)},
|
|
2824
|
+
dict(kwargs, fill_opacity=fill_opacity, stroke_width=stroke_width, color=color), n=0)
|
|
2825
|
+
|
|
2826
|
+
|
|
2827
|
+
class Sector(AnnularSector):
|
|
2828
|
+
def __init__(self, radius=1, angle=TAU / 4, start_angle=0, fill_opacity=1, stroke_width=0, color=WHITE,
|
|
2829
|
+
arc_center=ORIGIN, **kwargs):
|
|
2830
|
+
self._make("sector", {"radius": _num(radius), "angle": _num(angle), "start_angle": _num(start_angle),
|
|
2831
|
+
"arc_center": _P(arc_center)},
|
|
2832
|
+
dict(kwargs, fill_opacity=fill_opacity, stroke_width=stroke_width, color=color), n=0)
|
|
2833
|
+
|
|
2834
|
+
|
|
2835
|
+
class ArcBetweenPoints(Arc):
|
|
2836
|
+
_kind = "arc_between_points"
|
|
2837
|
+
|
|
2838
|
+
def __init__(self, start, end, angle=TAU / 4, radius=None, **kwargs):
|
|
2839
|
+
self._make(self._kind, {"start": _P(start), "end": _P(end), "angle": _num(angle),
|
|
2840
|
+
"radius": None if radius is None else _num(radius)}, kwargs, n=self._parts_n)
|
|
2841
|
+
|
|
2842
|
+
_parts_n = 0
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
class CurvedArrow(ArcBetweenPoints):
|
|
2846
|
+
_kind = "curved_arrow"
|
|
2847
|
+
_parts_n = 1
|
|
2848
|
+
|
|
2849
|
+
def __init__(self, start_point, end_point, **kwargs):
|
|
2850
|
+
ArcBetweenPoints.__init__(self, start_point, end_point, **kwargs)
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
class CurvedDoubleArrow(CurvedArrow):
|
|
2854
|
+
_kind = "curved_double_arrow"
|
|
2855
|
+
_parts_n = 2
|
|
2856
|
+
|
|
2857
|
+
|
|
2858
|
+
class Elbow(VMobject):
|
|
2859
|
+
def __init__(self, width=0.2, angle=0, **kwargs):
|
|
2860
|
+
self._make("elbow", {"width": _num(width), "angle": _num(angle)}, kwargs, n=0)
|
|
2861
|
+
|
|
2862
|
+
|
|
2863
|
+
class Cross(VGroup):
|
|
2864
|
+
def __init__(self, mobject=None, stroke_color=RED, stroke_width=6.0, scale_factor=1.0, **kwargs):
|
|
2865
|
+
self._kids = None
|
|
2866
|
+
self._make("cross", {"mobject": mobject._embed() if mobject is not None else None,
|
|
2867
|
+
"scale_factor": _num(scale_factor)},
|
|
2868
|
+
dict(kwargs, color=stroke_color, stroke_color=stroke_color, fill_color=WHITE,
|
|
2869
|
+
stroke_width=stroke_width), n=2)
|
|
2870
|
+
|
|
2871
|
+
|
|
2872
|
+
class Underline(Line):
|
|
2873
|
+
def __init__(self, mobject, buff=SMALL_BUFF, **kwargs):
|
|
2874
|
+
self._make("underline", {"mobject": mobject._embed(), "buff": _num(buff)}, kwargs, n=0)
|
|
2875
|
+
|
|
2876
|
+
|
|
2877
|
+
class LabeledDot(Dot):
|
|
2878
|
+
def __init__(self, label, radius=None, buff=SMALL_BUFF, **kwargs):
|
|
2879
|
+
lab = label._embed() if isinstance(label, Mobject) else str(label)
|
|
2880
|
+
self._make("labeled_dot", {"label": lab, "radius": None if radius is None else _num(radius),
|
|
2881
|
+
"buff": _num(buff)}, kwargs, _FILLED, n=1)
|
|
2882
|
+
|
|
2883
|
+
|
|
2884
|
+
class AnnotationDot(Dot):
|
|
2885
|
+
def __init__(self, radius=DEFAULT_DOT_RADIUS * 1.3, stroke_width=5, stroke_color=WHITE, fill_color=BLUE,
|
|
2886
|
+
**kwargs):
|
|
2887
|
+
Dot.__init__(self, radius=radius, stroke_width=stroke_width, stroke_color=stroke_color,
|
|
2888
|
+
fill_color=fill_color, **kwargs)
|
|
2889
|
+
|
|
2890
|
+
|
|
2891
|
+
class DashedVMobject(VMobject):
|
|
2892
|
+
def __init__(self, vmobject, num_dashes=15, dashed_ratio=0.5, dash_offset=0, color=None,
|
|
2893
|
+
equal_lengths=True, **kwargs):
|
|
2894
|
+
# The dashes keep the object's own style (a Circle's dashes are red).
|
|
2895
|
+
self._make("dashed_vmobject", {"vmobject": vmobject._embed(), "num_dashes": int(num_dashes),
|
|
2896
|
+
"dashed_ratio": _num(dashed_ratio), "dash_offset": _num(dash_offset),
|
|
2897
|
+
"equal_lengths": bool(equal_lengths)},
|
|
2898
|
+
dict(kwargs, color=color), {}, n=int(num_dashes))
|
|
2899
|
+
|
|
2900
|
+
|
|
2901
|
+
class CubicBezier(VMobject):
|
|
2902
|
+
def __init__(self, start_anchor, start_handle, end_handle, end_anchor, **kwargs):
|
|
2903
|
+
self._make("cubic_bezier", {"start_anchor": _P(start_anchor), "start_handle": _P(start_handle),
|
|
2904
|
+
"end_handle": _P(end_handle), "end_anchor": _P(end_anchor)}, kwargs, n=0)
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
class TangentLine(Line):
|
|
2908
|
+
def __init__(self, vmob, alpha, length=1, d_alpha=1e-6, **kwargs):
|
|
2909
|
+
self._make("tangent_line", {"vmobject": vmob._embed(), "alpha": _num(alpha), "length": _num(length)},
|
|
2910
|
+
kwargs, n=0)
|
|
2911
|
+
|
|
2912
|
+
|
|
2913
|
+
# ---------------------------------------------------------------- text and layout
|
|
2914
|
+
|
|
2915
|
+
def _text_kwargs(kwargs):
|
|
2916
|
+
return {k: kwargs.pop(k) for k in ("font_size", "font", "weight", "slant", "line_spacing", "letter_spacing")
|
|
2917
|
+
if k in kwargs}
|
|
2918
|
+
|
|
2919
|
+
|
|
2920
|
+
class Paragraph(VGroup):
|
|
2921
|
+
def __init__(self, *text, line_spacing=-1, alignment=None, **kwargs):
|
|
2922
|
+
lines = "\n".join(str(t) for t in text).split("\n")
|
|
2923
|
+
self.lines_text = lines
|
|
2924
|
+
self._kids = None
|
|
2925
|
+
geom = {"lines": lines, "line_spacing": _num(line_spacing), "alignment": alignment,
|
|
2926
|
+
"font_size": DEFAULT_FONT_SIZE}
|
|
2927
|
+
geom.update(_text_kwargs(kwargs))
|
|
2928
|
+
self._make("paragraph", geom, kwargs, _FILLED, n=len(lines))
|
|
2929
|
+
|
|
2930
|
+
|
|
2931
|
+
class BulletedList(VGroup):
|
|
2932
|
+
def __init__(self, *items, buff=MED_LARGE_BUFF, dot_scale_factor=2, tex_environment=None, dot_buff=SMALL_BUFF,
|
|
2933
|
+
**kwargs):
|
|
2934
|
+
self.items = [str(i) for i in items]
|
|
2935
|
+
self._kids = None
|
|
2936
|
+
geom = {"items": self.items, "buff": _num(buff), "dot_scale_factor": _num(dot_scale_factor),
|
|
2937
|
+
"dot_buff": _num(dot_buff), "font_size": DEFAULT_FONT_SIZE}
|
|
2938
|
+
geom.update(_text_kwargs(kwargs))
|
|
2939
|
+
self._make("bulleted_list", geom, kwargs, _FILLED, n=len(self.items))
|
|
2940
|
+
|
|
2941
|
+
def fade_all_but(self, index, opacity=0.5):
|
|
2942
|
+
for i in range(len(self.items)):
|
|
2943
|
+
self[i].set_opacity(1.0 if i == index else opacity)
|
|
2944
|
+
return self
|
|
2945
|
+
|
|
2946
|
+
|
|
2947
|
+
class MarkupText(Text):
|
|
2948
|
+
def __init__(self, text, fill_opacity=1, stroke_width=0, color=None, font_size=DEFAULT_FONT_SIZE,
|
|
2949
|
+
line_spacing=-1, font="", slant=NORMAL, weight=NORMAL, justify=False, **kwargs):
|
|
2950
|
+
import re
|
|
2951
|
+
plain = re.sub(r"&[a-z]+;", "x", re.sub(r"<[^>]*>", "", str(text)))
|
|
2952
|
+
self.text = str(text)
|
|
2953
|
+
self._make("markup_text", {"text": str(text), "font_size": _num(font_size), "line_spacing": _num(line_spacing),
|
|
2954
|
+
"font": font, "slant": slant, "weight": weight, "justify": bool(justify)},
|
|
2955
|
+
dict(kwargs, color=color or WHITE, fill_opacity=fill_opacity, stroke_width=stroke_width),
|
|
2956
|
+
_FILLED, n=sum(1 for ch in plain if not ch.isspace()))
|
|
2957
|
+
|
|
2958
|
+
|
|
2959
|
+
def _entry(v, element):
|
|
2960
|
+
"""A table or matrix entry: a string the engine sets in the element kind, or an existing mobject."""
|
|
2961
|
+
if isinstance(v, Mobject):
|
|
2962
|
+
return v._embed()
|
|
2963
|
+
return str(v)
|
|
2964
|
+
|
|
2965
|
+
|
|
2966
|
+
def _element_of(element_to_mobject, default):
|
|
2967
|
+
names = {MathTex: "mathtex", Tex: "tex", Text: "paragraph", Paragraph: "paragraph",
|
|
2968
|
+
DecimalNumber: "mathtex", Integer: "mathtex"}
|
|
2969
|
+
return names.get(element_to_mobject, default)
|
|
2970
|
+
|
|
2971
|
+
|
|
2972
|
+
def _format_entries(rows, element_to_mobject, config):
|
|
2973
|
+
if element_to_mobject in (Integer,):
|
|
2974
|
+
return [["%d" % int(round(float(x))) for x in r] for r in rows]
|
|
2975
|
+
if element_to_mobject in (DecimalNumber,):
|
|
2976
|
+
places = int((config or {}).get("num_decimal_places", 2))
|
|
2977
|
+
return [[("%." + str(places) + "f") % float(x) for x in r] for r in rows]
|
|
2978
|
+
return rows
|
|
2979
|
+
|
|
2980
|
+
|
|
2981
|
+
class Table(VGroup):
|
|
2982
|
+
_element = "paragraph"
|
|
2983
|
+
|
|
2984
|
+
def __init__(self, table, row_labels=None, col_labels=None, top_left_entry=None, v_buff=0.8, h_buff=1.3,
|
|
2985
|
+
include_outer_lines=False, include_inner_lines=True, add_background_rectangles_to_entries=False,
|
|
2986
|
+
entries_background_color=BLACK, include_background_rectangle=False,
|
|
2987
|
+
background_rectangle_color=BLACK, element_to_mobject=None, element_to_mobject_config=None,
|
|
2988
|
+
arrange_in_grid_config=None, line_config=None, **kwargs):
|
|
2989
|
+
rows = [list(r) for r in table]
|
|
2990
|
+
rows = _format_entries(rows, element_to_mobject, element_to_mobject_config)
|
|
2991
|
+
element = _element_of(element_to_mobject, self._element)
|
|
2992
|
+
self.n_rows = len(rows) + (1 if col_labels else 0)
|
|
2993
|
+
self.n_cols = (len(rows[0]) if rows else 0) + (1 if row_labels else 0)
|
|
2994
|
+
self._kids = None
|
|
2995
|
+
self._make("table", {
|
|
2996
|
+
"table": [[_entry(v, element) for v in r] for r in rows], "element": element,
|
|
2997
|
+
"row_labels": [m._embed() for m in row_labels] if row_labels else None,
|
|
2998
|
+
"col_labels": [m._embed() for m in col_labels] if col_labels else None,
|
|
2999
|
+
"top_left_entry": top_left_entry._embed() if top_left_entry is not None else None,
|
|
3000
|
+
"v_buff": _num(v_buff), "h_buff": _num(h_buff), "include_outer_lines": bool(include_outer_lines),
|
|
3001
|
+
"include_inner_lines": bool(include_inner_lines),
|
|
3002
|
+
"include_background_rectangle": bool(include_background_rectangle),
|
|
3003
|
+
"background_rectangle_color": _color(background_rectangle_color),
|
|
3004
|
+
"add_background_rectangles_to_entries": bool(add_background_rectangles_to_entries),
|
|
3005
|
+
"entries_background_color": _color(entries_background_color),
|
|
3006
|
+
"line_config": _style_dict(line_config),
|
|
3007
|
+
}, kwargs, {}, n=3)
|
|
3008
|
+
|
|
3009
|
+
def get_entries(self, pos=None):
|
|
3010
|
+
if pos is None:
|
|
3011
|
+
return self[0]
|
|
3012
|
+
r, c = pos
|
|
3013
|
+
return self[0][(r - 1) * self.n_cols + (c - 1)]
|
|
3014
|
+
|
|
3015
|
+
def get_entries_without_labels(self, pos=None):
|
|
3016
|
+
return self.get_entries(pos)
|
|
3017
|
+
|
|
3018
|
+
def get_rows(self):
|
|
3019
|
+
return VGroup(*[VGroup(*[self[0][r * self.n_cols + c] for c in range(self.n_cols)])
|
|
3020
|
+
for r in range(self.n_rows)])
|
|
3021
|
+
|
|
3022
|
+
def get_columns(self):
|
|
3023
|
+
return VGroup(*[VGroup(*[self[0][r * self.n_cols + c] for r in range(self.n_rows)])
|
|
3024
|
+
for c in range(self.n_cols)])
|
|
3025
|
+
|
|
3026
|
+
def get_horizontal_lines(self):
|
|
3027
|
+
return self[1]
|
|
3028
|
+
|
|
3029
|
+
def get_vertical_lines(self):
|
|
3030
|
+
return self[2]
|
|
3031
|
+
|
|
3032
|
+
def get_cell(self, pos, **kwargs):
|
|
3033
|
+
return SurroundingRectangle(self.get_entries(pos), buff=0.1, **kwargs)
|
|
3034
|
+
|
|
3035
|
+
|
|
3036
|
+
class MathTable(Table):
|
|
3037
|
+
_element = "mathtex"
|
|
3038
|
+
|
|
3039
|
+
|
|
3040
|
+
class IntegerTable(Table):
|
|
3041
|
+
def __init__(self, table, element_to_mobject=Integer, **kwargs):
|
|
3042
|
+
Table.__init__(self, table, element_to_mobject=element_to_mobject, **kwargs)
|
|
3043
|
+
|
|
3044
|
+
|
|
3045
|
+
class DecimalTable(Table):
|
|
3046
|
+
def __init__(self, table, element_to_mobject=DecimalNumber, element_to_mobject_config=None, **kwargs):
|
|
3047
|
+
Table.__init__(self, table, element_to_mobject=element_to_mobject,
|
|
3048
|
+
element_to_mobject_config=element_to_mobject_config or {"num_decimal_places": 1}, **kwargs)
|
|
3049
|
+
|
|
3050
|
+
|
|
3051
|
+
class Matrix(VMobject):
|
|
3052
|
+
_default_element = MathTex
|
|
3053
|
+
_default_config = None
|
|
3054
|
+
|
|
3055
|
+
def __init__(self, matrix, v_buff=0.8, h_buff=1.3, bracket_h_buff=MED_SMALL_BUFF,
|
|
3056
|
+
bracket_v_buff=MED_SMALL_BUFF, add_background_rectangles_to_entries=False,
|
|
3057
|
+
include_background_rectangle=False, element_to_mobject=None, element_to_mobject_config=None,
|
|
3058
|
+
element_alignment_corner=DR, left_bracket="[", right_bracket="]", stretch_brackets=True,
|
|
3059
|
+
bracket_config=None, **kwargs):
|
|
3060
|
+
rows = [list(r) if isinstance(r, (list, tuple)) or hasattr(r, "tolist") else [r]
|
|
3061
|
+
for r in _unarray(matrix)]
|
|
3062
|
+
rows = [list(_unarray(r)) for r in rows]
|
|
3063
|
+
element_to_mobject = element_to_mobject or self._default_element
|
|
3064
|
+
rows = _format_entries(rows, element_to_mobject, element_to_mobject_config or self._default_config)
|
|
3065
|
+
self.n_rows = len(rows)
|
|
3066
|
+
self.n_cols = len(rows[0]) if rows else 0
|
|
3067
|
+
self._make("matrix", {
|
|
3068
|
+
"matrix": [[_entry(v, "mathtex") for v in r] for r in rows], "v_buff": _num(v_buff),
|
|
3069
|
+
"h_buff": _num(h_buff), "bracket_h_buff": _num(bracket_h_buff), "bracket_v_buff": _num(bracket_v_buff),
|
|
3070
|
+
"left_bracket": left_bracket, "right_bracket": right_bracket,
|
|
3071
|
+
"element_alignment_corner": _P(element_alignment_corner),
|
|
3072
|
+
"include_background_rectangle": bool(include_background_rectangle),
|
|
3073
|
+
"add_background_rectangles_to_entries": bool(add_background_rectangles_to_entries),
|
|
3074
|
+
}, kwargs, _FILLED, n=3)
|
|
3075
|
+
|
|
3076
|
+
def get_entries(self):
|
|
3077
|
+
return self[0]
|
|
3078
|
+
|
|
3079
|
+
def get_mob_matrix(self):
|
|
3080
|
+
return [[self[0][r * self.n_cols + c] for c in range(self.n_cols)] for r in range(self.n_rows)]
|
|
3081
|
+
|
|
3082
|
+
def get_rows(self):
|
|
3083
|
+
return VGroup(*[VGroup(*row) for row in self.get_mob_matrix()])
|
|
3084
|
+
|
|
3085
|
+
def get_columns(self):
|
|
3086
|
+
return VGroup(*[VGroup(*[self[0][r * self.n_cols + c] for r in range(self.n_rows)])
|
|
3087
|
+
for c in range(self.n_cols)])
|
|
3088
|
+
|
|
3089
|
+
def get_brackets(self):
|
|
3090
|
+
return VGroup(self[1], self[2])
|
|
3091
|
+
|
|
3092
|
+
def set_column_colors(self, *colors):
|
|
3093
|
+
for col, color in zip(self.get_columns(), colors):
|
|
3094
|
+
col.set_color(color)
|
|
3095
|
+
return self
|
|
3096
|
+
|
|
3097
|
+
def set_row_colors(self, *colors):
|
|
3098
|
+
for row, color in zip(self.get_rows(), colors):
|
|
3099
|
+
row.set_color(color)
|
|
3100
|
+
return self
|
|
3101
|
+
|
|
3102
|
+
|
|
3103
|
+
class IntegerMatrix(Matrix):
|
|
3104
|
+
_default_element = Integer
|
|
3105
|
+
|
|
3106
|
+
|
|
3107
|
+
class DecimalMatrix(Matrix):
|
|
3108
|
+
_default_element = DecimalNumber
|
|
3109
|
+
_default_config = {"num_decimal_places": 1}
|
|
3110
|
+
|
|
3111
|
+
|
|
3112
|
+
class MobjectMatrix(Matrix):
|
|
3113
|
+
pass
|
|
3114
|
+
|
|
3115
|
+
|
|
3116
|
+
_CODE_LANGS = {".py": "python", ".js": "javascript", ".ts": "typescript", ".c": "c", ".cpp": "cpp",
|
|
3117
|
+
".java": "java", ".rs": "rust", ".go": "go", ".rb": "ruby", ".sh": "bash", ".html": "html",
|
|
3118
|
+
".css": "css", ".json": "json", ".lua": "lua", ".swift": "swift", ".kt": "kotlin"}
|
|
3119
|
+
|
|
3120
|
+
|
|
3121
|
+
class Code(VMobject):
|
|
3122
|
+
def __init__(self, code_file=None, code_string=None, language=None, formatter_style="vim", tab_width=4,
|
|
3123
|
+
add_line_numbers=True, line_numbers_from=1, background="rectangle", background_config=None,
|
|
3124
|
+
paragraph_config=None, **kwargs):
|
|
3125
|
+
if code_string is None and code_file is not None:
|
|
3126
|
+
with open(code_file) as fh:
|
|
3127
|
+
code_string = fh.read()
|
|
3128
|
+
code = (code_string or "").expandtabs(tab_width).rstrip("\n")
|
|
3129
|
+
if language is None and code_file:
|
|
3130
|
+
language = _CODE_LANGS.get("." + str(code_file).rsplit(".", 1)[-1].lower())
|
|
3131
|
+
self.code_lines = code.split("\n")
|
|
3132
|
+
self._make("code", {"code": code, "language": language or "python", "tab_width": int(tab_width),
|
|
3133
|
+
"add_line_numbers": bool(add_line_numbers), "line_numbers_from": int(line_numbers_from),
|
|
3134
|
+
"background": background, "formatter_style": formatter_style,
|
|
3135
|
+
"background_config": _style_dict(background_config),
|
|
3136
|
+
"paragraph_config": _style_dict(paragraph_config)}, kwargs, {},
|
|
3137
|
+
n=3 if add_line_numbers else 2)
|
|
3138
|
+
|
|
3139
|
+
@property
|
|
3140
|
+
def code(self):
|
|
3141
|
+
return self[self._n - 1]
|
|
3142
|
+
|
|
3143
|
+
@property
|
|
3144
|
+
def line_numbers(self):
|
|
3145
|
+
return self[1] if self._n == 3 else None
|
|
3146
|
+
|
|
3147
|
+
@property
|
|
3148
|
+
def background(self):
|
|
3149
|
+
return self[0]
|
|
3150
|
+
|
|
3151
|
+
|
|
3152
|
+
# ---------------------------------------------------------------- more graphing
|
|
3153
|
+
|
|
3154
|
+
class ComplexPlane(NumberPlane):
|
|
3155
|
+
_kind = "complex_plane"
|
|
3156
|
+
|
|
3157
|
+
def number_to_point(self, number):
|
|
3158
|
+
z = complex(number)
|
|
3159
|
+
return self.c2p(z.real, z.imag)
|
|
3160
|
+
|
|
3161
|
+
n2p = number_to_point
|
|
3162
|
+
|
|
3163
|
+
|
|
3164
|
+
class PolarPlane(Axes):
|
|
3165
|
+
def __init__(self, radius_max=None, size=None, radius_step=1, azimuth_step=None, azimuth_units="PI radians",
|
|
3166
|
+
azimuth_compact_fraction=True, azimuth_offset=0, azimuth_direction="CCW",
|
|
3167
|
+
azimuth_label_buff=SMALL_BUFF, azimuth_label_font_size=24, radius_config=None,
|
|
3168
|
+
background_line_style=None, faded_line_style=None, faded_line_ratio=1, **kwargs):
|
|
3169
|
+
w, h = _frame_size()
|
|
3170
|
+
self.radius_max = float(radius_max) if radius_max is not None else h / 2
|
|
3171
|
+
self.size = float(size) if size is not None else 2 * self.radius_max
|
|
3172
|
+
self.x_range = self.y_range = [-self.radius_max, self.radius_max, radius_step]
|
|
3173
|
+
self._make("polar_plane", {
|
|
3174
|
+
"radius_max": self.radius_max, "size": self.size, "radius_step": _num(radius_step),
|
|
3175
|
+
"azimuth_step": azimuth_step, "azimuth_units": azimuth_units,
|
|
3176
|
+
"azimuth_compact_fraction": bool(azimuth_compact_fraction), "azimuth_offset": _num(azimuth_offset),
|
|
3177
|
+
"azimuth_direction": azimuth_direction, "azimuth_label_buff": _num(azimuth_label_buff),
|
|
3178
|
+
"azimuth_label_font_size": _num(azimuth_label_font_size), "radius_config": _style_dict(radius_config),
|
|
3179
|
+
"background_line_style": _style_dict(background_line_style),
|
|
3180
|
+
"faded_line_style": _style_dict(faded_line_style), "faded_line_ratio": int(faded_line_ratio),
|
|
3181
|
+
}, kwargs, n=4)
|
|
3182
|
+
|
|
3183
|
+
_axis_index = (2, 3)
|
|
3184
|
+
|
|
3185
|
+
def polar_to_point(self, radius, azimuth):
|
|
3186
|
+
r, a = _np_scalar(radius), _np_scalar(azimuth)
|
|
3187
|
+
return self.c2p(r * np.cos(a), r * np.sin(a))
|
|
3188
|
+
|
|
3189
|
+
pr2pt = polar_to_point
|
|
3190
|
+
|
|
3191
|
+
|
|
3192
|
+
def _np_scalar(v):
|
|
3193
|
+
return v if isinstance(v, Expr) else float(v)
|
|
3194
|
+
|
|
3195
|
+
|
|
3196
|
+
class BarChart(Axes):
|
|
3197
|
+
def __init__(self, values, bar_names=None, y_range=None, x_length=None, y_length=None,
|
|
3198
|
+
bar_colors=("#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"), bar_width=0.6,
|
|
3199
|
+
bar_fill_opacity=0.7, bar_stroke_width=3, **kwargs):
|
|
3200
|
+
self.values = [float(v) for v in values]
|
|
3201
|
+
self.x_range = [0, len(self.values), 1]
|
|
3202
|
+
self.y_range = list(y_range) if y_range is not None else None
|
|
3203
|
+
self._make("bar_chart", {
|
|
3204
|
+
"values": self.values, "bar_names": [str(n) for n in bar_names] if bar_names else None,
|
|
3205
|
+
"y_range": self.y_range, "x_length": None if x_length is None else _num(x_length),
|
|
3206
|
+
"y_length": None if y_length is None else _num(y_length),
|
|
3207
|
+
"bar_colors": [_color(c) for c in bar_colors], "bar_width": _num(bar_width),
|
|
3208
|
+
"bar_fill_opacity": _num(bar_fill_opacity), "bar_stroke_width": _num(bar_stroke_width),
|
|
3209
|
+
}, kwargs, n=3)
|
|
3210
|
+
|
|
3211
|
+
@property
|
|
3212
|
+
def bars(self):
|
|
3213
|
+
return self[2]
|
|
3214
|
+
|
|
3215
|
+
def get_bar_labels(self, color=None, font_size=24, buff=MED_SMALL_BUFF, label_constructor=None):
|
|
3216
|
+
label_constructor = label_constructor or Tex
|
|
3217
|
+
labels = []
|
|
3218
|
+
for i, v in enumerate(self.values):
|
|
3219
|
+
lab = label_constructor(("%g" % v), font_size=font_size)
|
|
3220
|
+
if color is not None:
|
|
3221
|
+
lab.set_color(color)
|
|
3222
|
+
lab.next_to(self[2][i], UP if v >= 0 else DOWN, buff=buff)
|
|
3223
|
+
labels.append(lab)
|
|
3224
|
+
return VGroup(*labels)
|
|
3225
|
+
|
|
3226
|
+
def change_bar_values(self, values, update_colors=True):
|
|
3227
|
+
self.values = [float(v) for v in values]
|
|
3228
|
+
return self._op("change_bar_values", {"values": self.values})
|
|
3229
|
+
|
|
3230
|
+
|
|
3231
|
+
class GenericGraph(VMobject):
|
|
3232
|
+
"""Vertices and edges, laid out automatically (Manim's Graph)."""
|
|
3233
|
+
_directed = False
|
|
3234
|
+
|
|
3235
|
+
def __init__(self, vertices, edges, labels=False, label_fill_color=BLACK, layout="spring", layout_scale=2,
|
|
3236
|
+
layout_config=None, vertex_type=None, vertex_config=None, vertex_mobjects=None, edge_type=None,
|
|
3237
|
+
partitions=None, root_vertex=None, edge_config=None, **kwargs):
|
|
3238
|
+
self._vlist = list(vertices)
|
|
3239
|
+
index = {v: i for i, v in enumerate(self._vlist)}
|
|
3240
|
+
self._elist = [tuple(e) for e in edges]
|
|
3241
|
+
if isinstance(layout, dict):
|
|
3242
|
+
layout = {str(index[v]): _P(p) for v, p in layout.items()}
|
|
3243
|
+
|
|
3244
|
+
def per_vertex(cfg):
|
|
3245
|
+
cfg = dict(cfg or {})
|
|
3246
|
+
if cfg and all(k in index for k in cfg):
|
|
3247
|
+
return {"per": {str(index[k]): _style_dict(c) for k, c in cfg.items()}}
|
|
3248
|
+
return _style_dict(cfg)
|
|
3249
|
+
|
|
3250
|
+
def per_edge(cfg):
|
|
3251
|
+
cfg = dict(cfg or {})
|
|
3252
|
+
if cfg and all(isinstance(k, tuple) for k in cfg):
|
|
3253
|
+
return {"per": {"%d,%d" % (index[a], index[b]): _style_dict(c) for (a, b), c in cfg.items()}}
|
|
3254
|
+
return _style_dict(cfg)
|
|
3255
|
+
|
|
3256
|
+
self._make("network_graph", {
|
|
3257
|
+
"vertices": [str(v) for v in self._vlist], "edges": [[index[a], index[b]] for a, b in self._elist],
|
|
3258
|
+
"directed": self._directed, "labels": bool(labels) if not isinstance(labels, dict) else
|
|
3259
|
+
{str(index[k]): str(v) for k, v in labels.items()}, "label_fill_color": _color(label_fill_color),
|
|
3260
|
+
"layout": layout, "layout_scale": _j(layout_scale), "layout_config": _j(layout_config or {}),
|
|
3261
|
+
"root_vertex": index.get(root_vertex) if root_vertex is not None else None,
|
|
3262
|
+
"partitions": [[index[v] for v in p] for p in partitions] if partitions else None,
|
|
3263
|
+
"vertex_config": per_vertex(vertex_config), "edge_config": per_edge(edge_config),
|
|
3264
|
+
}, kwargs, {}, n=2)
|
|
3265
|
+
self.vertices = {v: _Part(_Part(self, 0), i) for i, v in enumerate(self._vlist)}
|
|
3266
|
+
self.edges = {e: _Part(_Part(self, 1), j) for j, e in enumerate(self._elist)}
|
|
3267
|
+
|
|
3268
|
+
def __getitem__(self, key):
|
|
3269
|
+
if isinstance(key, int) and not isinstance(key, bool) and key in (0, 1) and key not in self._vertex_keys():
|
|
3270
|
+
return Mobject.__getitem__(self, key)
|
|
3271
|
+
if key in self._vertex_keys():
|
|
3272
|
+
return self.vertices[key]
|
|
3273
|
+
return Mobject.__getitem__(self, key)
|
|
3274
|
+
|
|
3275
|
+
def _vertex_keys(self):
|
|
3276
|
+
return self.__dict__.get("vertices") or {}
|
|
3277
|
+
|
|
3278
|
+
def change_layout(self, layout="spring", layout_scale=2, layout_config=None, root_vertex=None, **kwargs):
|
|
3279
|
+
index = {v: i for i, v in enumerate(self._vlist)}
|
|
3280
|
+
if isinstance(layout, dict):
|
|
3281
|
+
layout = {str(index[v]): _P(p) for v, p in layout.items()}
|
|
3282
|
+
return self._op("change_layout", {"layout": layout, "layout_scale": _j(layout_scale),
|
|
3283
|
+
"layout_config": _j(layout_config or {}),
|
|
3284
|
+
"root_vertex": index.get(root_vertex)})
|
|
3285
|
+
|
|
3286
|
+
|
|
3287
|
+
class Graph(GenericGraph):
|
|
3288
|
+
pass
|
|
3289
|
+
|
|
3290
|
+
|
|
3291
|
+
class DiGraph(GenericGraph):
|
|
3292
|
+
_directed = True
|
|
3293
|
+
|
|
3294
|
+
|
|
3295
|
+
# ---------------------------------------------------------------- pictures
|
|
3296
|
+
|
|
3297
|
+
_MIME = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif",
|
|
3298
|
+
"webp": "image/webp", "svg": "image/svg+xml", "bmp": "image/bmp"}
|
|
3299
|
+
|
|
3300
|
+
|
|
3301
|
+
class SVGMobject(VMobject):
|
|
3302
|
+
def __init__(self, file_name=None, should_center=True, height=2, width=None, color=None, opacity=None,
|
|
3303
|
+
fill_color=None, fill_opacity=None, stroke_color=None, stroke_opacity=None, stroke_width=None,
|
|
3304
|
+
svg_default=None, path_string_config=None, use_svg_cache=True, trace=None, ops=None,
|
|
3305
|
+
remove_background=False, resize=None, **kwargs):
|
|
3306
|
+
geom = {"should_center": bool(should_center), "height": None if height is None else _num(height),
|
|
3307
|
+
"width": None if width is None else _num(width)}
|
|
3308
|
+
extra = ()
|
|
3309
|
+
image = _own_attr(file_name, "_cir_image_src") or file_name
|
|
3310
|
+
if trace is not None or (isinstance(image, str) and image.lower().endswith(_IMAGE_EXTS)):
|
|
3311
|
+
# A picture traced to outlines in the preview (2Vec), after any image ops.
|
|
3312
|
+
ref = _preview_object(image)
|
|
3313
|
+
if ref is not None:
|
|
3314
|
+
self._source = ref
|
|
3315
|
+
extra = ({"$h": ref._handle()},)
|
|
3316
|
+
image = None
|
|
3317
|
+
geom.update(image_src=image, ops=_image_op_chain(ops, remove_background, resize),
|
|
3318
|
+
trace=_trace_params(trace))
|
|
3319
|
+
style = {k: v for k, v in dict(color=color, opacity=opacity, fill_color=fill_color,
|
|
3320
|
+
fill_opacity=fill_opacity, stroke_color=stroke_color,
|
|
3321
|
+
stroke_opacity=stroke_opacity, stroke_width=stroke_width).items()
|
|
3322
|
+
if v is not None}
|
|
3323
|
+
self._make("svg_mobject", geom, dict(kwargs, **style), {}, n=None, extra=extra)
|
|
3324
|
+
return
|
|
3325
|
+
ref = _preview_object(file_name)
|
|
3326
|
+
traced = _own_attr(file_name, "_cir_svg") # an image's .trace() (pyctures_images.Vector)
|
|
3327
|
+
text = file_name if isinstance(file_name, str) else _own_attr(file_name, "svg")
|
|
3328
|
+
if isinstance(traced, str):
|
|
3329
|
+
geom["svg"] = traced
|
|
3330
|
+
elif ref is not None:
|
|
3331
|
+
# An SVG worked out in the preview (an image's .trace()): sent as the
|
|
3332
|
+
# call's 4th argument, which the page resolves to the object.
|
|
3333
|
+
self._source = ref
|
|
3334
|
+
extra = ({"$h": ref._handle()},)
|
|
3335
|
+
elif isinstance(text, str) and text.lstrip().startswith("<"):
|
|
3336
|
+
geom["svg"] = text # SVG markup itself
|
|
3337
|
+
else:
|
|
3338
|
+
try:
|
|
3339
|
+
with open(str(file_name), encoding="utf-8") as fh:
|
|
3340
|
+
geom["svg"] = fh.read()
|
|
3341
|
+
except (OSError, TypeError):
|
|
3342
|
+
geom["file_name"] = str(file_name)
|
|
3343
|
+
style = {k: v for k, v in dict(color=color, opacity=opacity, fill_color=fill_color,
|
|
3344
|
+
fill_opacity=fill_opacity, stroke_color=stroke_color,
|
|
3345
|
+
stroke_opacity=stroke_opacity, stroke_width=stroke_width).items()
|
|
3346
|
+
if v is not None}
|
|
3347
|
+
self._make("svg_mobject", geom, dict(kwargs, **style), {}, n=None, extra=extra)
|
|
3348
|
+
|
|
3349
|
+
|
|
3350
|
+
def _png_data_uri(pixels):
|
|
3351
|
+
"""An image array (rows of [r, g, b(, a)] 0-255, or floats 0-1) as a PNG data URI."""
|
|
3352
|
+
import base64, struct, zlib
|
|
3353
|
+
rows = [list(_unarray(r)) for r in _unarray(pixels)]
|
|
3354
|
+
h, w = len(rows), len(rows[0]) if rows else 0
|
|
3355
|
+
|
|
3356
|
+
def px(v):
|
|
3357
|
+
v = list(_unarray(v)) if hasattr(v, "__len__") or hasattr(v, "tolist") else [v, v, v]
|
|
3358
|
+
scale = 255.0 if all(isinstance(x, float) and x <= 1.0 for x in v) else 1.0
|
|
3359
|
+
v = [max(0, min(255, int(round(float(x) * scale)))) for x in v]
|
|
3360
|
+
return (v + [255])[:4] if len(v) >= 3 else [v[0], v[0], v[0], 255]
|
|
3361
|
+
|
|
3362
|
+
raw = b"".join(b"\x00" + bytes(c for p in r for c in px(p)) for r in rows)
|
|
3363
|
+
|
|
3364
|
+
def chunk(tag, data):
|
|
3365
|
+
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
|
3366
|
+
png = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0))
|
|
3367
|
+
+ chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
|
|
3368
|
+
return "data:image/png;base64," + base64.b64encode(png).decode("ascii")
|
|
3369
|
+
|
|
3370
|
+
|
|
3371
|
+
def _own_attr(obj, name):
|
|
3372
|
+
"""obj.name only if obj really has it: a JsRef answers every name with a lazy
|
|
3373
|
+
member (and records a command), so look without calling __getattr__."""
|
|
3374
|
+
found = inspect.getattr_static(obj, name, None)
|
|
3375
|
+
if found is None:
|
|
3376
|
+
return None
|
|
3377
|
+
return getattr(obj, name)
|
|
3378
|
+
|
|
3379
|
+
|
|
3380
|
+
def _preview_object(v):
|
|
3381
|
+
"""A JavaScript object in the browser page (a loaded or processed image, a canvas),
|
|
3382
|
+
or a wrapper holding one: its JsRef, else None."""
|
|
3383
|
+
if isinstance(v, _rt.JsRef):
|
|
3384
|
+
return v
|
|
3385
|
+
for name in ("_jsref", "ref", "handle"):
|
|
3386
|
+
x = _own_attr(v, name)
|
|
3387
|
+
if isinstance(x, _rt.JsRef):
|
|
3388
|
+
return x
|
|
3389
|
+
return None
|
|
3390
|
+
|
|
3391
|
+
|
|
3392
|
+
_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".avif")
|
|
3393
|
+
|
|
3394
|
+
|
|
3395
|
+
def _image_op_chain(ops=None, remove_background=False, resize=None):
|
|
3396
|
+
"""Image ops run in the browser before the picture is used, in order:
|
|
3397
|
+
remove_background, then ops=[("name", {params}) …], then resize. The engine
|
|
3398
|
+
holds the commands after it until they are done, as it does for text."""
|
|
3399
|
+
chain = []
|
|
3400
|
+
if remove_background:
|
|
3401
|
+
chain.append(["remove_background", _j(remove_background) if isinstance(remove_background, dict) else {}])
|
|
3402
|
+
for op in ops or ():
|
|
3403
|
+
if isinstance(op, str):
|
|
3404
|
+
chain.append([op, {}])
|
|
3405
|
+
else:
|
|
3406
|
+
op = list(op)
|
|
3407
|
+
chain.append([str(op[0]), _j(op[1] if len(op) > 1 and op[1] else {})])
|
|
3408
|
+
if resize is not None:
|
|
3409
|
+
chain.append(["resize", _j(resize) if isinstance(resize, dict) else {"width": _num(resize)}])
|
|
3410
|
+
return chain
|
|
3411
|
+
|
|
3412
|
+
|
|
3413
|
+
def _trace_params(trace):
|
|
3414
|
+
return _j(trace) if isinstance(trace, dict) else {}
|
|
3415
|
+
|
|
3416
|
+
|
|
3417
|
+
class ImageMobject(Mobject):
|
|
3418
|
+
"""A picture: a path beside the .py, a web address, a data URI, an array, or
|
|
3419
|
+
an image already in the browser page (load_image(...), and what image ops return).
|
|
3420
|
+
Optional rounded corners, a border, a shadow and a crop. Image ops run in the
|
|
3421
|
+
browser, no await needed: ops=[("remove_background",), ("resize", {"width": 800})],
|
|
3422
|
+
or remove_background=True, resize=800; trace=True (or {...}) makes it a vector
|
|
3423
|
+
outline instead."""
|
|
3424
|
+
|
|
3425
|
+
def __init__(self, filename_or_array, scale_to_resolution=1080, invert=False, image_mode="RGBA",
|
|
3426
|
+
corner_radius=0, border_color=None, border_width=0, shadow=None, crop=None, ops=None,
|
|
3427
|
+
remove_background=False, resize=None, trace=None, **kwargs):
|
|
3428
|
+
# A Picture (pyctures_images) names its source: a relative path, or a blob:
|
|
3429
|
+
# URL for an image op's result.
|
|
3430
|
+
src = _own_attr(filename_or_array, "_cir_image_src") or filename_or_array
|
|
3431
|
+
ref = _preview_object(src)
|
|
3432
|
+
extra = ()
|
|
3433
|
+
if ref is not None:
|
|
3434
|
+
# The element itself travels as the call's 4th argument (the page
|
|
3435
|
+
# resolves a top-level {"$h": n} to the object).
|
|
3436
|
+
self._source = ref # keeps the browser object alive
|
|
3437
|
+
src = None
|
|
3438
|
+
extra = ({"$h": ref._handle()},)
|
|
3439
|
+
elif not isinstance(src, str):
|
|
3440
|
+
src = _png_data_uri(src)
|
|
3441
|
+
# A relative path is sent as written: the app reads it from the file's folder.
|
|
3442
|
+
self.src = src
|
|
3443
|
+
chain = _image_op_chain(ops, remove_background, resize)
|
|
3444
|
+
if trace:
|
|
3445
|
+
# Traced to vector outlines (2Vec): an SVG object made from the picture.
|
|
3446
|
+
self._make("svg_mobject", {"image_src": src, "ops": chain, "trace": _trace_params(trace),
|
|
3447
|
+
"should_center": True, "height": None, "width": None},
|
|
3448
|
+
kwargs, {}, n=None, extra=extra)
|
|
3449
|
+
return
|
|
3450
|
+
props = {"src": src, "scale_to_resolution": int(scale_to_resolution), "invert": bool(invert)}
|
|
3451
|
+
if chain:
|
|
3452
|
+
props["ops"] = chain
|
|
3453
|
+
self._make("image_mobject", props, kwargs, {}, n=0, extra=extra)
|
|
3454
|
+
if crop is not None:
|
|
3455
|
+
self.crop(*crop)
|
|
3456
|
+
if corner_radius:
|
|
3457
|
+
self.set_corner_radius(corner_radius)
|
|
3458
|
+
if border_color is not None or border_width:
|
|
3459
|
+
self.set_border(border_color or WHITE, border_width or DEFAULT_STROKE_WIDTH)
|
|
3460
|
+
if shadow:
|
|
3461
|
+
self.set_shadow(**(shadow if isinstance(shadow, dict) else {}))
|
|
3462
|
+
|
|
3463
|
+
def set_corner_radius(self, radius):
|
|
3464
|
+
return self._op("set_corner_radius", {"radius": _num(radius)})
|
|
3465
|
+
|
|
3466
|
+
def set_border(self, color=WHITE, width=DEFAULT_STROKE_WIDTH):
|
|
3467
|
+
return self._op("set_border", {"color": _color(color), "width": _num(width)})
|
|
3468
|
+
|
|
3469
|
+
def crop(self, x0=0.0, y0=0.0, x1=1.0, y1=1.0):
|
|
3470
|
+
"""Keep the box (x0, y0)–(x1, y1), as fractions of the picture, origin bottom-left."""
|
|
3471
|
+
return self._op("crop", {"box": [_num(x0), _num(y0), _num(x1), _num(y1)]})
|
|
3472
|
+
|
|
3473
|
+
|
|
3474
|
+
# ---------------------------------------------------------------- more animations
|
|
3475
|
+
|
|
3476
|
+
class ApplyMethod(Animation):
|
|
3477
|
+
"""ApplyMethod(m.shift, UP): the method's change, played."""
|
|
3478
|
+
kind = "animate"
|
|
3479
|
+
|
|
3480
|
+
def __init__(self, method, *args, **kwargs):
|
|
3481
|
+
timing = {k: kwargs.pop(k) for k in ("run_time", "rate_func", "lag_ratio") if k in kwargs}
|
|
3482
|
+
mob = method.__self__
|
|
3483
|
+
ops = []
|
|
3484
|
+
st = _st()
|
|
3485
|
+
prev = st.capture
|
|
3486
|
+
st.capture = (mob._t(), ops)
|
|
3487
|
+
try:
|
|
3488
|
+
method(*args, **kwargs)
|
|
3489
|
+
finally:
|
|
3490
|
+
st.capture = prev
|
|
3491
|
+
self._ops = ops
|
|
3492
|
+
Animation.__init__(self, mob, **timing)
|
|
3493
|
+
|
|
3494
|
+
def _make_args(self, **kwargs):
|
|
3495
|
+
return {"ops": self._ops}
|
|
3496
|
+
|
|
3497
|
+
|
|
3498
|
+
class ApplyFunction(Animation):
|
|
3499
|
+
"""ApplyFunction(f, m): f's change to m, played."""
|
|
3500
|
+
kind = "animate"
|
|
3501
|
+
|
|
3502
|
+
def __init__(self, function, mobject, **kwargs):
|
|
3503
|
+
ops = []
|
|
3504
|
+
st = _st()
|
|
3505
|
+
prev = st.capture
|
|
3506
|
+
st.capture = (mobject._t(), ops)
|
|
3507
|
+
try:
|
|
3508
|
+
result = function(mobject)
|
|
3509
|
+
finally:
|
|
3510
|
+
st.capture = prev
|
|
3511
|
+
if isinstance(result, Mobject) and result is not mobject:
|
|
3512
|
+
ops.append(["become", {"mobject": result._embed()}])
|
|
3513
|
+
self._ops = ops
|
|
3514
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
3515
|
+
|
|
3516
|
+
def _make_args(self, **kwargs):
|
|
3517
|
+
return {"ops": self._ops}
|
|
3518
|
+
|
|
3519
|
+
|
|
3520
|
+
def _trace_formula(fn, args, what):
|
|
3521
|
+
tr, result, err = _run_trace(fn, args)
|
|
3522
|
+
if err is not None or result is None:
|
|
3523
|
+
raise TypeError("%s needs a function that can be worked out with + - * / and np.sin, np.cos … "
|
|
3524
|
+
"(not math.* or an if on the point)%s" % (what, ": %s" % err if err else ""))
|
|
3525
|
+
return _tree(result)
|
|
3526
|
+
|
|
3527
|
+
|
|
3528
|
+
class ApplyPointwiseFunction(Animation):
|
|
3529
|
+
kind = "apply_pointwise_function"
|
|
3530
|
+
|
|
3531
|
+
def __init__(self, function, mobject, run_time=3, **kwargs):
|
|
3532
|
+
self._formula = _trace_formula(function, (_var("p", "v"),), "ApplyPointwiseFunction")
|
|
3533
|
+
Animation.__init__(self, mobject, run_time=run_time, **kwargs)
|
|
3534
|
+
|
|
3535
|
+
def _make_args(self, **kwargs):
|
|
3536
|
+
return {"function": self._formula}
|
|
3537
|
+
|
|
3538
|
+
|
|
3539
|
+
class Homotopy(Animation):
|
|
3540
|
+
kind = "homotopy"
|
|
3541
|
+
|
|
3542
|
+
def __init__(self, homotopy, mobject, run_time=3, apply_function_kwargs=None, **kwargs):
|
|
3543
|
+
self._formula = _trace_formula(homotopy, (_var("x"), _var("y"), _var("z"), _var("t")), "Homotopy")
|
|
3544
|
+
Animation.__init__(self, mobject, run_time=run_time, **kwargs)
|
|
3545
|
+
|
|
3546
|
+
def _make_args(self, **kwargs):
|
|
3547
|
+
return {"homotopy": self._formula}
|
|
3548
|
+
|
|
3549
|
+
|
|
3550
|
+
class ShowPassingFlash(_Remover):
|
|
3551
|
+
kind = "show_passing_flash"
|
|
3552
|
+
|
|
3553
|
+
def _make_args(self, time_width=0.1, **kwargs):
|
|
3554
|
+
return {"time_width": _num(time_width)}
|
|
3555
|
+
|
|
3556
|
+
|
|
3557
|
+
class ShowIncreasingSubsets(Animation):
|
|
3558
|
+
kind = "show_increasing_subsets"
|
|
3559
|
+
|
|
3560
|
+
def _make_args(self, suspend_mobject_updating=False, int_func=None, reverse_rate_function=False, **kwargs):
|
|
3561
|
+
return {"int_func": "ceil" if getattr(int_func, "__name__", "") == "ceil" else "floor"}
|
|
3562
|
+
|
|
3563
|
+
|
|
3564
|
+
class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
|
3565
|
+
kind = "show_increasing_subsets"
|
|
3566
|
+
|
|
3567
|
+
|
|
3568
|
+
class AddTextLetterByLetter(Animation):
|
|
3569
|
+
kind = "add_text_letter_by_letter"
|
|
3570
|
+
|
|
3571
|
+
def __init__(self, text, suspend_mobject_updating=False, int_func=None, rate_func=linear, time_per_char=0.1,
|
|
3572
|
+
run_time=None, reverse_rate_function=False, introducer=True, **kwargs):
|
|
3573
|
+
self._tpc = time_per_char
|
|
3574
|
+
if run_time is None:
|
|
3575
|
+
n = text._count()
|
|
3576
|
+
run_time = max(1 / 60, n * time_per_char) if n is not None else None
|
|
3577
|
+
Animation.__init__(self, text, run_time=run_time, rate_func=rate_func, **kwargs)
|
|
3578
|
+
|
|
3579
|
+
def _make_args(self, **kwargs):
|
|
3580
|
+
return {"time_per_char": _num(self._tpc)}
|
|
3581
|
+
|
|
3582
|
+
|
|
3583
|
+
class RemoveTextLetterByLetter(AddTextLetterByLetter):
|
|
3584
|
+
kind = "remove_text_letter_by_letter"
|
|
3585
|
+
|
|
3586
|
+
def _removed(self):
|
|
3587
|
+
return [self.mobject]
|
|
3588
|
+
|
|
3589
|
+
|
|
3590
|
+
class CyclicReplace(Animation):
|
|
3591
|
+
kind = "cyclic_replace"
|
|
3592
|
+
|
|
3593
|
+
def __init__(self, *mobjects, path_arc=90 * DEGREES, **kwargs):
|
|
3594
|
+
self._mobs = _flat(mobjects)
|
|
3595
|
+
self._arc = path_arc
|
|
3596
|
+
Animation.__init__(self, None, **kwargs)
|
|
3597
|
+
|
|
3598
|
+
def _make_args(self, **kwargs):
|
|
3599
|
+
return {"mobjects": [m._embed() for m in self._mobs], "path_arc": _num(self._arc)}
|
|
3600
|
+
|
|
3601
|
+
def _added(self):
|
|
3602
|
+
return list(self._mobs)
|
|
3603
|
+
|
|
3604
|
+
|
|
3605
|
+
class Swap(CyclicReplace):
|
|
3606
|
+
kind = "swap"
|
|
3607
|
+
|
|
3608
|
+
|
|
3609
|
+
class FadeTransform(ReplacementTransform):
|
|
3610
|
+
kind = "fade_transform"
|
|
3611
|
+
|
|
3612
|
+
def __init__(self, mobject, target_mobject, stretch=True, dim_to_match=1, **kwargs):
|
|
3613
|
+
self._fade = {"stretch": bool(stretch), "dim_to_match": int(dim_to_match)}
|
|
3614
|
+
ReplacementTransform.__init__(self, mobject, target_mobject, **kwargs)
|
|
3615
|
+
|
|
3616
|
+
def _make_args(self, path_arc=0, **kwargs):
|
|
3617
|
+
return dict({"target_mobject": self.target_mobject._embed()}, **self._fade)
|
|
3618
|
+
|
|
3619
|
+
|
|
3620
|
+
class FadeTransformPieces(FadeTransform):
|
|
3621
|
+
kind = "fade_transform_pieces"
|
|
3622
|
+
|
|
3623
|
+
|
|
3624
|
+
class ChangeDecimalToValue(Animation):
|
|
3625
|
+
kind = "change_decimal_to_value"
|
|
3626
|
+
|
|
3627
|
+
def __init__(self, decimal_mob, target_number, **kwargs):
|
|
3628
|
+
self._target = target_number
|
|
3629
|
+
if _real_number(target_number):
|
|
3630
|
+
decimal_mob.number = target_number
|
|
3631
|
+
Animation.__init__(self, decimal_mob, **kwargs)
|
|
3632
|
+
|
|
3633
|
+
def _make_args(self, **kwargs):
|
|
3634
|
+
return {"target_number": _num(self._target)}
|
|
3635
|
+
|
|
3636
|
+
|
|
3637
|
+
class ChangingDecimal(Animation):
|
|
3638
|
+
kind = "changing_decimal"
|
|
3639
|
+
|
|
3640
|
+
def __init__(self, decimal_mob, number_update_func, suspend_mobject_updating=False, **kwargs):
|
|
3641
|
+
self._formula = _trace_formula(number_update_func, (_var("alpha"),), "ChangingDecimal")
|
|
3642
|
+
Animation.__init__(self, decimal_mob, **kwargs)
|
|
3643
|
+
|
|
3644
|
+
def _make_args(self, **kwargs):
|
|
3645
|
+
return {"function": self._formula}
|
|
3646
|
+
|
|
3647
|
+
|
|
3648
|
+
class ChangeSpeed(Animation):
|
|
3649
|
+
kind = "change_speed"
|
|
3650
|
+
|
|
3651
|
+
def __init__(self, anim, speedinfo, rate_func=None, affects_speed_updaters=True, **kwargs):
|
|
3652
|
+
self._inner = _as_anim(anim, "ChangeSpeed()")
|
|
3653
|
+
self._speed = {repr(float(k)): _num(v) for k, v in speedinfo.items()}
|
|
3654
|
+
Animation.__init__(self, None, rate_func=rate_func, **kwargs)
|
|
3655
|
+
|
|
3656
|
+
def _make_args(self, **kwargs):
|
|
3657
|
+
return {"anim": self._inner._anim(), "speedinfo": self._speed}
|
|
3658
|
+
|
|
3659
|
+
def _added(self):
|
|
3660
|
+
return self._inner._added()
|
|
3661
|
+
|
|
3662
|
+
def _removed(self):
|
|
3663
|
+
return self._inner._removed()
|
|
3664
|
+
|
|
3665
|
+
|
|
3666
|
+
class Broadcast(Animation):
|
|
3667
|
+
kind = "broadcast"
|
|
3668
|
+
|
|
3669
|
+
def __init__(self, mobject, focal_point=ORIGIN, n_mobs=5, initial_opacity=1, final_opacity=0,
|
|
3670
|
+
initial_width=0.0, remover=True, lag_ratio=0.2, run_time=3, **kwargs):
|
|
3671
|
+
self._b = {"focal_point": _P(focal_point), "n_mobs": int(n_mobs), "initial_opacity": _num(initial_opacity),
|
|
3672
|
+
"final_opacity": _num(final_opacity), "initial_width": _num(initial_width)}
|
|
3673
|
+
Animation.__init__(self, mobject, lag_ratio=lag_ratio, run_time=run_time, **kwargs)
|
|
3674
|
+
|
|
3675
|
+
def _make_args(self, **kwargs):
|
|
3676
|
+
return self._b
|
|
3677
|
+
|
|
3678
|
+
def _added(self):
|
|
3679
|
+
return []
|
|
3680
|
+
|
|
3681
|
+
|
|
3682
|
+
class ApplyWave(Animation):
|
|
3683
|
+
kind = "apply_wave"
|
|
3684
|
+
|
|
3685
|
+
def __init__(self, mobject, direction=UP, amplitude=0.2, wave_func=smooth, time_width=1, ripples=1, run_time=2,
|
|
3686
|
+
**kwargs):
|
|
3687
|
+
self._w = {"direction": _P(direction), "amplitude": _num(amplitude), "wave_func": _rate(wave_func),
|
|
3688
|
+
"time_width": _num(time_width), "ripples": int(ripples)}
|
|
3689
|
+
Animation.__init__(self, mobject, run_time=run_time, **kwargs)
|
|
3690
|
+
|
|
3691
|
+
def _make_args(self, **kwargs):
|
|
3692
|
+
return self._w
|
|
3693
|
+
|
|
3694
|
+
|
|
3695
|
+
class Blink(Animation):
|
|
3696
|
+
kind = "blink"
|
|
3697
|
+
|
|
3698
|
+
def __init__(self, mobject, time_on=0.5, time_off=0.5, blinks=1, hide_at_end=False, **kwargs):
|
|
3699
|
+
self._bl = {"time_on": _num(time_on), "time_off": _num(time_off), "blinks": int(blinks),
|
|
3700
|
+
"hide_at_end": bool(hide_at_end)}
|
|
3701
|
+
self._hide = hide_at_end
|
|
3702
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
3703
|
+
|
|
3704
|
+
def _make_args(self, **kwargs):
|
|
3705
|
+
return self._bl
|
|
3706
|
+
|
|
3707
|
+
|
|
3708
|
+
|
|
3709
|
+
class FocusOn(Animation):
|
|
3710
|
+
kind = "focus_on"
|
|
3711
|
+
|
|
3712
|
+
def __init__(self, focus_point, opacity=0.2, color=GREY, run_time=2, **kwargs):
|
|
3713
|
+
self._f = {"focus_point": _MP(focus_point), "opacity": _num(opacity), "color": _color(color)}
|
|
3714
|
+
Animation.__init__(self, None, run_time=run_time, **kwargs)
|
|
3715
|
+
|
|
3716
|
+
def _make_args(self, **kwargs):
|
|
3717
|
+
return self._f
|
|
3718
|
+
|
|
3719
|
+
|
|
3720
|
+
class ShrinkToCenter(Animation):
|
|
3721
|
+
kind = "shrink_to_center"
|
|
3722
|
+
|
|
3723
|
+
|
|
3724
|
+
class ApplyMatrix(ApplyPointwiseFunction):
|
|
3725
|
+
"""ApplyMatrix([[1, 1], [0, 1]], m): a linear map about about_point."""
|
|
3726
|
+
|
|
3727
|
+
def __init__(self, matrix, mobject, about_point=ORIGIN, **kwargs):
|
|
3728
|
+
rows = [list(_unarray(r)) for r in _unarray(matrix)]
|
|
3729
|
+
n = len(rows)
|
|
3730
|
+
full = [[float(rows[i][j]) if i < n and j < len(rows[i]) else float(i == j) for j in range(3)]
|
|
3731
|
+
for i in range(3)]
|
|
3732
|
+
about = Vec.of(about_point)
|
|
3733
|
+
|
|
3734
|
+
def term(c, v):
|
|
3735
|
+
return v if c == 1 else -v if c == -1 else c * v
|
|
3736
|
+
|
|
3737
|
+
def apply(p):
|
|
3738
|
+
q = [p[k] - about[k] if about[k] else p[k] for k in range(3)]
|
|
3739
|
+
out = []
|
|
3740
|
+
for i in range(3):
|
|
3741
|
+
terms = [term(full[i][j], q[j]) for j in range(3) if full[i][j]]
|
|
3742
|
+
v = terms[0] if terms else 0.0
|
|
3743
|
+
for t in terms[1:]:
|
|
3744
|
+
v = v + t
|
|
3745
|
+
out.append(v + about[i] if about[i] else v)
|
|
3746
|
+
return out
|
|
3747
|
+
ApplyPointwiseFunction.__init__(self, apply, mobject, **kwargs)
|
|
3748
|
+
|
|
3749
|
+
|
|
3750
|
+
class ScaleInPlace(Animation):
|
|
3751
|
+
"""ScaleInPlace(m, 2): .animate.scale(2) by another name."""
|
|
3752
|
+
kind = "animate"
|
|
3753
|
+
|
|
3754
|
+
def __init__(self, mobject, scale_factor, **kwargs):
|
|
3755
|
+
self._sf = scale_factor
|
|
3756
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
3757
|
+
|
|
3758
|
+
def _make_args(self, **kwargs):
|
|
3759
|
+
return {"ops": [["scale", {"scale_factor": _num(self._sf), "about_point": None, "about_edge": None}]]}
|
|
3760
|
+
|
|
3761
|
+
|
|
3762
|
+
# ---------------------------------------------------------------- product explainers
|
|
3763
|
+
#
|
|
3764
|
+
# Pymations extras (not in Manim), built from the shapes above: cards, callouts,
|
|
3765
|
+
# lower thirds, badges, end cards, highlights and a spotlight.
|
|
3766
|
+
|
|
3767
|
+
class Card(RoundedRectangle):
|
|
3768
|
+
"""A filled panel with rounded corners and a soft shadow."""
|
|
3769
|
+
|
|
3770
|
+
def __init__(self, width=4.0, height=2.5, fill=GREY_E, radius=0.25, shadow=True, stroke_color=None,
|
|
3771
|
+
stroke_width=0, **kwargs):
|
|
3772
|
+
RoundedRectangle.__init__(self, corner_radius=radius, width=width, height=height, color=fill,
|
|
3773
|
+
fill_color=fill, fill_opacity=kwargs.pop("fill_opacity", 1),
|
|
3774
|
+
stroke_color=stroke_color or fill, stroke_width=stroke_width, **kwargs)
|
|
3775
|
+
if shadow:
|
|
3776
|
+
self.set_shadow(**(shadow if isinstance(shadow, dict) else {}))
|
|
3777
|
+
|
|
3778
|
+
|
|
3779
|
+
class Callout(VGroup):
|
|
3780
|
+
"""A label in a box with an arrow to a spot: Callout(button, "Tap here", side=UR)."""
|
|
3781
|
+
|
|
3782
|
+
def __init__(self, target, text, side=UR, distance=1.6, color=YELLOW, text_color=WHITE, font_size=28,
|
|
3783
|
+
box_color=BLACK, box_opacity=0.8, **text_kwargs):
|
|
3784
|
+
spot = target.get_center() if isinstance(target, Mobject) else (
|
|
3785
|
+
target if isinstance(target, Expr) else Vec.of(target))
|
|
3786
|
+
d = Vec.of(side)
|
|
3787
|
+
norm = abs(d) or 1.0
|
|
3788
|
+
self.label = Text(text, font_size=font_size, color=text_color, **text_kwargs)
|
|
3789
|
+
self.label.move_to(spot + d * (distance / norm))
|
|
3790
|
+
self.box = SurroundingRectangle(self.label, color=color, buff=0.15, corner_radius=0.1,
|
|
3791
|
+
fill_color=box_color, fill_opacity=box_opacity)
|
|
3792
|
+
self.arrow = Arrow(self.box, target, buff=0.1, color=color, stroke_width=4)
|
|
3793
|
+
VGroup.__init__(self, self.box, self.label, self.arrow)
|
|
3794
|
+
|
|
3795
|
+
|
|
3796
|
+
class LowerThird(VGroup):
|
|
3797
|
+
"""A name strip for the bottom left: LowerThird("Ada Lovelace", "Founder")."""
|
|
3798
|
+
|
|
3799
|
+
def __init__(self, title, subtitle=None, color=BLUE, text_color=WHITE, background=BLACK,
|
|
3800
|
+
background_opacity=0.65, font="", corner=DL, buff=0.6):
|
|
3801
|
+
self.title = Text(title, font_size=36, weight=BOLD, color=text_color, font=font)
|
|
3802
|
+
lines = [self.title]
|
|
3803
|
+
if subtitle:
|
|
3804
|
+
self.subtitle = Text(subtitle, font_size=24, color=GREY_A, font=font)
|
|
3805
|
+
lines.append(self.subtitle)
|
|
3806
|
+
texts = VGroup(*lines).arrange(DOWN, aligned_edge=LEFT, buff=0.12)
|
|
3807
|
+
self.bar = Rectangle(width=0.08, height=1, color=color, fill_opacity=1, stroke_width=0)
|
|
3808
|
+
self.bar.stretch_to_fit_height(texts.height).next_to(texts, LEFT, buff=0.2)
|
|
3809
|
+
inner = VGroup(self.bar, texts)
|
|
3810
|
+
self.box = SurroundingRectangle(inner, buff=0.25, corner_radius=0.1, color=background,
|
|
3811
|
+
fill_color=background, fill_opacity=background_opacity, stroke_width=0)
|
|
3812
|
+
VGroup.__init__(self, self.box, inner)
|
|
3813
|
+
self.to_corner(corner, buff=buff)
|
|
3814
|
+
|
|
3815
|
+
|
|
3816
|
+
class Badge(VGroup):
|
|
3817
|
+
"""A pill-shaped label: Badge("NEW")."""
|
|
3818
|
+
|
|
3819
|
+
def __init__(self, text, color=BLUE, text_color=WHITE, font_size=24, **text_kwargs):
|
|
3820
|
+
self.label = Text(text, font_size=font_size, color=text_color, weight=BOLD, **text_kwargs)
|
|
3821
|
+
self.pill = SurroundingRectangle(self.label, buff=0.12, corner_radius=self.label.height / 2 + 0.12,
|
|
3822
|
+
color=color, fill_color=color, fill_opacity=1, stroke_width=0)
|
|
3823
|
+
VGroup.__init__(self, self.pill, self.label)
|
|
3824
|
+
|
|
3825
|
+
|
|
3826
|
+
class EndCard(VGroup):
|
|
3827
|
+
"""The closing card: a logo, a title and a web address, centred."""
|
|
3828
|
+
|
|
3829
|
+
def __init__(self, logo=None, title="", url=None, subtitle=None, font="", logo_height=1.6):
|
|
3830
|
+
parts = []
|
|
3831
|
+
if logo is not None:
|
|
3832
|
+
self.logo = logo if isinstance(logo, Mobject) else ImageMobject(logo)
|
|
3833
|
+
self.logo.scale_to_fit_height(logo_height)
|
|
3834
|
+
parts.append(self.logo)
|
|
3835
|
+
if title:
|
|
3836
|
+
parts.append(Text(title, font_size=56, weight=BOLD, font=font))
|
|
3837
|
+
if subtitle:
|
|
3838
|
+
parts.append(Text(subtitle, font_size=32, color=GREY_A, font=font))
|
|
3839
|
+
if url:
|
|
3840
|
+
parts.append(Text(url, font_size=30, color=GREY_B, font=font))
|
|
3841
|
+
VGroup.__init__(self, *parts)
|
|
3842
|
+
self.arrange(DOWN, buff=0.4).center()
|
|
3843
|
+
|
|
3844
|
+
|
|
3845
|
+
class Highlight(SurroundingRectangle):
|
|
3846
|
+
"""A box, or a soft glow (style="glow"), around something to look at."""
|
|
3847
|
+
|
|
3848
|
+
def __init__(self, mobject, color=YELLOW, style="box", buff=0.12, corner_radius=0.12, stroke_width=6,
|
|
3849
|
+
**kwargs):
|
|
3850
|
+
SurroundingRectangle.__init__(self, mobject, color=color, buff=buff, corner_radius=corner_radius,
|
|
3851
|
+
stroke_width=stroke_width, **kwargs)
|
|
3852
|
+
if style == "glow":
|
|
3853
|
+
self.set_stroke(opacity=0.8)
|
|
3854
|
+
self.set_shadow(blur=0.3, offset=(0, 0), color=color, opacity=0.9)
|
|
3855
|
+
|
|
3856
|
+
|
|
3857
|
+
class Spotlight(VMobject):
|
|
3858
|
+
"""Dims everything except one object: a dark sheet with a hole around it."""
|
|
3859
|
+
|
|
3860
|
+
def __init__(self, mobject, opacity=0.75, buff=0.15, color=BLACK, **kwargs):
|
|
3861
|
+
w, h = _frame_size()
|
|
3862
|
+
W, H = 1.5 * w, 1.5 * h
|
|
3863
|
+
outer = [[-W, -H, 0], [W, -H, 0], [W, H, 0], [-W, H, 0]]
|
|
3864
|
+
b = float(buff)
|
|
3865
|
+
hole = [mobject.get_corner(UL) + Vec(-b, b, 0), mobject.get_corner(UR) + Vec(b, b, 0),
|
|
3866
|
+
mobject.get_corner(DR) + Vec(b, -b, 0), mobject.get_corner(DL) + Vec(-b, -b, 0)]
|
|
3867
|
+
self._make("vmobject", {"subpaths": [[_P(p) for p in outer], [_P(p) for p in hole]], "closed": True,
|
|
3868
|
+
"mode": "corners"},
|
|
3869
|
+
dict(kwargs, color=color, fill_color=color, fill_opacity=opacity, stroke_width=0), n=0)
|
|
3870
|
+
|
|
3871
|
+
|
|
3872
|
+
# ---------------------------------------------------------------- sound
|
|
3873
|
+
|
|
3874
|
+
def _volume(gain):
|
|
3875
|
+
return 1.0 if gain is None else 10 ** (float(gain) / 20.0)
|
|
3876
|
+
|
|
3877
|
+
|
|
3878
|
+
def _sound(src, kind="sound", time_offset=0, volume=1.0, fade_in=0, fade_out=0, loop=False):
|
|
3879
|
+
_call("sound", {"src": str(src), "kind": kind, "time_offset": _num(time_offset), "volume": _num(volume),
|
|
3880
|
+
"fade_in": _num(fade_in), "fade_out": _num(fade_out), "loop": bool(loop)})
|
|
3881
|
+
|
|
3882
|
+
|
|
3883
|
+
def add_sound(sound_file, time_offset=0, gain=None, **kwargs):
|
|
3884
|
+
"""Play a sound from this point of the timeline (gain in dB, as in Manim)."""
|
|
3885
|
+
_sound(sound_file, "sound", time_offset, _volume(gain))
|
|
3886
|
+
|
|
3887
|
+
|
|
3888
|
+
def music(sound_file, volume=0.3, fade_in=0, fade_out=1, loop=True):
|
|
3889
|
+
"""A music bed under the whole timeline from here, quiet and looping."""
|
|
3890
|
+
_sound(sound_file, "music", 0, volume, fade_in, fade_out, loop)
|
|
3891
|
+
|
|
3892
|
+
|
|
3893
|
+
def wait_for_sound(sound=None, extra=0):
|
|
3894
|
+
"""Wait until a sound (or the last one started) has finished, plus extra seconds."""
|
|
3895
|
+
_call("wait_sound", {"src": None if sound is None else str(sound), "extra": _num(extra)})
|
|
3896
|
+
|
|
3897
|
+
|
|
3898
|
+
def _end_section_sound():
|
|
3899
|
+
st = _st()
|
|
3900
|
+
if st.section_sound is not None:
|
|
3901
|
+
src, st.section_sound = st.section_sound, None
|
|
3902
|
+
_call("wait_sound", {"src": src, "extra": 0})
|
|
3903
|
+
|
|
3904
|
+
|
|
3905
|
+
def _section(name, transition=None, transition_time=None, sound=None):
|
|
3906
|
+
_end_section_sound()
|
|
3907
|
+
if transition and transition != "none":
|
|
3908
|
+
tt = transition_time if transition_time is not None else (config.transition_time or 0.5)
|
|
3909
|
+
_call("section", str(name), {"transition": transition, "transition_time": _num(tt)})
|
|
3910
|
+
else:
|
|
3911
|
+
_call("section", str(name))
|
|
3912
|
+
if sound is not None:
|
|
3913
|
+
_sound(sound)
|
|
3914
|
+
_st().section_sound = str(sound)
|
|
3915
|
+
|
|
3916
|
+
|
|
3917
|
+
# ---------------------------------------------------------------- scenes
|
|
3918
|
+
|
|
3919
|
+
class Scene:
|
|
3920
|
+
"""construct() records the scene; every Scene in the file plays in order."""
|
|
3921
|
+
|
|
3922
|
+
def __init__(self, **kwargs):
|
|
3923
|
+
self.mobjects = []
|
|
3924
|
+
self.camera = _Camera()
|
|
3925
|
+
|
|
3926
|
+
def setup(self):
|
|
3927
|
+
pass
|
|
3928
|
+
|
|
3929
|
+
def construct(self):
|
|
3930
|
+
pass
|
|
3931
|
+
|
|
3932
|
+
def tear_down(self):
|
|
3933
|
+
pass
|
|
3934
|
+
|
|
3935
|
+
def render(self, preview=False):
|
|
3936
|
+
_st().rendered.add(type(self))
|
|
3937
|
+
_run_scene_sync(self)
|
|
3938
|
+
|
|
3939
|
+
# -- the stage
|
|
3940
|
+
|
|
3941
|
+
def _keep(self, mobs, to_front=False):
|
|
3942
|
+
for m in mobs:
|
|
3943
|
+
if m in self.mobjects:
|
|
3944
|
+
if not to_front:
|
|
3945
|
+
continue
|
|
3946
|
+
self.mobjects.remove(m)
|
|
3947
|
+
self.mobjects.append(m)
|
|
3948
|
+
|
|
3949
|
+
def _drop(self, mobs):
|
|
3950
|
+
# Removing a member of a group on the stage splits that group, so its
|
|
3951
|
+
# other members stay (Manim's rule).
|
|
3952
|
+
out = []
|
|
3953
|
+
for m in self.mobjects:
|
|
3954
|
+
if m in mobs:
|
|
3955
|
+
continue
|
|
3956
|
+
fam = m.get_family()
|
|
3957
|
+
if any(x in fam for x in mobs if x is not m):
|
|
3958
|
+
out.extend(_split(m, mobs))
|
|
3959
|
+
else:
|
|
3960
|
+
out.append(m)
|
|
3961
|
+
self.mobjects[:] = out
|
|
3962
|
+
|
|
3963
|
+
def add(self, *mobjects):
|
|
3964
|
+
mobs = _flat(mobjects)
|
|
3965
|
+
self._keep(mobs, to_front=True)
|
|
3966
|
+
_call("add", [m._t() for m in mobs])
|
|
3967
|
+
return self
|
|
3968
|
+
|
|
3969
|
+
add_foreground_mobjects = add_foreground_mobject = add
|
|
3970
|
+
|
|
3971
|
+
def remove(self, *mobjects):
|
|
3972
|
+
mobs = _flat(mobjects)
|
|
3973
|
+
self._drop(mobs)
|
|
3974
|
+
_call("remove", [m._t() for m in mobs])
|
|
3975
|
+
return self
|
|
3976
|
+
|
|
3977
|
+
remove_foreground_mobjects = remove_foreground_mobject = remove
|
|
3978
|
+
|
|
3979
|
+
def clear(self):
|
|
3980
|
+
self.mobjects = []
|
|
3981
|
+
_call("clear")
|
|
3982
|
+
return self
|
|
3983
|
+
|
|
3984
|
+
def bring_to_front(self, *mobjects):
|
|
3985
|
+
for m in _flat(mobjects):
|
|
3986
|
+
m.set_z_index(max([getattr(x, "z_index", 0) or 0 for x in self.mobjects] + [0]) + 1)
|
|
3987
|
+
return self
|
|
3988
|
+
|
|
3989
|
+
def bring_to_back(self, *mobjects):
|
|
3990
|
+
for m in _flat(mobjects):
|
|
3991
|
+
m.set_z_index(min([getattr(x, "z_index", 0) or 0 for x in self.mobjects] + [0]) - 1)
|
|
3992
|
+
return self
|
|
3993
|
+
|
|
3994
|
+
# -- the timeline
|
|
3995
|
+
|
|
3996
|
+
def play(self, *animations, run_time=None, rate_func=None, lag_ratio=None, **kwargs):
|
|
3997
|
+
anims = []
|
|
3998
|
+
for a in animations:
|
|
3999
|
+
if isinstance(a, (list, tuple, types.GeneratorType)):
|
|
4000
|
+
anims.extend(_as_anim(x) for x in a)
|
|
4001
|
+
else:
|
|
4002
|
+
anims.append(_as_anim(a))
|
|
4003
|
+
if not anims:
|
|
4004
|
+
return
|
|
4005
|
+
# As in Manim, play()'s run_time/rate_func/lag_ratio are SET ON EVERY
|
|
4006
|
+
# animation in the call (overriding their own), so opts stays empty.
|
|
4007
|
+
over = _timing({}, {"run_time": run_time, "rate_func": rate_func, "lag_ratio": lag_ratio})
|
|
4008
|
+
encoded = []
|
|
4009
|
+
for a in anims:
|
|
4010
|
+
d = a._anim()
|
|
4011
|
+
d.update(over)
|
|
4012
|
+
encoded.append(d)
|
|
4013
|
+
_call("play", encoded, {})
|
|
4014
|
+
for a in anims:
|
|
4015
|
+
self._keep(a._added())
|
|
4016
|
+
for a in anims:
|
|
4017
|
+
self._drop(a._removed())
|
|
4018
|
+
|
|
4019
|
+
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None, frozen_frame=None):
|
|
4020
|
+
_call("wait", _num(duration))
|
|
4021
|
+
|
|
4022
|
+
pause = wait
|
|
4023
|
+
|
|
4024
|
+
def wait_until(self, stop_condition, max_time=60):
|
|
4025
|
+
self.wait(max_time)
|
|
4026
|
+
|
|
4027
|
+
transition = None # "crossfade" | "slide_left" | "wipe" | "none"; else config.transition
|
|
4028
|
+
transition_time = None
|
|
4029
|
+
|
|
4030
|
+
def next_section(self, name="unnamed", section_type=None, skip_animations=False, sound=None):
|
|
4031
|
+
_section(name, sound=sound)
|
|
4032
|
+
|
|
4033
|
+
def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
|
|
4034
|
+
add_sound(sound_file, time_offset, gain)
|
|
4035
|
+
|
|
4036
|
+
def music(self, sound_file, **kwargs):
|
|
4037
|
+
music(sound_file, **kwargs)
|
|
4038
|
+
|
|
4039
|
+
def wait_for_sound(self, sound=None, extra=0):
|
|
4040
|
+
wait_for_sound(sound, extra)
|
|
4041
|
+
|
|
4042
|
+
def interactive_embed(self):
|
|
4043
|
+
pass
|
|
4044
|
+
|
|
4045
|
+
embed = interactive_embed
|
|
4046
|
+
|
|
4047
|
+
|
|
4048
|
+
class MovingCameraScene(Scene):
|
|
4049
|
+
pass
|
|
4050
|
+
|
|
4051
|
+
|
|
4052
|
+
class ZoomedScene(MovingCameraScene):
|
|
4053
|
+
pass
|
|
4054
|
+
|
|
4055
|
+
|
|
4056
|
+
class ThreeDScene(Scene):
|
|
4057
|
+
def _flat_note(self):
|
|
4058
|
+
_tell("3d", "Note: 3D is not shown yet, so this scene plays in 2D.")
|
|
4059
|
+
|
|
4060
|
+
def set_camera_orientation(self, *a, **k):
|
|
4061
|
+
self._flat_note()
|
|
4062
|
+
|
|
4063
|
+
def move_camera(self, *a, added_anims=(), run_time=None, **k):
|
|
4064
|
+
self._flat_note()
|
|
4065
|
+
if added_anims:
|
|
4066
|
+
self.play(*added_anims, run_time=run_time)
|
|
4067
|
+
else:
|
|
4068
|
+
self.wait(run_time or 1.0)
|
|
4069
|
+
|
|
4070
|
+
def begin_ambient_camera_rotation(self, *a, **k):
|
|
4071
|
+
self._flat_note()
|
|
4072
|
+
|
|
4073
|
+
def stop_ambient_camera_rotation(self, *a, **k):
|
|
4074
|
+
pass
|
|
4075
|
+
|
|
4076
|
+
def set_to_default_angled_camera_orientation(self, *a, **k):
|
|
4077
|
+
self._flat_note()
|
|
4078
|
+
|
|
4079
|
+
def add_fixed_in_frame_mobjects(self, *mobjects):
|
|
4080
|
+
return self.add(*mobjects)
|
|
4081
|
+
|
|
4082
|
+
add_fixed_orientation_mobjects = add_fixed_in_frame_mobjects
|
|
4083
|
+
|
|
4084
|
+
|
|
4085
|
+
def _split(group, gone):
|
|
4086
|
+
out = []
|
|
4087
|
+
for k in group._kids or ():
|
|
4088
|
+
if k in gone:
|
|
4089
|
+
continue
|
|
4090
|
+
if any(x in k.get_family() for x in gone):
|
|
4091
|
+
out.extend(_split(k, gone))
|
|
4092
|
+
else:
|
|
4093
|
+
out.append(k)
|
|
4094
|
+
return out
|
|
4095
|
+
|
|
4096
|
+
|
|
4097
|
+
def _scene_section(scene, first):
|
|
4098
|
+
tr = scene.transition if scene.transition is not None else config.transition
|
|
4099
|
+
tt = scene.transition_time if scene.transition_time is not None else config.transition_time
|
|
4100
|
+
_section(type(scene).__name__, None if first else tr, tt)
|
|
4101
|
+
|
|
4102
|
+
|
|
4103
|
+
def _run_scene_sync(scene):
|
|
4104
|
+
st = _st()
|
|
4105
|
+
_scene_section(scene, not st.scenes_run)
|
|
4106
|
+
st.scenes_run += 1
|
|
4107
|
+
scene.clear()
|
|
4108
|
+
for step in (scene.setup, scene.construct, scene.tear_down):
|
|
4109
|
+
r = step()
|
|
4110
|
+
if hasattr(r, "send"):
|
|
4111
|
+
r.close()
|
|
4112
|
+
raise TypeError("an async construct() is played by Pymations itself; remove the "
|
|
4113
|
+
"render() call")
|
|
4114
|
+
_end_section_sound()
|
|
4115
|
+
|
|
4116
|
+
|
|
4117
|
+
async def _run_scenes(classes):
|
|
4118
|
+
st = _st()
|
|
4119
|
+
_end_section_sound()
|
|
4120
|
+
for cls in classes:
|
|
4121
|
+
scene = cls()
|
|
4122
|
+
_scene_section(scene, not st.scenes_run)
|
|
4123
|
+
st.scenes_run += 1
|
|
4124
|
+
scene.clear()
|
|
4125
|
+
for step in (scene.setup, scene.construct, scene.tear_down):
|
|
4126
|
+
r = step()
|
|
4127
|
+
if hasattr(r, "send"):
|
|
4128
|
+
await r
|
|
4129
|
+
_end_section_sound()
|
|
4130
|
+
|
|
4131
|
+
|
|
4132
|
+
def _defined_at(cls):
|
|
4133
|
+
"""The line a Scene class starts on, from the functions it defines."""
|
|
4134
|
+
lines = [f.__code__.co_firstlineno for f in vars(cls).values()
|
|
4135
|
+
if getattr(getattr(f, "__code__", None), "co_filename", None) == _rt.USER_FILE]
|
|
4136
|
+
return min(lines) if lines else 0
|
|
4137
|
+
|
|
4138
|
+
|
|
4139
|
+
class _Building:
|
|
4140
|
+
"""The Scene runner as the runtime drives it: while it runs, commands record
|
|
4141
|
+
the timeline even when a Pyctures loop is live (an async construct resumes
|
|
4142
|
+
from a tick)."""
|
|
4143
|
+
|
|
4144
|
+
def __init__(self, coro):
|
|
4145
|
+
self.coro = coro
|
|
4146
|
+
|
|
4147
|
+
def send(self, value):
|
|
4148
|
+
st = _st()
|
|
4149
|
+
prev, st.building = st.building, True
|
|
4150
|
+
try:
|
|
4151
|
+
return self.coro.send(value)
|
|
4152
|
+
finally:
|
|
4153
|
+
st.building = prev
|
|
4154
|
+
|
|
4155
|
+
def throw(self, *exc):
|
|
4156
|
+
return self.coro.throw(*exc)
|
|
4157
|
+
|
|
4158
|
+
def close(self):
|
|
4159
|
+
self.coro.close()
|
|
4160
|
+
|
|
4161
|
+
|
|
4162
|
+
def _after_top_level(ns):
|
|
4163
|
+
"""Every Scene the file defines, in order, each its own section."""
|
|
4164
|
+
st = _st()
|
|
4165
|
+
_end_section_sound() # a bare script's last section(sound=...)
|
|
4166
|
+
classes = [v for v in list(ns.values())
|
|
4167
|
+
if isinstance(v, type) and issubclass(v, Scene) and v.__module__ == ns.get("__name__")
|
|
4168
|
+
and v.construct is not Scene.construct and v not in st.rendered]
|
|
4169
|
+
# In the order they are written: a class reusing a name already in the
|
|
4170
|
+
# namespace (say Picture) would otherwise keep that name's earlier place.
|
|
4171
|
+
classes.sort(key=_defined_at)
|
|
4172
|
+
if classes:
|
|
4173
|
+
return _Building(_run_scenes(classes))
|
|
4174
|
+
return None
|
|
4175
|
+
|
|
4176
|
+
|
|
4177
|
+
# ---------------------------------------------------------------- the bare form
|
|
4178
|
+
|
|
4179
|
+
def _bare():
|
|
4180
|
+
st = _st()
|
|
4181
|
+
if st.scene is None:
|
|
4182
|
+
st.scene = Scene()
|
|
4183
|
+
return st.scene
|
|
4184
|
+
|
|
4185
|
+
|
|
4186
|
+
def play(*animations, **kwargs):
|
|
4187
|
+
"""Play animations on the file's timeline (a script without a Scene class)."""
|
|
4188
|
+
return _bare().play(*animations, **kwargs)
|
|
4189
|
+
|
|
4190
|
+
|
|
4191
|
+
def wait(duration=DEFAULT_WAIT_TIME, **kwargs):
|
|
4192
|
+
return _bare().wait(duration)
|
|
4193
|
+
|
|
4194
|
+
|
|
4195
|
+
def add(*mobjects):
|
|
4196
|
+
return _bare().add(*mobjects)
|
|
4197
|
+
|
|
4198
|
+
|
|
4199
|
+
def remove(*mobjects):
|
|
4200
|
+
return _bare().remove(*mobjects)
|
|
4201
|
+
|
|
4202
|
+
|
|
4203
|
+
def clear():
|
|
4204
|
+
return _bare().clear()
|
|
4205
|
+
|
|
4206
|
+
|
|
4207
|
+
def section(name, sound=None, transition=None, transition_time=None):
|
|
4208
|
+
"""Start a named section of the timeline (a tick on the scrubber). With sound=,
|
|
4209
|
+
play that voice-over and make the section last at least as long as it."""
|
|
4210
|
+
_section(name, transition, transition_time, sound)
|
|
4211
|
+
|
|
4212
|
+
|
|
4213
|
+
# numpy when the runtime has it, else the little that scene scripts use. Both
|
|
4214
|
+
# turn maths on preview values (tracker values, positions, time) into formulas:
|
|
4215
|
+
# the stand-in directly, real numpy through Expr.__array_ufunc__.
|
|
4216
|
+
def _np_fn(name, py):
|
|
4217
|
+
def f(*args):
|
|
4218
|
+
if any(_is_sym(a) for a in args):
|
|
4219
|
+
return _fn(name, *[_tree_arg(a) for a in args])
|
|
4220
|
+
if any(isinstance(a, (list, tuple)) for a in args):
|
|
4221
|
+
n = max(len(a) for a in args if isinstance(a, (list, tuple)))
|
|
4222
|
+
cols = [[a[i] if isinstance(a, (list, tuple)) else a for a in args] for i in range(n)]
|
|
4223
|
+
out = [py(*c) for c in cols]
|
|
4224
|
+
return Vec.of(out) if len(out) == 3 and isinstance(args[0], Vec) else out
|
|
4225
|
+
return py(*args)
|
|
4226
|
+
f.__name__ = name
|
|
4227
|
+
return f
|
|
4228
|
+
|
|
4229
|
+
|
|
4230
|
+
def _tree_arg(a):
|
|
4231
|
+
return Expr(_tree(a), _shape_of(a)) if isinstance(a, (list, tuple)) else a
|
|
4232
|
+
|
|
4233
|
+
|
|
4234
|
+
class _NumpyLite:
|
|
4235
|
+
pi = math.pi
|
|
4236
|
+
e = math.e
|
|
4237
|
+
inf = float("inf")
|
|
4238
|
+
|
|
4239
|
+
@staticmethod
|
|
4240
|
+
def array(v, dtype=None):
|
|
4241
|
+
v = list(v)
|
|
4242
|
+
if len(v) in (2, 3) and all(isinstance(x, (int, float)) for x in v):
|
|
4243
|
+
return Vec.of(v)
|
|
4244
|
+
return v
|
|
4245
|
+
|
|
4246
|
+
asarray = array
|
|
4247
|
+
|
|
4248
|
+
@staticmethod
|
|
4249
|
+
def linspace(a, b, n=50, endpoint=True):
|
|
4250
|
+
n = int(n)
|
|
4251
|
+
d = (n - 1) if endpoint else n
|
|
4252
|
+
return [a + (b - a) * i / (d or 1) for i in range(n)]
|
|
4253
|
+
|
|
4254
|
+
@staticmethod
|
|
4255
|
+
def arange(a, b=None, step=1):
|
|
4256
|
+
if b is None:
|
|
4257
|
+
a, b = 0, a
|
|
4258
|
+
out, x = [], a
|
|
4259
|
+
while (x < b) if step > 0 else (x > b):
|
|
4260
|
+
out.append(x)
|
|
4261
|
+
x += step
|
|
4262
|
+
return out
|
|
4263
|
+
|
|
4264
|
+
@staticmethod
|
|
4265
|
+
def zeros(n):
|
|
4266
|
+
return Vec(0, 0, 0) if n == 3 else [0.0] * int(n)
|
|
4267
|
+
|
|
4268
|
+
@staticmethod
|
|
4269
|
+
def ones(n):
|
|
4270
|
+
return Vec(1, 1, 1) if n == 3 else [1.0] * int(n)
|
|
4271
|
+
|
|
4272
|
+
@staticmethod
|
|
4273
|
+
def where(c, a, b):
|
|
4274
|
+
if any(_is_sym(x) for x in (c, a, b)):
|
|
4275
|
+
return Expr(["where", _tree(c), _tree(a), _tree(b)],
|
|
4276
|
+
"v" if "v" in (_shape_of(a), _shape_of(b)) else "s")
|
|
4277
|
+
return a if c else b
|
|
4278
|
+
|
|
4279
|
+
@staticmethod
|
|
4280
|
+
def dot(a, b):
|
|
4281
|
+
return sum(x * y for x, y in zip(a, b))
|
|
4282
|
+
|
|
4283
|
+
@staticmethod
|
|
4284
|
+
def sum(v):
|
|
4285
|
+
return sum(v)
|
|
4286
|
+
|
|
4287
|
+
@staticmethod
|
|
4288
|
+
def mean(v):
|
|
4289
|
+
v = list(v)
|
|
4290
|
+
return sum(v) / len(v)
|
|
4291
|
+
|
|
4292
|
+
class linalg:
|
|
4293
|
+
@staticmethod
|
|
4294
|
+
def norm(v):
|
|
4295
|
+
if _is_sym(v):
|
|
4296
|
+
return _fn("norm", _tree_arg(v))
|
|
4297
|
+
return math.sqrt(sum(x * x for x in v))
|
|
4298
|
+
|
|
4299
|
+
|
|
4300
|
+
for _k, _py in (("sin", math.sin), ("cos", math.cos), ("tan", math.tan), ("arcsin", math.asin),
|
|
4301
|
+
("arccos", math.acos), ("arctan", math.atan), ("arctan2", math.atan2), ("sinh", math.sinh),
|
|
4302
|
+
("cosh", math.cosh), ("tanh", math.tanh), ("exp", math.exp), ("log", math.log),
|
|
4303
|
+
("log10", math.log10), ("log2", math.log2), ("sqrt", math.sqrt), ("abs", abs),
|
|
4304
|
+
("absolute", abs), ("floor", math.floor), ("ceil", math.ceil), ("round", round),
|
|
4305
|
+
("sign", lambda x: (x > 0) - (x < 0)), ("clip", lambda x, lo, hi: min(max(x, lo), hi)),
|
|
4306
|
+
("minimum", min), ("maximum", max), ("hypot", math.hypot), ("degrees", math.degrees),
|
|
4307
|
+
("radians", math.radians)):
|
|
4308
|
+
setattr(_NumpyLite, _k, staticmethod(_np_fn(_UFUNCS.get(_k, _k), _py)))
|
|
4309
|
+
|
|
4310
|
+
|
|
4311
|
+
class _LazyNumpy:
|
|
4312
|
+
"""'np' as Manim's star import gives it, loaded on first use."""
|
|
4313
|
+
|
|
4314
|
+
def _real(self):
|
|
4315
|
+
mod = self.__dict__.get("_mod")
|
|
4316
|
+
if mod is None:
|
|
4317
|
+
try:
|
|
4318
|
+
import numpy as mod
|
|
4319
|
+
except Exception:
|
|
4320
|
+
mod = _NumpyLite
|
|
4321
|
+
self.__dict__["_mod"] = mod
|
|
4322
|
+
return mod
|
|
4323
|
+
|
|
4324
|
+
def __getattr__(self, name):
|
|
4325
|
+
if name.startswith("__"):
|
|
4326
|
+
raise AttributeError(name)
|
|
4327
|
+
return getattr(self._real(), name)
|
|
4328
|
+
|
|
4329
|
+
|
|
4330
|
+
np = _LazyNumpy()
|
|
4331
|
+
|
|
4332
|
+
|
|
4333
|
+
# ---------------------------------------------------------------- install
|
|
4334
|
+
|
|
4335
|
+
# time() stays pymations.time(): a star import must not hide the time module.
|
|
4336
|
+
__all__ = [n for n in list(globals()) if not n.startswith("_") and n not in (
|
|
4337
|
+
"sys", "math", "types", "inspect", "VERSION", "H_ANIM", "time")]
|
|
4338
|
+
|
|
4339
|
+
_rt._on_start[:] = [h for h in _rt._on_start if getattr(h, "__module__", None) != __name__] + [_on_start]
|
|
4340
|
+
_rt._after_top_level[:] = [h for h in _rt._after_top_level
|
|
4341
|
+
if getattr(h, "__module__", None) != __name__] + [_after_top_level]
|
|
4342
|
+
_rt._before_frame[:] = [h for h in _rt._before_frame
|
|
4343
|
+
if getattr(h, "__module__", None) != __name__] + [_before_frame]
|
|
4344
|
+
_rt._on_frame[:] = [h for h in _rt._on_frame if getattr(h, "__module__", None) != __name__] + [_on_frame]
|
|
4345
|
+
_rt._live_hooks[:] = [h for h in _rt._live_hooks if getattr(h, "__module__", None) != __name__] + [_stays_live]
|
|
4346
|
+
_rt._hidden_vars.update({n: globals()[n] for n in __all__})
|