pymations 0.1.0 → 0.2.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/README.md +1 -1
- package/THIRD-PARTY-NOTICES +23 -0
- package/engine/LICENSE-polygon-clipping.txt +11 -0
- package/engine/anim.js +8 -7
- package/engine/manifest.json +1 -0
- package/package.json +1 -1
- package/pyctures.py +4 -2
- package/pymations.js +17 -4
- package/pymations.py +803 -36
package/pymations.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# GENERATED by circuitry/scripts/sync-pymations-lib.mjs from src/lib/python-canvas/canvas-anim-py.ts (version
|
|
1
|
+
# GENERATED by circuitry/scripts/sync-pymations-lib.mjs from src/lib/python-canvas/canvas-anim-py.ts (version 22).
|
|
2
2
|
# Do not edit here: change it in Circuitry, then run the script.
|
|
3
3
|
# Pymations is MIT licensed; see LICENSE.
|
|
4
4
|
import sys, math, types, inspect
|
|
5
5
|
|
|
6
|
-
VERSION = "
|
|
6
|
+
VERSION = "22"
|
|
7
7
|
_rt = sys.modules["pyctures"]
|
|
8
8
|
H_ANIM = 6
|
|
9
9
|
|
|
@@ -101,6 +101,41 @@ def _reset_once():
|
|
|
101
101
|
_rt._emit("call", 0, H_ANIM, "reset", [{}])
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
def _outer_opts():
|
|
105
|
+
"""Where a play()/wait() sits in the user's code, for the editor's cursor:
|
|
106
|
+
{"outer_line": n} when it is called from a helper function (n is the line in
|
|
107
|
+
construct(), or the script's top level, that led here), and {"line_end": m}
|
|
108
|
+
when that call spans several lines (Python 3.11+ knows the end). Empty for a
|
|
109
|
+
one-line call written in construct()."""
|
|
110
|
+
f = sys._getframe(1)
|
|
111
|
+
inner = outer = 0
|
|
112
|
+
outer_frame = None
|
|
113
|
+
while f is not None:
|
|
114
|
+
if f.f_code.co_filename == _rt.USER_FILE:
|
|
115
|
+
inner = inner or f.f_lineno
|
|
116
|
+
outer, outer_frame = f.f_lineno, f
|
|
117
|
+
if f.f_code.co_name in ("construct", "<module>"):
|
|
118
|
+
break
|
|
119
|
+
f = f.f_back
|
|
120
|
+
opts = {"outer_line": outer} if outer and outer != inner else {}
|
|
121
|
+
end = _call_end_line(outer_frame)
|
|
122
|
+
if end and end > outer:
|
|
123
|
+
opts["line_end"] = end
|
|
124
|
+
return opts
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _call_end_line(frame):
|
|
128
|
+
"""The last line of the call a frame is making (Python 3.11+), else None."""
|
|
129
|
+
positions = getattr(getattr(frame, "f_code", None), "co_positions", None)
|
|
130
|
+
if positions is None or frame.f_lasti < 0:
|
|
131
|
+
return None
|
|
132
|
+
try:
|
|
133
|
+
pos = list(positions())[frame.f_lasti // 2]
|
|
134
|
+
except (IndexError, ValueError):
|
|
135
|
+
return None
|
|
136
|
+
return pos[1]
|
|
137
|
+
|
|
138
|
+
|
|
104
139
|
def _call(method, *args):
|
|
105
140
|
if _st().trace is not None:
|
|
106
141
|
raise _Untraceable("it calls %s()" % method)
|
|
@@ -1283,6 +1318,10 @@ class Mobject:
|
|
|
1283
1318
|
def has_updaters(self):
|
|
1284
1319
|
return bool(self.__dict__.get("updaters"))
|
|
1285
1320
|
|
|
1321
|
+
def apply_function(self, function, **kwargs):
|
|
1322
|
+
"""Move every point p to function(p) (np maths on p works)."""
|
|
1323
|
+
return self._op("apply_function", {"function": _trace_formula(function, (_var("p", "v"),), "apply_function")})
|
|
1324
|
+
|
|
1286
1325
|
def point_from_proportion(self, alpha):
|
|
1287
1326
|
return Point(["get", self._ref(), "point_from_proportion", _coord(alpha)])
|
|
1288
1327
|
|
|
@@ -2131,7 +2170,8 @@ class Axes(VMobject):
|
|
|
2131
2170
|
"tips": bool(tips), "axis_config": _style_dict(axis_config),
|
|
2132
2171
|
"x_axis_config": _style_dict(x_axis_config), "y_axis_config": _style_dict(y_axis_config)}
|
|
2133
2172
|
geom.update(self._extra(kwargs))
|
|
2134
|
-
self._make(self._kind, geom, kwargs,
|
|
2173
|
+
self._make(self._kind, geom, kwargs,
|
|
2174
|
+
n=4 if isinstance(self, NumberPlane) else 3 if isinstance(self, ThreeDAxes) else 2)
|
|
2135
2175
|
|
|
2136
2176
|
def _length(self, frame_length, r):
|
|
2137
2177
|
return float(frame_length)
|
|
@@ -2300,10 +2340,57 @@ class Axes(VMobject):
|
|
|
2300
2340
|
|
|
2301
2341
|
|
|
2302
2342
|
class ThreeDAxes(Axes):
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2343
|
+
"""x, y and z axes (parts [0], [1], [2]), seen through the 3D camera."""
|
|
2344
|
+
_kind = "three_d_axes"
|
|
2345
|
+
|
|
2346
|
+
def __init__(self, x_range=(-6, 6, 1), y_range=(-5, 5, 1), z_range=(-4, 4, 1), x_length=None, y_length=None,
|
|
2347
|
+
z_length=None, z_axis_config=None, z_normal=DOWN, num_axis_pieces=20, light_source=None, depth=None,
|
|
2348
|
+
gloss=0.5, **kwargs):
|
|
2349
|
+
_w, h = _frame_size()
|
|
2350
|
+
self.z_range = _range3(z_range, (-4, 4, 1))
|
|
2351
|
+
self._z = {"z_range": self.z_range,
|
|
2352
|
+
"z_length": _num(z_length) if z_length is not None else h - 1.5,
|
|
2353
|
+
"z_axis_config": _style_dict(z_axis_config), "z_normal": _P(z_normal)}
|
|
2354
|
+
Axes.__init__(self, x_range, y_range, x_length if x_length is not None else h + 2.5,
|
|
2355
|
+
y_length if y_length is not None else h + 2.5, **kwargs)
|
|
2356
|
+
|
|
2357
|
+
def _extra(self, kwargs):
|
|
2358
|
+
return self._z
|
|
2359
|
+
|
|
2360
|
+
@property
|
|
2361
|
+
def z_axis(self):
|
|
2362
|
+
return self[2]
|
|
2363
|
+
|
|
2364
|
+
def get_axes(self):
|
|
2365
|
+
return VGroup(self[0], self[1], self[2])
|
|
2366
|
+
|
|
2367
|
+
def coords_to_point(self, *coords):
|
|
2368
|
+
if len(coords) == 1:
|
|
2369
|
+
coords = tuple(_unarray(coords[0]))
|
|
2370
|
+
x = _coord(coords[0])
|
|
2371
|
+
y = _coord(coords[1]) if len(coords) > 1 else 0.0
|
|
2372
|
+
z = _coord(coords[2]) if len(coords) > 2 else 0.0
|
|
2373
|
+
return Point(["c2p", self._ref(), x, y, z])
|
|
2374
|
+
|
|
2375
|
+
c2p = coords_to_point
|
|
2376
|
+
|
|
2377
|
+
def get_z_axis_label(self, label, edge=OUT, direction=RIGHT, buff=SMALL_BUFF, rotation=PI / 2,
|
|
2378
|
+
rotation_axis=RIGHT, **kwargs):
|
|
2379
|
+
m = label if isinstance(label, Mobject) else MathTex(label)
|
|
2380
|
+
m.rotate(rotation, axis=rotation_axis)
|
|
2381
|
+
return m.next_to(self.z_axis.get_corner(edge), direction, buff=buff)
|
|
2382
|
+
|
|
2383
|
+
def get_axis_labels(self, x_label="x", y_label="y", z_label="z"):
|
|
2384
|
+
return VGroup(self.get_x_axis_label(x_label), self.get_y_axis_label(y_label), self.get_z_axis_label(z_label))
|
|
2385
|
+
|
|
2386
|
+
def plot_surface(self, function, u_range=None, v_range=None, colorscale=None, colorscale_axis=2, **kwargs):
|
|
2387
|
+
"""z = function(u, v) over the axes, sampled here on a grid."""
|
|
2388
|
+
u = list(u_range) if u_range is not None else self.x_range[:2]
|
|
2389
|
+
v = list(v_range) if v_range is not None else self.y_range[:2]
|
|
2390
|
+
surface = Surface(lambda a, b: [a, b, function(a, b)], u_range=u, v_range=v, _axes=self, **kwargs)
|
|
2391
|
+
if colorscale is not None:
|
|
2392
|
+
surface.set_fill_by_value(self, colorscale, axis=colorscale_axis)
|
|
2393
|
+
return surface
|
|
2307
2394
|
|
|
2308
2395
|
|
|
2309
2396
|
class NumberPlane(Axes):
|
|
@@ -2393,6 +2480,49 @@ class _Camera:
|
|
|
2393
2480
|
def __init__(self):
|
|
2394
2481
|
self.frame = CameraFrame()
|
|
2395
2482
|
|
|
2483
|
+
# The 3D camera's angles (ThreeDScene), as trackers and as attributes.
|
|
2484
|
+
phi_tracker = property(lambda self: _camera_tracker("phi"))
|
|
2485
|
+
theta_tracker = property(lambda self: _camera_tracker("theta"))
|
|
2486
|
+
gamma_tracker = property(lambda self: _camera_tracker("gamma"))
|
|
2487
|
+
zoom_tracker = property(lambda self: _camera_tracker("zoom"))
|
|
2488
|
+
focal_distance_tracker = property(lambda self: _camera_tracker("focal_distance"))
|
|
2489
|
+
|
|
2490
|
+
def get_phi(self):
|
|
2491
|
+
return self.phi_tracker.get_value()
|
|
2492
|
+
|
|
2493
|
+
def get_theta(self):
|
|
2494
|
+
return self.theta_tracker.get_value()
|
|
2495
|
+
|
|
2496
|
+
def get_gamma(self):
|
|
2497
|
+
return self.gamma_tracker.get_value()
|
|
2498
|
+
|
|
2499
|
+
def get_zoom(self):
|
|
2500
|
+
return self.zoom_tracker.get_value()
|
|
2501
|
+
|
|
2502
|
+
def get_focal_distance(self):
|
|
2503
|
+
return self.focal_distance_tracker.get_value()
|
|
2504
|
+
|
|
2505
|
+
def set_phi(self, value):
|
|
2506
|
+
_camera_op("set_camera_orientation", _camera_args(phi=value))
|
|
2507
|
+
|
|
2508
|
+
def set_theta(self, value):
|
|
2509
|
+
_camera_op("set_camera_orientation", _camera_args(theta=value))
|
|
2510
|
+
|
|
2511
|
+
def set_gamma(self, value):
|
|
2512
|
+
_camera_op("set_camera_orientation", _camera_args(gamma=value))
|
|
2513
|
+
|
|
2514
|
+
def set_zoom(self, value):
|
|
2515
|
+
_camera_op("set_camera_orientation", _camera_args(zoom=value))
|
|
2516
|
+
|
|
2517
|
+
def set_focal_distance(self, value):
|
|
2518
|
+
_camera_op("set_camera_orientation", _camera_args(focal_distance=value))
|
|
2519
|
+
|
|
2520
|
+
phi = property(get_phi, set_phi)
|
|
2521
|
+
theta = property(get_theta, set_theta)
|
|
2522
|
+
gamma = property(get_gamma, set_gamma)
|
|
2523
|
+
zoom = property(get_zoom, set_zoom)
|
|
2524
|
+
focal_distance = property(get_focal_distance, set_focal_distance)
|
|
2525
|
+
|
|
2396
2526
|
@property
|
|
2397
2527
|
def background_color(self):
|
|
2398
2528
|
return config.background_color
|
|
@@ -2956,10 +3086,13 @@ class MarkupText(Text):
|
|
|
2956
3086
|
_FILLED, n=sum(1 for ch in plain if not ch.isspace()))
|
|
2957
3087
|
|
|
2958
3088
|
|
|
2959
|
-
def _entry(v, element):
|
|
2960
|
-
"""A table or matrix
|
|
3089
|
+
def _entry(v, element=None):
|
|
3090
|
+
"""A table or matrix cell: a string the engine sets as the element kind, or an
|
|
3091
|
+
object already made, sent as {"id": n, "path": [...]} so it can never be
|
|
3092
|
+
mistaken for a number to write out. The engine places that object in the cell."""
|
|
2961
3093
|
if isinstance(v, Mobject):
|
|
2962
|
-
|
|
3094
|
+
t = v._embed()
|
|
3095
|
+
return {"id": t, "path": []} if isinstance(t, int) else t
|
|
2963
3096
|
return str(v)
|
|
2964
3097
|
|
|
2965
3098
|
|
|
@@ -2994,9 +3127,9 @@ class Table(VGroup):
|
|
|
2994
3127
|
self._kids = None
|
|
2995
3128
|
self._make("table", {
|
|
2996
3129
|
"table": [[_entry(v, element) for v in r] for r in rows], "element": element,
|
|
2997
|
-
"row_labels": [m
|
|
2998
|
-
"col_labels": [m
|
|
2999
|
-
"top_left_entry": top_left_entry
|
|
3130
|
+
"row_labels": [_entry(m) for m in row_labels] if row_labels else None,
|
|
3131
|
+
"col_labels": [_entry(m) for m in col_labels] if col_labels else None,
|
|
3132
|
+
"top_left_entry": _entry(top_left_entry) if top_left_entry is not None else None,
|
|
3000
3133
|
"v_buff": _num(v_buff), "h_buff": _num(h_buff), "include_outer_lines": bool(include_outer_lines),
|
|
3001
3134
|
"include_inner_lines": bool(include_inner_lines),
|
|
3002
3135
|
"include_background_rectangle": bool(include_background_rectangle),
|
|
@@ -3902,11 +4035,16 @@ def _end_section_sound():
|
|
|
3902
4035
|
_call("wait_sound", {"src": src, "extra": 0})
|
|
3903
4036
|
|
|
3904
4037
|
|
|
3905
|
-
def _section(name, transition=None, transition_time=None, sound=None):
|
|
4038
|
+
def _section(name, transition=None, transition_time=None, sound=None, lines=None):
|
|
3906
4039
|
_end_section_sound()
|
|
4040
|
+
opts = {}
|
|
3907
4041
|
if transition and transition != "none":
|
|
3908
4042
|
tt = transition_time if transition_time is not None else (config.transition_time or 0.5)
|
|
3909
|
-
|
|
4043
|
+
opts.update(transition=transition, transition_time=_num(tt))
|
|
4044
|
+
if lines:
|
|
4045
|
+
opts["lines"] = list(lines) # the Scene class's first and last source line
|
|
4046
|
+
if opts:
|
|
4047
|
+
_call("section", str(name), opts)
|
|
3910
4048
|
else:
|
|
3911
4049
|
_call("section", str(name))
|
|
3912
4050
|
if sound is not None:
|
|
@@ -3914,6 +4052,566 @@ def _section(name, transition=None, transition_time=None, sound=None):
|
|
|
3914
4052
|
_st().section_sound = str(sound)
|
|
3915
4053
|
|
|
3916
4054
|
|
|
4055
|
+
# ---------------------------------------------------------------- 3D
|
|
4056
|
+
#
|
|
4057
|
+
# 3D is drawn the way Manim draws it: shapes projected through a 3D camera,
|
|
4058
|
+
# sorted by depth and shaded, by the engine. Python sends the objects (surfaces
|
|
4059
|
+
# sampled here on their grid, like plots) and the camera's moves.
|
|
4060
|
+
|
|
4061
|
+
def _res2(resolution, default):
|
|
4062
|
+
r = default if resolution is None else resolution
|
|
4063
|
+
if isinstance(r, (int, float)):
|
|
4064
|
+
return [int(r), int(r)]
|
|
4065
|
+
r = list(r)
|
|
4066
|
+
return [int(r[0]), int(r[1] if len(r) > 1 else r[0])]
|
|
4067
|
+
|
|
4068
|
+
|
|
4069
|
+
def _surface_style(kwargs, fill_color=BLUE_D, fill_opacity=1.0, checkerboard_colors=(BLUE_D, BLUE_E),
|
|
4070
|
+
stroke_color=LIGHT_GREY, stroke_width=0.5, set_color=False):
|
|
4071
|
+
"""Manim's Surface look: a checkerboard of two blues, a thin grey grid. As in
|
|
4072
|
+
Manim, color= / fill_color= change the fill under the checkerboard, which stays
|
|
4073
|
+
unless checkerboard_colors=False. set_color: the shape calls Manim's
|
|
4074
|
+
set_color(color) itself (Dot3D, Arrow3D, a coloured Line3D): one colour over
|
|
4075
|
+
fill and stroke, no checkerboard."""
|
|
4076
|
+
checkerboard_colors = kwargs.pop("checkerboard_colors", checkerboard_colors)
|
|
4077
|
+
color = kwargs.pop("color", None)
|
|
4078
|
+
if color is not None:
|
|
4079
|
+
fill_color = color
|
|
4080
|
+
if set_color:
|
|
4081
|
+
checkerboard_colors = False
|
|
4082
|
+
if "stroke_color" not in kwargs:
|
|
4083
|
+
stroke_color = color
|
|
4084
|
+
if "fill_color" in kwargs:
|
|
4085
|
+
fill_color = kwargs.pop("fill_color")
|
|
4086
|
+
cb = [_color(c) for c in checkerboard_colors] if checkerboard_colors else False
|
|
4087
|
+
return {"fill_color": _color(fill_color), "fill_opacity": _num(kwargs.pop("fill_opacity", fill_opacity)),
|
|
4088
|
+
"checkerboard_colors": cb, "stroke_color": _color(kwargs.pop("stroke_color", stroke_color)),
|
|
4089
|
+
"stroke_width": _num(kwargs.pop("stroke_width", stroke_width))}
|
|
4090
|
+
|
|
4091
|
+
|
|
4092
|
+
def _grid(func, u_range, v_range, res, to_point):
|
|
4093
|
+
(u0, u1), (v0, v1) = (float(u_range[0]), float(u_range[1])), (float(v_range[0]), float(v_range[1]))
|
|
4094
|
+
ru, rv = res
|
|
4095
|
+
pts = []
|
|
4096
|
+
for i in range(ru + 1):
|
|
4097
|
+
u = u0 + (u1 - u0) * i / ru
|
|
4098
|
+
for j in range(rv + 1):
|
|
4099
|
+
v = v0 + (v1 - v0) * j / rv
|
|
4100
|
+
pts.append(to_point(func, u, v))
|
|
4101
|
+
return pts
|
|
4102
|
+
|
|
4103
|
+
|
|
4104
|
+
def _uv_point(func, u, v):
|
|
4105
|
+
p = _unarray(func(u, v))
|
|
4106
|
+
if _is_sym(p):
|
|
4107
|
+
p = list(p)
|
|
4108
|
+
return {"$pt": _tree((p + [0.0, 0.0])[:3])}
|
|
4109
|
+
return list(Vec.of(p))
|
|
4110
|
+
|
|
4111
|
+
|
|
4112
|
+
class Surface(VGroup):
|
|
4113
|
+
"""func(u, v) → a point, sampled on a resolution grid (u outer, v inner)."""
|
|
4114
|
+
|
|
4115
|
+
def __init__(self, func, u_range=(0, 1), v_range=(0, 1), resolution=32, surface_piece_config=None,
|
|
4116
|
+
fill_color=BLUE_D, fill_opacity=1.0, checkerboard_colors=(BLUE_D, BLUE_E), stroke_color=LIGHT_GREY,
|
|
4117
|
+
stroke_width=0.5, should_make_jagged=False, pre_function_handle_to_anchor_scale_factor=0.00001,
|
|
4118
|
+
_axes=None, **kwargs):
|
|
4119
|
+
self._func = func
|
|
4120
|
+
self.u_range, self.v_range = list(u_range), list(v_range)
|
|
4121
|
+
res = self.resolution = _res2(resolution, 32)
|
|
4122
|
+
style = _surface_style(kwargs, fill_color=fill_color, fill_opacity=fill_opacity,
|
|
4123
|
+
checkerboard_colors=checkerboard_colors, stroke_color=stroke_color,
|
|
4124
|
+
stroke_width=stroke_width)
|
|
4125
|
+
flat = _grid(func, u_range, v_range, res, _uv_point)
|
|
4126
|
+
# grid[i][j]: u step i (outer), v step j (inner).
|
|
4127
|
+
grid = [flat[i * (res[1] + 1):(i + 1) * (res[1] + 1)] for i in range(res[0] + 1)]
|
|
4128
|
+
geom = dict(style, grid=grid, resolution=res, u_range=[_num(x) for x in u_range[:2]],
|
|
4129
|
+
v_range=[_num(x) for x in v_range[:2]])
|
|
4130
|
+
if _axes is not None:
|
|
4131
|
+
geom["axes"] = _axes._ref()
|
|
4132
|
+
self._kids = None
|
|
4133
|
+
self._make("surface", geom, kwargs, {}, n=res[0] * res[1])
|
|
4134
|
+
|
|
4135
|
+
def set_fill_by_checkerboard(self, *colors, opacity=None):
|
|
4136
|
+
return self._op("set_fill_by_checkerboard", {"colors": [_color(c) for c in _flat_colors(colors)],
|
|
4137
|
+
"opacity": None if opacity is None else _num(opacity)})
|
|
4138
|
+
|
|
4139
|
+
def set_fill_by_value(self, axes, colorscale=None, axis=2, **kwargs):
|
|
4140
|
+
scale = colorscale if colorscale is not None else [BLUE, GREEN, YELLOW, RED]
|
|
4141
|
+
enc = [[_color(c[0]), _num(c[1])] if isinstance(c, (list, tuple)) else _color(c) for c in scale]
|
|
4142
|
+
return self._op("set_fill_by_value", {"axes": axes._ref(), "colorscale": enc, "axis": int(axis)})
|
|
4143
|
+
|
|
4144
|
+
|
|
4145
|
+
class _Solid(VGroup):
|
|
4146
|
+
"""A 3D shape the engine builds from its parameters (Manim's look and defaults)."""
|
|
4147
|
+
_kind = ""
|
|
4148
|
+
_style = None # None = the Surface checkerboard
|
|
4149
|
+
|
|
4150
|
+
_set_color = False # True: Manim's class colours itself with set_color(color)
|
|
4151
|
+
|
|
4152
|
+
def _solid(self, geom, kwargs, n, style_defaults=None):
|
|
4153
|
+
self._kids = None
|
|
4154
|
+
style = _surface_style(kwargs, set_color=self._set_color, **(style_defaults or {}))
|
|
4155
|
+
self._make(self._kind, dict(style, **geom), kwargs, {}, n=n)
|
|
4156
|
+
|
|
4157
|
+
|
|
4158
|
+
class Sphere(_Solid):
|
|
4159
|
+
_kind = "sphere"
|
|
4160
|
+
|
|
4161
|
+
def __init__(self, center=ORIGIN, radius=1, resolution=None, u_range=(0, TAU), v_range=(0, PI), **kwargs):
|
|
4162
|
+
res = _res2(resolution, (24, 12))
|
|
4163
|
+
self._solid({"center": _P(center), "radius": _num(radius), "resolution": res,
|
|
4164
|
+
"u_range": [_num(x) for x in u_range], "v_range": [_num(x) for x in v_range]},
|
|
4165
|
+
kwargs, res[0] * res[1])
|
|
4166
|
+
|
|
4167
|
+
|
|
4168
|
+
class Dot3D(Sphere):
|
|
4169
|
+
_kind = "dot3d"
|
|
4170
|
+
_set_color = True
|
|
4171
|
+
|
|
4172
|
+
def __init__(self, point=ORIGIN, radius=DEFAULT_DOT_RADIUS, color=WHITE, resolution=(8, 8), **kwargs):
|
|
4173
|
+
res = _res2(resolution, (8, 8))
|
|
4174
|
+
self._solid({"point": _P(point), "radius": _num(radius), "resolution": res}, dict(kwargs, color=color),
|
|
4175
|
+
res[0] * res[1])
|
|
4176
|
+
|
|
4177
|
+
|
|
4178
|
+
class Torus(_Solid):
|
|
4179
|
+
_kind = "torus"
|
|
4180
|
+
|
|
4181
|
+
def __init__(self, major_radius=3, minor_radius=1, u_range=(0, TAU), v_range=(0, TAU), resolution=None,
|
|
4182
|
+
**kwargs):
|
|
4183
|
+
res = _res2(resolution, (24, 24))
|
|
4184
|
+
self._solid({"major_radius": _num(major_radius), "minor_radius": _num(minor_radius), "resolution": res,
|
|
4185
|
+
"u_range": [_num(x) for x in u_range], "v_range": [_num(x) for x in v_range]},
|
|
4186
|
+
kwargs, res[0] * res[1])
|
|
4187
|
+
|
|
4188
|
+
|
|
4189
|
+
class Cone(_Solid):
|
|
4190
|
+
_kind = "cone"
|
|
4191
|
+
|
|
4192
|
+
def __init__(self, base_radius=1, height=1, direction=Z_AXIS, show_base=False, v_range=(0, TAU), u_min=0,
|
|
4193
|
+
checkerboard_colors=False, resolution=None, **kwargs):
|
|
4194
|
+
res = _res2(resolution, 32)
|
|
4195
|
+
self._solid({"base_radius": _num(base_radius), "height": _num(height), "direction": _P(direction),
|
|
4196
|
+
"show_base": bool(show_base), "v_range": [_num(x) for x in v_range], "u_min": _num(u_min),
|
|
4197
|
+
"resolution": res}, kwargs, None, {"checkerboard_colors": checkerboard_colors})
|
|
4198
|
+
|
|
4199
|
+
|
|
4200
|
+
class Cylinder(_Solid):
|
|
4201
|
+
_kind = "cylinder"
|
|
4202
|
+
|
|
4203
|
+
def __init__(self, radius=1, height=2, direction=Z_AXIS, v_range=(0, TAU), show_ends=True, resolution=(24, 24),
|
|
4204
|
+
**kwargs):
|
|
4205
|
+
res = _res2(resolution, (24, 24))
|
|
4206
|
+
self._solid({"radius": _num(radius), "height": _num(height), "direction": _P(direction),
|
|
4207
|
+
"v_range": [_num(x) for x in v_range], "show_ends": bool(show_ends), "resolution": res},
|
|
4208
|
+
kwargs, None)
|
|
4209
|
+
|
|
4210
|
+
|
|
4211
|
+
def _line_res(resolution):
|
|
4212
|
+
"""A Line3D's resolution: a number is the way round; along it, 2 (Manim's)."""
|
|
4213
|
+
if isinstance(resolution, (int, float)):
|
|
4214
|
+
return [2, int(resolution)]
|
|
4215
|
+
return _res2(resolution, (2, 24))
|
|
4216
|
+
|
|
4217
|
+
|
|
4218
|
+
class Line3D(Cylinder):
|
|
4219
|
+
_kind = "line3d"
|
|
4220
|
+
|
|
4221
|
+
def __init__(self, start=LEFT, end=RIGHT, thickness=0.02, color=None, resolution=24, **kwargs):
|
|
4222
|
+
if color is not None:
|
|
4223
|
+
kwargs["color"] = color
|
|
4224
|
+
self._set_color = True
|
|
4225
|
+
self.start, self.end = start, end
|
|
4226
|
+
self._solid({"start": _MP(start), "end": _MP(end), "thickness": _num(thickness),
|
|
4227
|
+
"resolution": _line_res(resolution)}, kwargs, None)
|
|
4228
|
+
|
|
4229
|
+
@classmethod
|
|
4230
|
+
def parallel_to(cls, line, point=ORIGIN, length=5, **kwargs):
|
|
4231
|
+
d = line.get_end() - line.get_start()
|
|
4232
|
+
return cls(point - d * (length / 2.0) / _fn("norm", d), point + d * (length / 2.0) / _fn("norm", d), **kwargs)
|
|
4233
|
+
|
|
4234
|
+
@classmethod
|
|
4235
|
+
def perpendicular_to(cls, line, point=ORIGIN, length=5, **kwargs):
|
|
4236
|
+
raise NotImplementedError("Line3D.perpendicular_to is not available in Pymations yet")
|
|
4237
|
+
|
|
4238
|
+
|
|
4239
|
+
class Arrow3D(Line3D):
|
|
4240
|
+
_kind = "arrow3d"
|
|
4241
|
+
_set_color = True
|
|
4242
|
+
|
|
4243
|
+
def __init__(self, start=LEFT, end=RIGHT, thickness=0.02, height=0.3, base_radius=0.08, color=WHITE,
|
|
4244
|
+
resolution=24, **kwargs):
|
|
4245
|
+
self.start, self.end = start, end
|
|
4246
|
+
self._solid({"start": _MP(start), "end": _MP(end), "thickness": _num(thickness), "height": _num(height),
|
|
4247
|
+
"base_radius": _num(base_radius), "resolution": _line_res(resolution)},
|
|
4248
|
+
dict(kwargs, color=color), None)
|
|
4249
|
+
|
|
4250
|
+
|
|
4251
|
+
class Cube(_Solid):
|
|
4252
|
+
_kind = "cube"
|
|
4253
|
+
|
|
4254
|
+
def __init__(self, side_length=2, fill_opacity=0.75, fill_color=BLUE, stroke_width=0, **kwargs):
|
|
4255
|
+
self._solid({"side_length": _num(side_length)}, kwargs, 6,
|
|
4256
|
+
{"fill_color": fill_color, "fill_opacity": fill_opacity, "stroke_width": stroke_width,
|
|
4257
|
+
"stroke_color": WHITE, "checkerboard_colors": False})
|
|
4258
|
+
|
|
4259
|
+
|
|
4260
|
+
class Prism(Cube):
|
|
4261
|
+
_kind = "prism"
|
|
4262
|
+
|
|
4263
|
+
def __init__(self, dimensions=(3, 2, 1), fill_opacity=0.75, fill_color=BLUE, stroke_width=0, **kwargs):
|
|
4264
|
+
self._solid({"dimensions": [_num(x) for x in dimensions]}, kwargs, 6,
|
|
4265
|
+
{"fill_color": fill_color, "fill_opacity": fill_opacity, "stroke_width": stroke_width,
|
|
4266
|
+
"stroke_color": WHITE, "checkerboard_colors": False})
|
|
4267
|
+
|
|
4268
|
+
|
|
4269
|
+
class Polyhedron(VGroup):
|
|
4270
|
+
"""Faces from vertex_coords and faces_list; parts [0] the faces, [1] the vertices and edges."""
|
|
4271
|
+
_kind = "polyhedron"
|
|
4272
|
+
|
|
4273
|
+
def __init__(self, vertex_coords=None, faces_list=None, faces_config=None, graph_config=None, **kwargs):
|
|
4274
|
+
self._kids = None
|
|
4275
|
+
geom = {"faces_config": _style_dict(faces_config), "graph_config": _style_dict(graph_config)}
|
|
4276
|
+
if self._kind == "polyhedron":
|
|
4277
|
+
geom.update(vertex_coords=[_P(v) for v in vertex_coords], faces_list=[[int(i) for i in f]
|
|
4278
|
+
for f in faces_list])
|
|
4279
|
+
geom.update(self._params())
|
|
4280
|
+
self._make(self._kind, geom, kwargs, {}, n=2)
|
|
4281
|
+
|
|
4282
|
+
def _params(self):
|
|
4283
|
+
return {}
|
|
4284
|
+
|
|
4285
|
+
@property
|
|
4286
|
+
def faces(self):
|
|
4287
|
+
return self[0]
|
|
4288
|
+
|
|
4289
|
+
@property
|
|
4290
|
+
def graph(self):
|
|
4291
|
+
return self[1]
|
|
4292
|
+
|
|
4293
|
+
|
|
4294
|
+
class _Platonic(Polyhedron):
|
|
4295
|
+
def __init__(self, edge_length=1, faces_config=None, graph_config=None, **kwargs):
|
|
4296
|
+
self.edge_length = edge_length
|
|
4297
|
+
Polyhedron.__init__(self, faces_config=faces_config, graph_config=graph_config, **kwargs)
|
|
4298
|
+
|
|
4299
|
+
def _params(self):
|
|
4300
|
+
return {"edge_length": _num(self.edge_length)}
|
|
4301
|
+
|
|
4302
|
+
|
|
4303
|
+
class Tetrahedron(_Platonic):
|
|
4304
|
+
_kind = "tetrahedron"
|
|
4305
|
+
|
|
4306
|
+
|
|
4307
|
+
class Octahedron(_Platonic):
|
|
4308
|
+
_kind = "octahedron"
|
|
4309
|
+
|
|
4310
|
+
|
|
4311
|
+
class Icosahedron(_Platonic):
|
|
4312
|
+
_kind = "icosahedron"
|
|
4313
|
+
|
|
4314
|
+
|
|
4315
|
+
class Dodecahedron(_Platonic):
|
|
4316
|
+
_kind = "dodecahedron"
|
|
4317
|
+
|
|
4318
|
+
|
|
4319
|
+
# ---------------------------------------------------------------- Boolean shapes
|
|
4320
|
+
|
|
4321
|
+
class Union(VMobject):
|
|
4322
|
+
"""The outline around all the shapes together."""
|
|
4323
|
+
_kind = "union"
|
|
4324
|
+
|
|
4325
|
+
def __init__(self, *vmobjects, **kwargs):
|
|
4326
|
+
mobs = _flat(vmobjects)
|
|
4327
|
+
if len(mobs) < 2:
|
|
4328
|
+
raise ValueError("%s needs at least 2 shapes" % type(self).__name__)
|
|
4329
|
+
self._make(self._kind, {"mobjects": [m._embed() for m in mobs]}, kwargs, n=0)
|
|
4330
|
+
|
|
4331
|
+
|
|
4332
|
+
class Intersection(Union):
|
|
4333
|
+
"""Where all the shapes overlap."""
|
|
4334
|
+
_kind = "intersection"
|
|
4335
|
+
|
|
4336
|
+
|
|
4337
|
+
class Difference(VMobject):
|
|
4338
|
+
"""subject with clip cut away."""
|
|
4339
|
+
_kind = "difference"
|
|
4340
|
+
|
|
4341
|
+
def __init__(self, subject, clip, **kwargs):
|
|
4342
|
+
self._make(self._kind, {"subject": subject._embed(), "clip": clip._embed()}, kwargs, n=0)
|
|
4343
|
+
|
|
4344
|
+
|
|
4345
|
+
class Exclusion(Difference):
|
|
4346
|
+
"""Where exactly one of the two shapes is."""
|
|
4347
|
+
_kind = "exclusion"
|
|
4348
|
+
|
|
4349
|
+
|
|
4350
|
+
# ---------------------------------------------------------------- the 3D camera
|
|
4351
|
+
|
|
4352
|
+
_CAMERA_DEFAULTS = {"phi": 0.0, "theta": -90 * DEGREES, "gamma": 0.0, "zoom": 1.0, "focal_distance": 20.0}
|
|
4353
|
+
|
|
4354
|
+
|
|
4355
|
+
class _CameraTracker(ValueTracker):
|
|
4356
|
+
"""A camera angle as a tracker: animate it, read it in updaters."""
|
|
4357
|
+
|
|
4358
|
+
def __init__(self, param):
|
|
4359
|
+
value = _CAMERA_DEFAULTS[param]
|
|
4360
|
+
self._value = self._initial = value
|
|
4361
|
+
self._make("value_tracker", {"value": value, "camera": param}, {}, {}, n=0)
|
|
4362
|
+
|
|
4363
|
+
|
|
4364
|
+
def _camera_tracker(param):
|
|
4365
|
+
st = _st()
|
|
4366
|
+
trackers = st.__dict__.setdefault("camera_trackers", {})
|
|
4367
|
+
t = trackers.get(param)
|
|
4368
|
+
if t is None:
|
|
4369
|
+
# A real object even when first touched inside an updater being traced.
|
|
4370
|
+
prev, st.trace = st.trace, None
|
|
4371
|
+
try:
|
|
4372
|
+
t = trackers[param] = _CameraTracker(param)
|
|
4373
|
+
finally:
|
|
4374
|
+
st.trace = prev
|
|
4375
|
+
return t
|
|
4376
|
+
|
|
4377
|
+
|
|
4378
|
+
def _camera_args(phi=None, theta=None, gamma=None, zoom=None, focal_distance=None, frame_center=None):
|
|
4379
|
+
args = {}
|
|
4380
|
+
for k, v in (("phi", phi), ("theta", theta), ("gamma", gamma), ("zoom", zoom),
|
|
4381
|
+
("focal_distance", focal_distance)):
|
|
4382
|
+
if v is not None:
|
|
4383
|
+
args[k] = _num(v)
|
|
4384
|
+
t = _st().__dict__.get("camera_trackers", {}).get(k)
|
|
4385
|
+
if t is not None and _real_number(v):
|
|
4386
|
+
t._value = float(v)
|
|
4387
|
+
if frame_center is not None:
|
|
4388
|
+
args["frame_center"] = _MP(frame_center)
|
|
4389
|
+
return args
|
|
4390
|
+
|
|
4391
|
+
|
|
4392
|
+
def _camera_op(name, args):
|
|
4393
|
+
_call("op", 0, name, args)
|
|
4394
|
+
|
|
4395
|
+
|
|
4396
|
+
class _MoveCamera(Animation):
|
|
4397
|
+
kind = "move_camera"
|
|
4398
|
+
|
|
4399
|
+
def __init__(self, cam_args, **kwargs):
|
|
4400
|
+
self._cam = cam_args
|
|
4401
|
+
Animation.__init__(self, None, **kwargs)
|
|
4402
|
+
|
|
4403
|
+
def _anim(self):
|
|
4404
|
+
return _timing({"kind": self.kind, "target": 0, "args": self._cam}, self._kw)
|
|
4405
|
+
|
|
4406
|
+
def _added(self):
|
|
4407
|
+
return []
|
|
4408
|
+
|
|
4409
|
+
|
|
4410
|
+
# ---------------------------------------------------------------- complex maths, as formulas
|
|
4411
|
+
#
|
|
4412
|
+
# ApplyComplexFunction(lambda z: z ** 2, plane) traces the function on a
|
|
4413
|
+
# symbolic z = x + iy: complex arithmetic is expanded here into a real and an
|
|
4414
|
+
# imaginary formula, which the engine maps every point with.
|
|
4415
|
+
|
|
4416
|
+
def _re_im(v):
|
|
4417
|
+
if isinstance(v, _CExpr):
|
|
4418
|
+
return v.re, v.im
|
|
4419
|
+
if isinstance(v, complex):
|
|
4420
|
+
return v.real, v.imag
|
|
4421
|
+
if isinstance(v, (int, float, Expr)) and not isinstance(v, bool):
|
|
4422
|
+
return v, 0.0
|
|
4423
|
+
tolist = getattr(v, "tolist", None)
|
|
4424
|
+
if tolist is not None:
|
|
4425
|
+
return _re_im(tolist())
|
|
4426
|
+
raise TypeError("expected a complex number, got %r" % (v,))
|
|
4427
|
+
|
|
4428
|
+
|
|
4429
|
+
def _is_zero(v):
|
|
4430
|
+
return isinstance(v, (int, float)) and not isinstance(v, bool) and v == 0
|
|
4431
|
+
|
|
4432
|
+
|
|
4433
|
+
def _m(a, b):
|
|
4434
|
+
if _is_zero(a) or _is_zero(b):
|
|
4435
|
+
return 0.0
|
|
4436
|
+
return a * b
|
|
4437
|
+
|
|
4438
|
+
|
|
4439
|
+
def _a(a, b):
|
|
4440
|
+
if _is_zero(a):
|
|
4441
|
+
return b
|
|
4442
|
+
if _is_zero(b):
|
|
4443
|
+
return a
|
|
4444
|
+
return a + b
|
|
4445
|
+
|
|
4446
|
+
|
|
4447
|
+
class _CExpr:
|
|
4448
|
+
"""A complex value worked out in the browser: re + i·im, each a formula or a number."""
|
|
4449
|
+
__slots__ = ("re", "im")
|
|
4450
|
+
|
|
4451
|
+
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
4452
|
+
if method != "__call__" or len(inputs) != 1:
|
|
4453
|
+
return NotImplemented
|
|
4454
|
+
names = {"exp": "exp", "log": "log", "sqrt": "sqrt", "sin": "sin", "cos": "cos", "tan": "tan",
|
|
4455
|
+
"sinh": "sinh", "cosh": "cosh", "absolute": "abs", "conjugate": "conj", "real": "real",
|
|
4456
|
+
"imag": "imag"}
|
|
4457
|
+
name = names.get(ufunc.__name__)
|
|
4458
|
+
return NotImplemented if name is None else _cfn(name, self)
|
|
4459
|
+
|
|
4460
|
+
def __init__(self, re, im):
|
|
4461
|
+
self.re, self.im = re, im
|
|
4462
|
+
|
|
4463
|
+
@property
|
|
4464
|
+
def real(self):
|
|
4465
|
+
return self.re
|
|
4466
|
+
|
|
4467
|
+
@property
|
|
4468
|
+
def imag(self):
|
|
4469
|
+
return self.im
|
|
4470
|
+
|
|
4471
|
+
def conjugate(self):
|
|
4472
|
+
return _CExpr(self.re, -self.im if not _is_zero(self.im) else 0.0)
|
|
4473
|
+
|
|
4474
|
+
def __add__(self, o):
|
|
4475
|
+
a, b = _re_im(o)
|
|
4476
|
+
return _CExpr(_a(self.re, a), _a(self.im, b))
|
|
4477
|
+
|
|
4478
|
+
__radd__ = __add__
|
|
4479
|
+
|
|
4480
|
+
def __sub__(self, o):
|
|
4481
|
+
a, b = _re_im(o)
|
|
4482
|
+
return _CExpr(_a(self.re, -a), _a(self.im, -b))
|
|
4483
|
+
|
|
4484
|
+
def __rsub__(self, o):
|
|
4485
|
+
return _CExpr(*_re_im(o)) - self
|
|
4486
|
+
|
|
4487
|
+
def __neg__(self):
|
|
4488
|
+
return _CExpr(-self.re, -self.im)
|
|
4489
|
+
|
|
4490
|
+
def __mul__(self, o):
|
|
4491
|
+
a, b = _re_im(o)
|
|
4492
|
+
return _CExpr(_a(_m(self.re, a), -_m(self.im, b) if not _is_zero(_m(self.im, b)) else 0.0),
|
|
4493
|
+
_a(_m(self.re, b), _m(self.im, a)))
|
|
4494
|
+
|
|
4495
|
+
__rmul__ = __mul__
|
|
4496
|
+
|
|
4497
|
+
def __truediv__(self, o):
|
|
4498
|
+
a, b = _re_im(o)
|
|
4499
|
+
if not isinstance(a, Expr) and not isinstance(b, Expr):
|
|
4500
|
+
d = a * a + b * b
|
|
4501
|
+
return self * _CExpr(a / d, -b / d)
|
|
4502
|
+
d = _a(_m(a, a), _m(b, b))
|
|
4503
|
+
return self * _CExpr(a / d, -b / d)
|
|
4504
|
+
|
|
4505
|
+
def __rtruediv__(self, o):
|
|
4506
|
+
return _CExpr(*_re_im(o)) / self
|
|
4507
|
+
|
|
4508
|
+
def __pow__(self, n):
|
|
4509
|
+
if isinstance(n, int) and not isinstance(n, bool):
|
|
4510
|
+
if n == 0:
|
|
4511
|
+
return _CExpr(1.0, 0.0)
|
|
4512
|
+
out = self
|
|
4513
|
+
for _ in range(abs(n) - 1):
|
|
4514
|
+
out = out * self
|
|
4515
|
+
return 1 / out if n < 0 else out
|
|
4516
|
+
# Any other power, through polar form.
|
|
4517
|
+
r, th = abs(self), _fn("atan2", self.im, self.re)
|
|
4518
|
+
a, b = _re_im(n)
|
|
4519
|
+
if not _is_zero(b):
|
|
4520
|
+
return _cexp(_CExpr(_fn("log", r), th) * _CExpr(a, b))
|
|
4521
|
+
rn = r ** a
|
|
4522
|
+
return _CExpr(rn * _fn("cos", th * a), rn * _fn("sin", th * a))
|
|
4523
|
+
|
|
4524
|
+
def __abs__(self):
|
|
4525
|
+
return _fn("sqrt", _a(_m(self.re, self.re), _m(self.im, self.im)))
|
|
4526
|
+
|
|
4527
|
+
def _no(self, *a):
|
|
4528
|
+
_lazy_error("this complex value")
|
|
4529
|
+
|
|
4530
|
+
__bool__ = __float__ = __complex__ = __int__ = _no
|
|
4531
|
+
|
|
4532
|
+
|
|
4533
|
+
def _cexp(z):
|
|
4534
|
+
e = _fn("exp", z.re) if isinstance(z.re, Expr) else math.exp(z.re)
|
|
4535
|
+
return _CExpr(_m(e, _fn("cos", z.im) if isinstance(z.im, Expr) else math.cos(z.im)),
|
|
4536
|
+
_m(e, _fn("sin", z.im) if isinstance(z.im, Expr) else math.sin(z.im)))
|
|
4537
|
+
|
|
4538
|
+
|
|
4539
|
+
def _cfn(name, z):
|
|
4540
|
+
"""np.exp(z), np.sin(z) … on a symbolic complex z."""
|
|
4541
|
+
if name == "exp":
|
|
4542
|
+
return _cexp(z)
|
|
4543
|
+
if name == "log":
|
|
4544
|
+
return _CExpr(_fn("log", abs(z)), _fn("atan2", z.im, z.re))
|
|
4545
|
+
if name == "sqrt":
|
|
4546
|
+
return z ** 0.5
|
|
4547
|
+
if name in ("sin", "cos", "sinh", "cosh"):
|
|
4548
|
+
iz = _CExpr(-z.im, z.re) if name in ("sin", "cos") else z
|
|
4549
|
+
e1, e2 = _cexp(iz), _cexp(-iz)
|
|
4550
|
+
if name == "sin": # (e^{iz} - e^{-iz}) / 2i
|
|
4551
|
+
d = e1 - e2
|
|
4552
|
+
return _CExpr(d.im / 2, -d.re / 2)
|
|
4553
|
+
if name == "cos":
|
|
4554
|
+
return (e1 + e2) * 0.5
|
|
4555
|
+
return (e1 - e2) * 0.5 if name == "sinh" else (e1 + e2) * 0.5
|
|
4556
|
+
if name == "tan":
|
|
4557
|
+
return _cfn("sin", z) / _cfn("cos", z)
|
|
4558
|
+
if name == "abs":
|
|
4559
|
+
return abs(z)
|
|
4560
|
+
if name in ("conj", "conjugate"):
|
|
4561
|
+
return z.conjugate()
|
|
4562
|
+
if name == "real":
|
|
4563
|
+
return z.re
|
|
4564
|
+
if name == "imag":
|
|
4565
|
+
return z.im
|
|
4566
|
+
raise TypeError("np.%s does not work on a complex value in Pymations" % name)
|
|
4567
|
+
|
|
4568
|
+
|
|
4569
|
+
def _complex_formula(fn, args, what):
|
|
4570
|
+
tr, result, err = _run_trace(fn, args)
|
|
4571
|
+
if err is None:
|
|
4572
|
+
try:
|
|
4573
|
+
re, im = _re_im(result)
|
|
4574
|
+
return ["v", [_tree(re), _tree(im), 0.0]]
|
|
4575
|
+
except Exception as e: # noqa: BLE001
|
|
4576
|
+
err = e
|
|
4577
|
+
raise TypeError("%s needs a function the browser can work out with + - * / ** and np.exp, np.sin, np.log … "
|
|
4578
|
+
"(not cmath.* or an if on z): %s" % (what, err))
|
|
4579
|
+
|
|
4580
|
+
|
|
4581
|
+
def _z():
|
|
4582
|
+
return _CExpr(Expr(["var", "x"], "s"), Expr(["var", "y"], "s"))
|
|
4583
|
+
|
|
4584
|
+
|
|
4585
|
+
class ApplyComplexFunction(Animation):
|
|
4586
|
+
"""Moves every point z (x + iy) to function(z): ApplyComplexFunction(lambda z: z ** 2, plane)."""
|
|
4587
|
+
kind = "apply_complex_function"
|
|
4588
|
+
|
|
4589
|
+
def __init__(self, function, mobject, **kwargs):
|
|
4590
|
+
self._formula = _complex_formula(function, (_z(),), "ApplyComplexFunction")
|
|
4591
|
+
Animation.__init__(self, mobject, **kwargs)
|
|
4592
|
+
|
|
4593
|
+
def _make_args(self, **kwargs):
|
|
4594
|
+
return {"function": self._formula}
|
|
4595
|
+
|
|
4596
|
+
|
|
4597
|
+
class ComplexHomotopy(Animation):
|
|
4598
|
+
"""function(z, t) for t from 0 to 1 moves every point z: a complex Homotopy."""
|
|
4599
|
+
kind = "complex_homotopy"
|
|
4600
|
+
|
|
4601
|
+
def __init__(self, complex_homotopy, mobject, run_time=3, **kwargs):
|
|
4602
|
+
self._formula = _complex_formula(complex_homotopy, (_z(), _var("t")), "ComplexHomotopy")
|
|
4603
|
+
Animation.__init__(self, mobject, run_time=run_time, **kwargs)
|
|
4604
|
+
|
|
4605
|
+
def _make_args(self, **kwargs):
|
|
4606
|
+
return {"function": self._formula}
|
|
4607
|
+
|
|
4608
|
+
|
|
4609
|
+
class PhaseFlow(Animation):
|
|
4610
|
+
def __init__(self, *a, **k):
|
|
4611
|
+
raise NotImplementedError("PhaseFlow is not available in Pymations yet; Homotopy or "
|
|
4612
|
+
"ApplyPointwiseFunction can often show the same motion")
|
|
4613
|
+
|
|
4614
|
+
|
|
3917
4615
|
# ---------------------------------------------------------------- scenes
|
|
3918
4616
|
|
|
3919
4617
|
class Scene:
|
|
@@ -4010,14 +4708,18 @@ class Scene:
|
|
|
4010
4708
|
d = a._anim()
|
|
4011
4709
|
d.update(over)
|
|
4012
4710
|
encoded.append(d)
|
|
4013
|
-
_call("play", encoded,
|
|
4711
|
+
_call("play", encoded, _outer_opts())
|
|
4014
4712
|
for a in anims:
|
|
4015
4713
|
self._keep(a._added())
|
|
4016
4714
|
for a in anims:
|
|
4017
4715
|
self._drop(a._removed())
|
|
4018
4716
|
|
|
4019
4717
|
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None, frozen_frame=None):
|
|
4020
|
-
|
|
4718
|
+
opts = _outer_opts()
|
|
4719
|
+
if opts:
|
|
4720
|
+
_call("wait", _num(duration), opts)
|
|
4721
|
+
else:
|
|
4722
|
+
_call("wait", _num(duration))
|
|
4021
4723
|
|
|
4022
4724
|
pause = wait
|
|
4023
4725
|
|
|
@@ -4054,32 +4756,64 @@ class ZoomedScene(MovingCameraScene):
|
|
|
4054
4756
|
|
|
4055
4757
|
|
|
4056
4758
|
class ThreeDScene(Scene):
|
|
4057
|
-
|
|
4058
|
-
_tell("3d", "Note: 3D is not shown yet, so this scene plays in 2D.")
|
|
4759
|
+
"""A scene seen through a 3D camera: shapes projected, depth-sorted and shaded."""
|
|
4059
4760
|
|
|
4060
|
-
def
|
|
4061
|
-
|
|
4761
|
+
def __init__(self, ambient_camera_rotation=None, default_angled_camera_orientation_kwargs=None, **kwargs):
|
|
4762
|
+
Scene.__init__(self, **kwargs)
|
|
4763
|
+
self.default_angled_camera_orientation_kwargs = default_angled_camera_orientation_kwargs or {
|
|
4764
|
+
"phi": 70 * DEGREES, "theta": -135 * DEGREES}
|
|
4062
4765
|
|
|
4063
|
-
def
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
self.play(*added_anims, run_time=run_time)
|
|
4067
|
-
else:
|
|
4068
|
-
self.wait(run_time or 1.0)
|
|
4766
|
+
def set_camera_orientation(self, phi=None, theta=None, gamma=None, zoom=None, focal_distance=None,
|
|
4767
|
+
frame_center=None, **kwargs):
|
|
4768
|
+
_camera_op("set_camera_orientation", _camera_args(phi, theta, gamma, zoom, focal_distance, frame_center))
|
|
4069
4769
|
|
|
4070
|
-
def
|
|
4071
|
-
|
|
4770
|
+
def move_camera(self, phi=None, theta=None, gamma=None, zoom=None, focal_distance=None, frame_center=None,
|
|
4771
|
+
added_anims=(), **kwargs):
|
|
4772
|
+
timing = {k: kwargs.pop(k) for k in ("run_time", "rate_func", "lag_ratio") if k in kwargs}
|
|
4773
|
+
cam = _MoveCamera(_camera_args(phi, theta, gamma, zoom, focal_distance, frame_center))
|
|
4774
|
+
self.play(cam, *list(added_anims), **timing)
|
|
4072
4775
|
|
|
4073
|
-
def
|
|
4074
|
-
|
|
4776
|
+
def begin_ambient_camera_rotation(self, rate=0.02, about="theta"):
|
|
4777
|
+
_call("begin_ambient_camera_rotation", _num(rate), str(about).lower())
|
|
4778
|
+
|
|
4779
|
+
def stop_ambient_camera_rotation(self, about="theta"):
|
|
4780
|
+
_call("stop_ambient_camera_rotation", str(about).lower())
|
|
4781
|
+
|
|
4782
|
+
def begin_3dillusion_camera_rotation(self, rate=1, origin_phi=None, origin_theta=None):
|
|
4783
|
+
_call("begin_3dillusion_camera_rotation", _num(rate), None if origin_phi is None else _num(origin_phi),
|
|
4784
|
+
None if origin_theta is None else _num(origin_theta))
|
|
4075
4785
|
|
|
4076
|
-
def
|
|
4077
|
-
|
|
4786
|
+
def stop_3dillusion_camera_rotation(self):
|
|
4787
|
+
_call("stop_3dillusion_camera_rotation")
|
|
4788
|
+
|
|
4789
|
+
def set_to_default_angled_camera_orientation(self, **kwargs):
|
|
4790
|
+
self.set_camera_orientation(**dict(self.default_angled_camera_orientation_kwargs, **kwargs))
|
|
4078
4791
|
|
|
4079
4792
|
def add_fixed_in_frame_mobjects(self, *mobjects):
|
|
4080
|
-
|
|
4793
|
+
"""Kept flat on the screen (titles, labels): not turned by the camera."""
|
|
4794
|
+
mobs = _flat(mobjects)
|
|
4795
|
+
for m in mobs:
|
|
4796
|
+
m._op("fix_in_frame", {})
|
|
4797
|
+
return self.add(*mobs)
|
|
4798
|
+
|
|
4799
|
+
def remove_fixed_in_frame_mobjects(self, *mobjects):
|
|
4800
|
+
for m in _flat(mobjects):
|
|
4801
|
+
m._op("unfix_from_frame", {})
|
|
4802
|
+
|
|
4803
|
+
def add_fixed_orientation_mobjects(self, *mobjects, use_static_center_func=False, center_func=None):
|
|
4804
|
+
"""Placed in 3D but always facing the camera (labels on axes)."""
|
|
4805
|
+
mobs = _flat(mobjects)
|
|
4806
|
+
for m in mobs:
|
|
4807
|
+
m._op("fix_orientation", {})
|
|
4808
|
+
return self.add(*mobs)
|
|
4809
|
+
|
|
4810
|
+
def remove_fixed_orientation_mobjects(self, *mobjects):
|
|
4811
|
+
for m in _flat(mobjects):
|
|
4812
|
+
m._op("unfix_orientation", {})
|
|
4081
4813
|
|
|
4082
|
-
|
|
4814
|
+
|
|
4815
|
+
class SpecialThreeDScene(ThreeDScene):
|
|
4816
|
+
pass
|
|
4083
4817
|
|
|
4084
4818
|
|
|
4085
4819
|
def _split(group, gone):
|
|
@@ -4097,7 +4831,36 @@ def _split(group, gone):
|
|
|
4097
4831
|
def _scene_section(scene, first):
|
|
4098
4832
|
tr = scene.transition if scene.transition is not None else config.transition
|
|
4099
4833
|
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)
|
|
4834
|
+
_section(type(scene).__name__, None if first else tr, tt, lines=_class_lines(type(scene).__name__))
|
|
4835
|
+
|
|
4836
|
+
|
|
4837
|
+
def _class_lines(name):
|
|
4838
|
+
"""[first, last] source lines of a top-level class in the user's file, so the
|
|
4839
|
+
editor's cursor in it finds that Scene's section of the timeline."""
|
|
4840
|
+
st = _st()
|
|
4841
|
+
spans = st.__dict__.get("class_spans")
|
|
4842
|
+
if spans is None:
|
|
4843
|
+
spans = {}
|
|
4844
|
+
try:
|
|
4845
|
+
import ast
|
|
4846
|
+
for node in ast.parse(getattr(_rt.S, "source", "") or "").body:
|
|
4847
|
+
if isinstance(node, ast.ClassDef):
|
|
4848
|
+
first = min([node.lineno] + [d.lineno for d in node.decorator_list])
|
|
4849
|
+
spans[node.name] = [first, getattr(node, "end_lineno", None) or node.lineno]
|
|
4850
|
+
except SyntaxError:
|
|
4851
|
+
pass
|
|
4852
|
+
st.__dict__["class_spans"] = spans
|
|
4853
|
+
return spans.get(name)
|
|
4854
|
+
|
|
4855
|
+
|
|
4856
|
+
def _scene_camera(scene):
|
|
4857
|
+
"""After a scene's clear(): a ThreeDScene switches the engine to the 3D camera,
|
|
4858
|
+
a plain Scene after one switches it back."""
|
|
4859
|
+
st = _st()
|
|
4860
|
+
three_d = isinstance(scene, ThreeDScene)
|
|
4861
|
+
if three_d or st.__dict__.get("three_d"):
|
|
4862
|
+
_call("config", {"three_d": three_d})
|
|
4863
|
+
st.__dict__["three_d"] = three_d
|
|
4101
4864
|
|
|
4102
4865
|
|
|
4103
4866
|
def _run_scene_sync(scene):
|
|
@@ -4105,6 +4868,7 @@ def _run_scene_sync(scene):
|
|
|
4105
4868
|
_scene_section(scene, not st.scenes_run)
|
|
4106
4869
|
st.scenes_run += 1
|
|
4107
4870
|
scene.clear()
|
|
4871
|
+
_scene_camera(scene)
|
|
4108
4872
|
for step in (scene.setup, scene.construct, scene.tear_down):
|
|
4109
4873
|
r = step()
|
|
4110
4874
|
if hasattr(r, "send"):
|
|
@@ -4122,6 +4886,7 @@ async def _run_scenes(classes):
|
|
|
4122
4886
|
_scene_section(scene, not st.scenes_run)
|
|
4123
4887
|
st.scenes_run += 1
|
|
4124
4888
|
scene.clear()
|
|
4889
|
+
_scene_camera(scene)
|
|
4125
4890
|
for step in (scene.setup, scene.construct, scene.tear_down):
|
|
4126
4891
|
r = step()
|
|
4127
4892
|
if hasattr(r, "send"):
|
|
@@ -4215,6 +4980,8 @@ def section(name, sound=None, transition=None, transition_time=None):
|
|
|
4215
4980
|
# the stand-in directly, real numpy through Expr.__array_ufunc__.
|
|
4216
4981
|
def _np_fn(name, py):
|
|
4217
4982
|
def f(*args):
|
|
4983
|
+
if args and isinstance(args[0], _CExpr):
|
|
4984
|
+
return _cfn(name, args[0])
|
|
4218
4985
|
if any(_is_sym(a) for a in args):
|
|
4219
4986
|
return _fn(name, *[_tree_arg(a) for a in args])
|
|
4220
4987
|
if any(isinstance(a, (list, tuple)) for a in args):
|