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/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 15).
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 = "15"
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, n=4 if isinstance(self, NumberPlane) else 2)
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
- 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)
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 entry: a string the engine sets in the element kind, or an existing mobject."""
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
- return v._embed()
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._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,
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
- _call("section", str(name), {"transition": transition, "transition_time": _num(tt)})
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
- _call("wait", _num(duration))
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
- def _flat_note(self):
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 set_camera_orientation(self, *a, **k):
4061
- self._flat_note()
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 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)
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 begin_ambient_camera_rotation(self, *a, **k):
4071
- self._flat_note()
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 stop_ambient_camera_rotation(self, *a, **k):
4074
- pass
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 set_to_default_angled_camera_orientation(self, *a, **k):
4077
- self._flat_note()
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
- return self.add(*mobjects)
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
- add_fixed_orientation_mobjects = add_fixed_in_frame_mobjects
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):