Merge pull request #2094 from 3b1b/video-work

Video work
This commit is contained in:
Grant Sanderson 2024-01-17 13:08:43 -08:00 committed by GitHub
commit 2c110790d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 414 additions and 172 deletions

1
.gitignore vendored
View file

@ -91,6 +91,7 @@ ipython_config.py
# pyenv
.python-version
pyrightconfig.json
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

View file

@ -74,6 +74,7 @@ class Animation(object):
# It is, however, okay and desirable to call
# the internal updaters of self.starting_mobject,
# or any others among self.get_all_mobjects()
self.mobject_was_updating = not self.mobject.updating_suspended
self.mobject.suspend_updating()
self.families = list(self.get_all_families_zipped())
self.interpolate(0)
@ -81,7 +82,7 @@ class Animation(object):
def finish(self) -> None:
self.interpolate(self.final_alpha_value)
self.mobject.set_animating_status(False)
if self.suspend_mobject_updating:
if self.suspend_mobject_updating and self.mobject_was_updating:
self.mobject.resume_updating()
def clean_up_from_scene(self, scene: Scene) -> None:

View file

@ -167,7 +167,6 @@ class LaggedStartMap(LaggedStart):
self,
anim_func: Callable[[Mobject], Animation],
group: Mobject,
arg_creator: Callable[[Mobject], tuple] | None = None,
run_time: float = 2.0,
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
**kwargs

View file

@ -12,6 +12,7 @@ from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.rate_functions import linear
from manimlib.utils.rate_functions import double_smooth
from manimlib.utils.rate_functions import smooth
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING
@ -138,6 +139,8 @@ class DrawBorderThenFill(Animation):
submob.pointwise_become_partial(outline, 0, subalpha)
else:
submob.interpolate(outline, start, subalpha)
submob.note_changed_stroke()
submob.note_changed_fill()
class Write(DrawBorderThenFill):
@ -189,6 +192,7 @@ class ShowIncreasingSubsets(Animation):
def interpolate_mobject(self, alpha: float) -> None:
n_submobs = len(self.all_submobs)
alpha = self.rate_func(alpha)
index = int(self.int_func(alpha * n_submobs))
self.update_submobject_list(index)
@ -206,7 +210,7 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
super().__init__(group, int_func=int_func, **kwargs)
def update_submobject_list(self, index: int) -> None:
# N = len(self.all_submobs)
index = int(clip(index, 0, len(self.all_submobs) - 1))
if index == 0:
self.mobject.set_submobjects([])
else:

View file

@ -258,6 +258,7 @@ class FlashAround(VShowPassingFlash):
self,
mobject: Mobject,
time_width: float = 1.0,
taper_width: float = 0.0,
stroke_width: float = 4.0,
color: ManimColor = YELLOW,
buff: float = SMALL_BUFF,
@ -270,7 +271,7 @@ class FlashAround(VShowPassingFlash):
path.insert_n_curves(n_inserted_curves)
path.set_points(path.get_points_without_null_curves())
path.set_stroke(color, stroke_width)
super().__init__(path, time_width=time_width, **kwargs)
super().__init__(path, time_width=time_width, taper_width=taper_width, **kwargs)
def get_path(self, mobject: Mobject, buff: float) -> SurroundingRectangle:
return SurroundingRectangle(mobject, buff=buff)
@ -278,7 +279,7 @@ class FlashAround(VShowPassingFlash):
class FlashUnder(FlashAround):
def get_path(self, mobject: Mobject, buff: float) -> Underline:
return Underline(mobject, buff=buff)
return Underline(mobject, buff=buff, stretch_factor=1.0)
class ShowCreationThenDestruction(ShowPassingFlash):

View file

@ -11,6 +11,7 @@ if TYPE_CHECKING:
import numpy as np
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import VMobject
class Homotopy(Animation):
@ -105,7 +106,7 @@ class MoveAlongPath(Animation):
def __init__(
self,
mobject: Mobject,
path: Mobject,
path: VMobject,
suspend_mobject_updating: bool = False,
**kwargs
):
@ -113,5 +114,5 @@ class MoveAlongPath(Animation):
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
point = self.path.point_from_proportion(alpha)
point = self.path.quick_point_from_proportion(self.rate_func(alpha))
self.mobject.move_to(point)

View file

@ -22,6 +22,7 @@ from manimlib.mobject.types.dot_cloud import DotCloud
from manimlib.mobject.types.surface import ParametricSurface
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.simple_functions import binary_search
from manimlib.utils.space_ops import angle_of_vector
@ -174,6 +175,7 @@ class CoordinateSystem(ABC):
self,
function: Callable[[float], float],
x_range: Sequence[float] | None = None,
bind: bool = False,
**kwargs
) -> ParametricCurve:
x_range = x_range or self.x_range
@ -194,6 +196,10 @@ class CoordinateSystem(ABC):
)
graph.underlying_function = function
graph.x_range = x_range
if bind:
self.bind_graph_to_func(graph, function)
return graph
def get_parametric_curve(
@ -239,7 +245,7 @@ class CoordinateSystem(ABC):
def bind_graph_to_func(
self,
graph: VMobject,
func: Callable[[Vect3], Vect3],
func: Callable[[VectN], VectN],
jagged: bool = False,
get_discontinuities: Optional[Callable[[], Vect3]] = None
) -> VMobject:
@ -398,9 +404,24 @@ class CoordinateSystem(ABC):
rect.set_fill(negative_color)
return result
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=1):
# TODO
pass
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=0.5):
if not hasattr(graph, "x_range"):
raise Exception("Argument `graph` must have attribute `x_range`")
alpha_bounds = [
inverse_interpolate(*graph.x_range, x)
for x in x_range
]
sub_graph = graph.copy()
sub_graph.pointwise_become_partial(graph, *alpha_bounds)
sub_graph.add_line_to(self.c2p(x_range[1], 0))
sub_graph.add_line_to(self.c2p(x_range[0], 0))
sub_graph.add_line_to(sub_graph.get_start())
sub_graph.set_stroke(width=0)
sub_graph.set_fill(fill_color, fill_opacity)
return sub_graph
class Axes(VGroup, CoordinateSystem):
@ -421,6 +442,7 @@ class Axes(VGroup, CoordinateSystem):
**kwargs
):
CoordinateSystem.__init__(self, x_range, y_range, **kwargs)
kwargs.pop("num_sampled_graph_points_per_tick", None)
VGroup.__init__(self, **kwargs)
axis_config = dict(**axis_config, unit_size=unit_size)
@ -507,7 +529,7 @@ class ThreeDAxes(Axes):
z_range: RangeSpecifier = (-4.0, 4.0, 1.0),
z_axis_config: dict = dict(),
z_normal: Vect3 = DOWN,
depth: float = 6.0,
depth: float | None = None,
flat_stroke: bool = False,
**kwargs
):
@ -519,7 +541,7 @@ class ThreeDAxes(Axes):
axis_config=merge_dicts_recursively(
self.default_axis_config,
self.default_z_axis_config,
kwargs.get("axes_config", {}),
kwargs.get("axis_config", {}),
z_axis_config
),
length=depth,
@ -549,20 +571,47 @@ class ThreeDAxes(Axes):
axis.add(label)
self.axis_labels = labels
def get_graph(self, func, color=BLUE_E, opacity=0.9, **kwargs):
def get_graph(
self,
func,
color=BLUE_E,
opacity=0.9,
u_range=None,
v_range=None,
**kwargs
) -> ParametricSurface:
xu = self.x_axis.get_unit_size()
yu = self.y_axis.get_unit_size()
zu = self.z_axis.get_unit_size()
x0, y0, z0 = self.get_origin()
u_range = u_range or self.x_range[:2]
v_range = v_range or self.y_range[:2]
return ParametricSurface(
lambda u, v: [xu * u + x0, yu * v + y0, zu * func(u, v) + z0],
u_range=self.x_range[:2],
v_range=self.y_range[:2],
u_range=u_range,
v_range=v_range,
color=color,
opacity=opacity,
**kwargs
)
def get_parametric_surface(
self,
func,
color=BLUE_E,
opacity=0.9,
**kwargs
) -> ParametricSurface:
surface = ParametricSurface(func, color=color, opacity=opacity, **kwargs)
xu = self.x_axis.get_unit_size()
yu = self.y_axis.get_unit_size()
zu = self.z_axis.get_unit_size()
axes = [self.x_axis, self.y_axis, self.z_axis]
for dim, axis in zip(range(3), axes):
surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN)
surface.shift(self.get_origin())
return surface
class NumberPlane(Axes):
default_axis_config: dict = dict(

View file

@ -7,12 +7,15 @@ import numpy as np
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR
from manimlib.constants import GREY_A, RED, WHITE, BLACK
from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
from manimlib.constants import DEGREES, PI, TAU
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import bezier
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.iterables import adjacent_n_tuples
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.simple_functions import clip
@ -26,6 +29,7 @@ from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import normalize
from manimlib.utils.space_ops import rotate_vector
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.utils.space_ops import rotation_about_z
from typing import TYPE_CHECKING
@ -213,28 +217,11 @@ class Arc(TipableVMobject):
):
super().__init__(**kwargs)
self.set_points(Arc.create_quadratic_bezier_points(
angle=angle,
start_angle=start_angle,
n_components=n_components
))
self.set_points(quadratic_bezier_points_for_arc(angle, n_components))
self.rotate(start_angle, about_point=ORIGIN)
self.scale(radius, about_point=ORIGIN)
self.shift(arc_center)
@staticmethod
def create_quadratic_bezier_points(
angle: float,
start_angle: float = 0,
n_components: int = 8
) -> Vect3Array:
n_points = 2 * n_components + 1
angles = np.linspace(start_angle, start_angle + angle, n_points)
points = np.array([np.cos(angles), np.sin(angles), np.zeros(n_points)]).T
# Adjust handles
theta = angle / n_components
points[1::2] /= np.cos(theta / 2)
return points
def get_arc_center(self) -> Vect3:
"""
Looks at the normals to the first two
@ -448,8 +435,8 @@ class Annulus(VMobject):
)
self.radius = outer_radius
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0)
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0)
outer_path = outer_radius * quadratic_bezier_points_for_arc(TAU)
inner_path = inner_radius * quadratic_bezier_points_for_arc(-TAU)
self.add_subpath(outer_path)
self.add_subpath(inner_path)
self.shift(center)
@ -466,6 +453,7 @@ class Line(TipableVMobject):
):
super().__init__(**kwargs)
self.path_arc = path_arc
self.buff = buff
self.set_start_and_end_attrs(start, end)
self.set_points_by_ends(self.start, self.end, buff, path_arc)
@ -476,32 +464,15 @@ class Line(TipableVMobject):
buff: float = 0,
path_arc: float = 0
) -> Self:
vect = end - start
dist = get_norm(vect)
if np.isclose(dist, 0):
self.set_points_as_corners([start, end])
return self
if path_arc:
neg = path_arc < 0
if neg:
path_arc = -path_arc
start, end = end, start
radius = (dist / 2) / math.sin(path_arc / 2)
alpha = (PI - path_arc) / 2
center = start + radius * normalize(rotate_vector(end - start, alpha))
self.clear_points()
self.start_new_path(start)
self.add_arc_to(end, path_arc)
raw_arc_points = Arc.create_quadratic_bezier_points(
angle=path_arc - 2 * buff / radius,
start_angle=angle_of_vector(start - center) + buff / radius,
)
if neg:
raw_arc_points = raw_arc_points[::-1]
self.set_points(center + radius * raw_arc_points)
else:
if buff > 0 and dist > 0:
start = start + vect * (buff / dist)
end = end - vect * (buff / dist)
self.set_points_as_corners([start, end])
# Apply buffer
if buff > 0:
length = self.get_arc_length()
alpha = min(buff / length, 0.5)
self.pointwise_become_partial(self, alpha, 1 - alpha)
return self
def set_path_arc(self, new_value: float) -> Self:
@ -673,15 +644,17 @@ class Arrow(Line):
stroke_width: float = 5,
buff: float = 0.25,
tip_width_ratio: float = 5,
width_to_tip_len: float = 0.0075,
tip_len_to_width: float = 0.0075,
max_tip_length_to_length_ratio: float = 0.3,
max_width_to_length_ratio: float = 8.0,
**kwargs,
):
self.tip_width_ratio = tip_width_ratio
self.width_to_tip_len = width_to_tip_len
self.tip_len_to_width = tip_len_to_width
self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio
self.max_width_to_length_ratio = max_width_to_length_ratio
self.n_tip_points = 3
self.original_stroke_width = stroke_width
super().__init__(
start, end,
stroke_color=stroke_color,
@ -705,27 +678,32 @@ class Arrow(Line):
def insert_tip_anchor(self) -> Self:
prev_end = self.get_end()
arc_len = self.get_arc_length()
tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio
if tip_len >= self.max_tip_length_to_length_ratio * arc_len:
tip_len = self.get_stroke_width() * self.tip_width_ratio * self.tip_len_to_width
if tip_len >= self.max_tip_length_to_length_ratio * arc_len or arc_len == 0:
alpha = self.max_tip_length_to_length_ratio
else:
alpha = tip_len / arc_len
self.pointwise_become_partial(self, 0, 1 - alpha)
# Dumb that this is needed
self.start_new_path(self.point_from_proportion(1 - 1e-5))
if self.path_arc > 0 and self.buff > 0:
self.insert_n_curves(10) # Is this needed?
self.pointwise_become_partial(self, 0.0, 1.0 - alpha)
self.add_line_to(self.get_end())
self.add_line_to(prev_end)
self.n_tip_points = 3
return self
@Mobject.affects_data
def create_tip_with_stroke_width(self) -> Self:
if self.get_num_points() < 3:
return self
tip_width = self.tip_width_ratio * min(
float(self.get_stroke_width()),
stroke_width = min(
self.original_stroke_width,
self.max_width_to_length_ratio * self.get_length(),
)
self.data['stroke_width'][:-3] = self.data['stroke_width'][0]
self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3)
tip_width = self.tip_width_ratio * stroke_width
ntp = self.n_tip_points
self.data['stroke_width'][:-ntp] = self.data['stroke_width'][0]
self.data['stroke_width'][-ntp:, 0] = tip_width * np.linspace(1, 0, ntp)
return self
def reset_tip(self) -> Self:
@ -742,6 +720,7 @@ class Arrow(Line):
*args, **kwargs
) -> Self:
super().set_stroke(color=color, width=width, *args, **kwargs)
self.original_stroke_width = self.get_stroke_width()
if self.has_points():
self.reset_tip()
return self
@ -817,7 +796,7 @@ class FillArrow(Line):
R = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a)
# Find arc points
points1 = Arc.create_quadratic_bezier_points(path_arc)
points1 = quadratic_bezier_points_for_arc(path_arc)
points2 = np.array(points1[::-1])
points1 *= (R + thickness / 2)
points2 *= (R - thickness / 2)
@ -1046,6 +1025,12 @@ class Rectangle(Polygon):
self.set_width(width, stretch=True)
self.set_height(height, stretch=True)
def surround(self, mobject, buff=SMALL_BUFF) -> Self:
target_shape = np.array(mobject.get_shape()) + 2 * buff
self.set_shape(*target_shape)
self.move_to(mobject)
return self
class Square(Rectangle):
def __init__(self, side_length: float = 2.0, **kwargs):

View file

@ -224,7 +224,7 @@ class Mobject(object):
@affects_family_data
def reverse_points(self) -> Self:
for mob in self.get_family():
mob.data = mob.data[::-1]
mob.data[:] = mob.data[::-1]
return self
@affects_family_data
@ -790,13 +790,13 @@ class Mobject(object):
def update(self, dt: float = 0, recurse: bool = True) -> Self:
if not self.has_updaters or self.updating_suspended:
return self
if recurse:
for submob in self.submobjects:
submob.update(dt, recurse)
for updater in self.time_based_updaters:
updater(self, dt)
for updater in self.non_time_updaters:
updater(self)
if recurse:
for submob in self.submobjects:
submob.update(dt, recurse)
return self
def get_time_based_updaters(self) -> list[TimeBasedUpdater]:
@ -1334,7 +1334,7 @@ class Mobject(object):
rgbs = resize_with_interpolation(rgbs, len(data))
data[name][:, :3] = rgbs
if opacity is not None:
if isinstance(opacity, list):
if not isinstance(opacity, (float, int)):
opacity = resize_with_interpolation(np.array(opacity), len(data))
data[name][:, 3] = opacity
return self
@ -1540,6 +1540,9 @@ class Mobject(object):
def get_depth(self) -> float:
return self.length_over_dim(2)
def get_shape(self) -> Tuple[float]:
return tuple(self.length_over_dim(dim) for dim in range(3))
def get_coord(self, dim: int, direction: Vect3 = ORIGIN) -> float:
"""
Meant to generalize get_x, get_y, get_z
@ -1598,6 +1601,12 @@ class Mobject(object):
def match_color(self, mobject: Mobject) -> Self:
return self.set_color(mobject.get_color())
def match_style(self, mobject: Mobject) -> Self:
self.set_color(mobject.get_color())
self.set_opacity(mobject.get_opacity())
self.set_shading(*mobject.get_shading())
return self
def match_dim_size(self, mobject: Mobject, dim: int, **kwargs) -> Self:
return self.rescale_to_fit(
mobject.length_over_dim(dim), dim,

View file

@ -94,7 +94,8 @@ class NumberLine(Line):
x_max = self.x_max
else:
x_max = self.x_max + self.x_step
return np.arange(self.x_min, x_max, self.x_step)
result = np.arange(self.x_min, x_max, self.x_step)
return result[result <= self.x_max]
def add_ticks(self) -> None:
ticks = VGroup()

View file

@ -98,6 +98,8 @@ class DecimalNumber(VMobject):
formatter = self.get_complex_formatter()
else:
formatter = self.get_formatter()
if self.num_decimal_places == 0 and isinstance(number, float):
number = int(number)
num_string = formatter.format(number)
rounded_num = np.round(number, self.num_decimal_places)
@ -149,7 +151,7 @@ class DecimalNumber(VMobject):
":",
"+" if config["include_sign"] else "",
"," if config["group_with_commas"] else "",
f".{ndp}f",
f".{ndp}f" if ndp > 0 else "d",
"}",
])
@ -169,6 +171,7 @@ class DecimalNumber(VMobject):
self.set_submobjects_from_number(number)
self.move_to(move_to_point, self.edge_to_fix)
self.set_style(**style)
self.fix_in_frame(self._is_fixed_in_frame)
return self
def _handle_scale_side_effects(self, scale_factor: float) -> Self:

View file

@ -27,13 +27,20 @@ class SurroundingRectangle(Rectangle):
color: ManimColor = YELLOW,
**kwargs
):
super().__init__(
width=mobject.get_width() + 2 * buff,
height=mobject.get_height() + 2 * buff,
color=color,
**kwargs
)
self.move_to(mobject)
super().__init__(color=color, **kwargs)
self.buff = buff
self.surround(mobject)
def surround(self, mobject, buff=None) -> Self:
self.mobject = mobject
self.buff = buff if buff is not None else self.buff
super().surround(mobject, self.buff)
return self
def set_buff(self, buff) -> Self:
self.buff = buff
self.surround(self.mobject)
return self
class BackgroundRectangle(SurroundingRectangle):
@ -110,6 +117,7 @@ class Underline(Line):
buff: float = SMALL_BUFF,
stroke_color=WHITE,
stroke_width: float | Sequence[float] = [0, 3, 3, 0],
stretch_factor=1.2,
**kwargs
):
super().__init__(
@ -119,5 +127,6 @@ class Underline(Line):
**kwargs
)
self.insert_n_curves(30)
self.match_width(mobject)
self.set_stroke(stroke_color, stroke_width)
self.set_width(mobject.get_width() * stretch_factor)
self.next_to(mobject, DOWN, buff=buff)

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import numpy as np
import itertools as it
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.rotation import Rotating
@ -14,6 +15,7 @@ from manimlib.constants import DOWN
from manimlib.constants import FRAME_WIDTH
from manimlib.constants import GREEN
from manimlib.constants import GREEN_SCREEN
from manimlib.constants import GREEN_E
from manimlib.constants import GREY
from manimlib.constants import GREY_A
from manimlib.constants import GREY_B
@ -26,6 +28,7 @@ from manimlib.constants import ORIGIN
from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import RED
from manimlib.constants import RED_E
from manimlib.constants import RIGHT
from manimlib.constants import SMALL_BUFF
from manimlib.constants import SMALL_BUFF
@ -36,6 +39,7 @@ from manimlib.constants import DL
from manimlib.constants import DR
from manimlib.constants import WHITE
from manimlib.constants import YELLOW
from manimlib.constants import TAU
from manimlib.mobject.boolean_ops import Difference
from manimlib.mobject.geometry import Arc
from manimlib.mobject.geometry import Circle
@ -44,6 +48,7 @@ from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import AnnularSector
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.numbers import Integer
from manimlib.mobject.svg.svg_mobject import SVGMobject
@ -358,7 +363,7 @@ class Bubble(SVGMobject):
stroke_width: float = 3.0,
**kwargs
):
self.direction = direction
self.direction = LEFT # Possibly updated below by self.flip()
self.bubble_center_adjustment_factor = bubble_center_adjustment_factor
self.content_scale_factor = content_scale_factor
@ -380,7 +385,7 @@ class Bubble(SVGMobject):
if direction[0] > 0:
self.flip()
self.content = Mobject()
self.content = VMobject()
def get_tip(self):
# TODO, find a better way
@ -403,10 +408,10 @@ class Bubble(SVGMobject):
self.direction = -np.array(self.direction)
return self
def pin_to(self, mobject):
def pin_to(self, mobject, auto_flip=False):
mob_center = mobject.get_center()
want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0])
if want_to_flip:
if want_to_flip and auto_flip:
self.flip()
boundary_point = mobject.get_bounding_box_point(UP - self.direction)
vector_from_center = 1.0 * (boundary_point - mob_center)
@ -579,7 +584,6 @@ class Piano3D(VGroup):
key.set_color(BLACK)
class DieFace(VGroup):
def __init__(
self,
@ -590,7 +594,7 @@ class DieFace(VGroup):
stroke_width: float = 2.0,
fill_color: ManimColor = GREY_E,
dot_radius: float = 0.08,
dot_color: ManimColor = BLUE_B,
dot_color: ManimColor = WHITE,
dot_coalesce_factor: float = 0.5
):
dot = Dot(radius=dot_radius, fill_color=dot_color)
@ -622,5 +626,50 @@ class DieFace(VGroup):
arrangement.space_out_submobjects(dot_coalesce_factor)
super().__init__(square, arrangement)
self.dots = arrangement
self.value = value
self.index = value
class Dartboard(VGroup):
radius = 3
n_sectors = 20
def __init__(self, **kwargs):
super().__init__(**kwargs)
n_sectors = self.n_sectors
angle = TAU / n_sectors
segments = VGroup(*[
VGroup(*[
AnnularSector(
inner_radius=in_r,
outer_radius=out_r,
start_angle=n * angle,
angle=angle,
fill_color=color,
)
for n, color in zip(
range(n_sectors),
it.cycle(colors)
)
])
for colors, in_r, out_r in [
([GREY_B, GREY_E], 0, 1),
([GREEN_E, RED_E], 0.5, 0.55),
([GREEN_E, RED_E], 0.95, 1),
]
])
segments.rotate(-angle / 2)
bullseyes = VGroup(*[
Circle(radius=r)
for r in [0.07, 0.035]
])
bullseyes.set_fill(opacity=1)
bullseyes.set_stroke(width=0)
bullseyes[0].set_color(GREEN_E)
bullseyes[1].set_color(RED_E)
self.bullseye = bullseyes[1]
self.add(*segments, *bullseyes)
self.scale(self.radius)

View file

@ -47,6 +47,7 @@ class StringMobject(SVGMobject, ABC):
self,
string: str,
fill_color: ManimColor = WHITE,
fill_border_width: float = 0.5,
stroke_color: ManimColor = WHITE,
stroke_width: float = 0,
base_color: ManimColor = WHITE,
@ -65,12 +66,10 @@ class StringMobject(SVGMobject, ABC):
self.use_labelled_svg = use_labelled_svg
self.parse()
super().__init__(
stroke_color=stroke_color,
fill_color=fill_color,
stroke_width=stroke_width,
**kwargs
)
super().__init__(**kwargs)
self.set_stroke(stroke_color, stroke_width)
self.set_fill(fill_color, border_width=fill_border_width)
self.note_changed_stroke()
self.labels = [submob.label for submob in self.submobjects]
def get_file_path(self, is_labelled: bool = False) -> str:

View file

@ -22,6 +22,7 @@ DEFAULT_GLOW_DOT_RADIUS = 0.2
DEFAULT_GRID_HEIGHT = 6
DEFAULT_BUFF_RATIO = 0.5
class DotCloud(PMobject):
shader_folder: str = "true_dot"
render_primitive: int = moderngl.POINTS
@ -116,6 +117,10 @@ class DotCloud(PMobject):
def get_radius(self) -> float:
return self.get_radii().max()
def scale_radii(self, scale_factor: float) -> Self:
self.set_radius(scale_factor * self.get_radii())
return self
def set_glow_factor(self, glow_factor: float) -> Self:
self.uniforms["glow_factor"] = glow_factor
return self

View file

@ -130,6 +130,8 @@ class Surface(Mobject):
def get_unit_normals(self) -> Vect3Array:
nu, nv = self.resolution
indices = np.arange(nu * nv)
if len(indices) == 0:
return np.zeros((3, 0))
left = indices - 1
right = indices + 1
@ -166,7 +168,7 @@ class Surface(Mobject):
nu, nv = smobject.resolution
self.data['point'][:] = self.get_partial_points_array(
self.data['point'], a, b,
smobject.data['point'], a, b,
(nu, nv, 3),
axis=axis
)
@ -183,7 +185,7 @@ class Surface(Mobject):
if len(points) == 0:
return points
nu, nv = resolution[:2]
points = points.reshape(resolution)
points = points.reshape(resolution).copy()
max_index = resolution[axis] - 1
lower_index, lower_residue = integer_interpolate(0, max_index, a)
upper_index, upper_residue = integer_interpolate(0, max_index, b)

View file

@ -11,17 +11,20 @@ from manimlib.constants import DEFAULT_STROKE_WIDTH
from manimlib.constants import DEGREES
from manimlib.constants import JOINT_TYPE_MAP
from manimlib.constants import ORIGIN, OUT
from manimlib.constants import TAU
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Point
from manimlib.utils.bezier import bezier
from manimlib.utils.bezier import get_quadratic_approximation_of_cubic
from manimlib.utils.bezier import approx_smooth_quadratic_bezier_handles
from manimlib.utils.bezier import smooth_quadratic_path
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.bezier import find_intersection
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.bezier import outer_interpolate
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
from manimlib.utils.color import color_gradient
from manimlib.utils.color import rgb_to_hex
from manimlib.utils.iterables import make_even
@ -38,6 +41,8 @@ from manimlib.utils.space_ops import get_unit_normal
from manimlib.utils.space_ops import line_intersects_path
from manimlib.utils.space_ops import midpoint
from manimlib.utils.space_ops import normalize_along_axis
from manimlib.utils.space_ops import rotation_between_vectors
from manimlib.utils.space_ops import poly_line_length
from manimlib.utils.space_ops import z_to_vector
from manimlib.shader_wrapper import ShaderWrapper
from manimlib.shader_wrapper import FillShaderWrapper
@ -234,7 +239,7 @@ class VMobject(Mobject):
stroke_opacity: float | Iterable[float] | None = None,
stroke_rgba: Vect4 | None = None,
stroke_width: float | Iterable[float] | None = None,
stroke_background: bool = True,
stroke_background: bool = False,
shading: Tuple[float, float, float] | None = None,
recurse: bool = True
) -> Self:
@ -374,7 +379,7 @@ class VMobject(Mobject):
data = self.data if self.has_points() else self._data_defaults
return rgb_to_hex(data["stroke_rgba"][0, :3])
def get_stroke_width(self) -> float | np.ndarray:
def get_stroke_width(self) -> float:
data = self.data if self.has_points() else self._data_defaults
return data["stroke_width"][0, 0]
@ -436,7 +441,7 @@ class VMobject(Mobject):
fill_border_width: float = 0.5,
recurse: bool = True
) -> Self:
super().apply_depth_test(recurse)
super().deactivate_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
@ -455,6 +460,9 @@ class VMobject(Mobject):
anchors: Vect3Array,
handles: Vect3Array,
) -> Self:
if len(anchors) == 0:
self.clear_points()
return self
assert(len(anchors) == len(handles) + 1)
points = resize_array(self.get_points(), 2 * len(anchors) - 1)
points[0::2] = anchors
@ -543,6 +551,26 @@ class VMobject(Mobject):
self.add_cubic_bezier_curve_to(new_handle, handle, point)
return self
def add_arc_to(self, point: Vect3, angle: float, n_components: int | None = None, threshold: float = 1e-3) -> Self:
self.throw_error_if_no_points()
if abs(angle) < threshold:
self.add_line_to(point)
return self
# Assign default value for n_components
if n_components is None:
n_components = int(np.ceil(8 * abs(angle) / TAU))
arc_points = quadratic_bezier_points_for_arc(angle, n_components)
target_vect = point - self.get_end()
curr_vect = arc_points[-1] - arc_points[0]
arc_points = arc_points @ rotation_between_vectors(curr_vect, target_vect).T
arc_points *= get_norm(target_vect) / get_norm(curr_vect)
arc_points += (self.get_end() - arc_points[0])
self.append_points(arc_points[1:])
return self
def has_new_path_started(self) -> bool:
points = self.get_points()
if len(points) == 0:
@ -642,6 +670,8 @@ class VMobject(Mobject):
def change_anchor_mode(self, mode: str) -> Self:
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
if self.get_num_points() == 0:
return self
subpaths = self.get_subpaths()
self.clear_points()
for subpath in subpaths:
@ -745,7 +775,7 @@ class VMobject(Mobject):
return self.get_subpaths_from_points(self.get_points())
def get_nth_curve_points(self, n: int) -> Vect3Array:
assert(n < self.get_num_curves())
assert n < self.get_num_curves()
return self.get_points()[2 * n:2 * n + 3]
def get_nth_curve_function(self, n: int) -> Callable[[float], Vect3]:
@ -761,12 +791,14 @@ class VMobject(Mobject):
curve_func = self.get_nth_curve_function(n)
return curve_func(residue)
def point_from_proportion(self, alpha: float) -> Vect3:
if alpha <= 0:
return self.get_start()
elif alpha >= 1:
return self.get_end()
def curve_and_prop_of_partial_point(self, alpha) -> Tuple[int, float]:
"""
If you want a point a proportion alpha along the curve, this
gives you the index of the appropriate bezier curve, together
with the proportion along that curve you'd need to travel
"""
if alpha == 0:
return (0, 0.0)
partials: list[float] = [0]
for tup in self.get_bezier_tuples():
if self.consider_points_equal(tup[0], tup[1]):
@ -778,14 +810,24 @@ class VMobject(Mobject):
partials.append(partials[-1] + arclen)
full = partials[-1]
if full == 0:
return self.get_start()
return len(partials), 1.0
# First index where the partial length is more than alpha times the full length
i = next(
index = next(
(i for i, x in enumerate(partials) if x >= full * alpha),
len(partials) # Default
len(partials) - 1 # Default
)
residue = float(inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha))
return self.get_nth_curve_function(i - 1)(residue)
residue = float(inverse_interpolate(
partials[index - 1] / full, partials[index] / full, alpha
))
return index - 1, residue
def point_from_proportion(self, alpha: float) -> Vect3:
if alpha <= 0:
return self.get_start()
elif alpha >= 1:
return self.get_end()
index, residue = self.curve_and_prop_of_partial_point(alpha)
return self.get_nth_curve_function(index)(residue)
def get_anchors_and_handles(self) -> list[Vect3]:
"""
@ -814,14 +856,16 @@ class VMobject(Mobject):
return np.vstack(new_points)
def get_arc_length(self, n_sample_points: int | None = None) -> float:
if n_sample_points is None:
n_sample_points = 4 * self.get_num_curves() + 1
if n_sample_points is not None:
points = np.array([
self.point_from_proportion(a)
self.quick_point_from_proportion(a)
for a in np.linspace(0, 1, n_sample_points)
])
diffs = points[1:] - points[:-1]
return sum(map(get_norm, diffs))
return poly_line_length(points)
points = self.get_points()
inner_len = poly_line_length(points[::2])
outer_len = poly_line_length(points)
return interpolate(inner_len, outer_len, 1 / 3)
def get_area_vector(self) -> Vect3:
# Returns a vector whose length is the area bound by

View file

@ -1021,9 +1021,10 @@ class EndScene(Exception):
class ThreeDScene(Scene):
samples = 4
default_frame_orientation = (-30, 70)
always_depth_test = True
def add(self, *mobjects, set_depth_test: bool = True):
for mob in mobjects:
if set_depth_test and not mob.is_fixed_in_frame():
if set_depth_test and not mob.is_fixed_in_frame() and self.always_depth_test:
mob.apply_depth_test()
super().add(*mobjects)

View file

@ -42,6 +42,7 @@ class SceneFileWriter(object):
# Where should this be written
output_directory: str | None = None,
file_name: str | None = None,
subdirectory_for_videos: bool = False,
open_file_upon_completion: bool = False,
show_file_location_upon_completion: bool = False,
quiet: bool = False,
@ -49,8 +50,8 @@ class SceneFileWriter(object):
progress_description_len: int = 40,
video_codec: str = "libx264",
pixel_format: str = "yuv420p",
saturation: float = 1.7,
gamma: float = 1.2,
saturation: float = 1.0,
gamma: float = 1.0,
):
self.scene: Scene = scene
self.write_to_movie = write_to_movie
@ -63,6 +64,7 @@ class SceneFileWriter(object):
self.output_directory = output_directory
self.file_name = file_name
self.open_file_upon_completion = open_file_upon_completion
self.subdirectory_for_videos = subdirectory_for_videos
self.show_file_location_upon_completion = show_file_location_upon_completion
self.quiet = quiet
self.total_frames = total_frames
@ -88,7 +90,10 @@ class SceneFileWriter(object):
image_file = add_extension_if_not_present(scene_name, ".png")
self.image_file_path = os.path.join(image_dir, image_file)
if self.write_to_movie:
if self.subdirectory_for_videos:
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
else:
movie_dir = guarantee_existence(out_dir)
movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension)
self.movie_file_path = os.path.join(movie_dir, movie_file)
if self.break_into_partial_movies:

View file

@ -56,6 +56,7 @@ class ShaderWrapper(object):
self.init_program_code()
self.init_program()
self.texture_names_to_ids = dict()
if texture_paths is not None:
self.init_textures(texture_paths)
self.init_vao()
@ -82,11 +83,10 @@ class ShaderWrapper(object):
self.vert_format = moderngl.detect_format(self.program, self.vert_attributes)
def init_textures(self, texture_paths: dict[str, str]):
names_to_ids = {
self.texture_names_to_ids = {
name: get_texture_id(image_path_to_texture(path, self.ctx))
for name, path in texture_paths.items()
}
self.update_program_uniforms(names_to_ids)
def init_vao(self):
self.vbo = None
@ -138,6 +138,7 @@ class ShaderWrapper(object):
self.mobject_uniforms,
self.depth_test,
self.render_primitive,
self.texture_names_to_ids,
]))
def refresh_id(self) -> None:
@ -224,7 +225,8 @@ class ShaderWrapper(object):
def update_program_uniforms(self, camera_uniforms: UniformDict):
if self.program is None:
return
for name, value in (*self.mobject_uniforms.items(), *camera_uniforms.items()):
for uniforms in [self.mobject_uniforms, camera_uniforms, self.texture_names_to_ids]:
for name, value in uniforms.items():
set_program_uniform(self.program, name, value)
def get_vertex_buffer_object(self, refresh: bool = True):

View file

@ -5,7 +5,11 @@ uniform mat4 perspective;
in vec4 color;
in float scaled_aaw;
in vec3 v_point;
in vec3 point;
in vec3 to_cam;
in vec3 center;
in float radius;
in vec2 uv_coords;
out vec4 frag_color;
@ -13,9 +17,8 @@ out vec4 frag_color;
#INSERT finalize_color.glsl
void main() {
vec2 vect = 2.0 * gl_PointCoord.xy - vec2(1.0);
float r = length(vect);
if(r > 1.0 + scaled_aaw) discard;
float r = length(uv_coords.xy);
if(r > 1.0) discard;
frag_color = color;
@ -24,9 +27,9 @@ void main() {
}
if(shading != vec3(0.0)){
vec3 normal = vec3(vect, sqrt(1 - r * r));
normal = (perspective * vec4(normal, 0.0)).xyz;
frag_color = finalize_color(frag_color, v_point, normal);
vec3 point_3d = point + radius * sqrt(1 - r * r) * to_cam;
vec3 normal = normalize(point_3d - center);
frag_color = finalize_color(frag_color, point_3d, normal);
}
frag_color.a *= smoothstep(1.0, 1.0 - scaled_aaw, r);

View file

@ -0,0 +1,44 @@
#version 330
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
uniform float pixel_size;
uniform float anti_alias_width;
uniform float frame_scale;
uniform vec3 camera_position;
in vec3 v_point[1];
in float v_radius[1];
in vec4 v_rgba[1];
out vec4 color;
out float scaled_aaw;
out vec3 point;
out vec3 to_cam;
out vec3 center;
out float radius;
out vec2 uv_coords;
#INSERT emit_gl_Position.glsl
void main(){
color = v_rgba[0];
radius = v_radius[0];
center = v_point[0];
scaled_aaw = (anti_alias_width * pixel_size) / v_radius[0];
to_cam = normalize(camera_position - v_point[0]);
vec3 right = v_radius[0] * normalize(cross(vec3(0, 1, 1), to_cam));
vec3 up = v_radius[0] * normalize(cross(to_cam, right));
for(int i = -1; i < 2; i += 2){
for(int j = -1; j < 2; j += 2){
point = v_point[0] + i * right + j * up;
uv_coords = vec2(i, j);
emit_gl_Position(point);
EmitVertex();
}
}
EndPrimitive();
}

View file

@ -1,26 +1,16 @@
#version 330
uniform float pixel_size;
uniform float anti_alias_width;
in vec3 point;
in float radius;
in vec4 rgba;
out vec4 color;
out float scaled_aaw;
out vec3 v_point;
out vec3 light_pos;
out float v_radius;
out vec4 v_rgba;
#INSERT emit_gl_Position.glsl
void main(){
v_point = point;
color = rgba;
scaled_aaw = (anti_alias_width * pixel_size) / radius;
emit_gl_Position(point);
float z = -10 * gl_Position.z;
float scaled_radius = radius * 1.0 / (1.0 - z);
gl_PointSize = 2 * ((scaled_radius / pixel_size) + anti_alias_width);
v_radius = radius;
v_rgba = rgba;
}

View file

@ -171,6 +171,16 @@ def match_interpolate(
)
def quadratic_bezier_points_for_arc(angle: float, n_components: int = 8):
n_points = 2 * n_components + 1
angles = np.linspace(0, angle, n_points)
points = np.array([np.cos(angles), np.sin(angles), np.zeros(n_points)]).T
# Adjust handles
theta = angle / n_components
points[1::2] /= np.cos(theta / 2)
return points
def approx_smooth_quadratic_bezier_handles(
points: FloatArray
) -> FloatArray:

View file

@ -136,6 +136,18 @@ def array_is_constant(arr: np.ndarray) -> bool:
return len(arr) > 0 and (arr == arr[0]).all()
def cartesian_product(*arrays: np.ndarray):
"""
Copied from https://stackoverflow.com/a/11146645
"""
la = len(arrays)
dtype = np.result_type(*arrays)
arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(np.ix_(*arrays)):
arr[..., i] = a
return arr.reshape(-1, la)
def hash_obj(obj: object) -> int:
if isinstance(obj, dict):
return hash(tuple(sorted([

View file

@ -21,6 +21,7 @@ if TYPE_CHECKING:
from moderngl.framebuffer import Framebuffer
# Global maps updated as textures are allocated
ID_TO_TEXTURE: dict[int, moderngl.Texture] = dict()
PROGRAM_UNIFORM_MIRRORS: dict[int, dict[str, float | tuple]] = dict()
@ -134,7 +135,6 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
return f"vec3[{len(rgb_list)}]({data})"
@lru_cache()
def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray]:
"""

View file

@ -61,6 +61,13 @@ def normalize(
return np.zeros(len(vect))
def poly_line_length(points):
"""
Return the sum of the lengths between adjacent points
"""
diffs = points[1:] - points[:-1]
return np.sqrt((diffs**2).sum(1)).sum()
# Operations related to rotation
@ -199,7 +206,7 @@ def normalize_along_axis(
) -> np.ndarray:
norms = np.sqrt((array * array).sum(axis))
norms[norms == 0] = 1
return (array.T / norms).T
return array / norms[:, np.newaxis]
def get_unit_normal(

View file

@ -16,7 +16,7 @@ def num_tex_symbols(tex: str) -> int:
# \begin{array}{cc}, etc.
pattern = "|".join(
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
for s in ["begin", "end", "phantom", "text"]
for s in ["begin", "end", "phantom"]
)
tex = re.sub(pattern, "", tex)

View file

@ -49,6 +49,7 @@ TEX_TO_SYMBOL_COUNT = {
R"\div": 2,
R"\doteq": 2,
R"\dotfill": 0,
R"\dots": 3,
R"\emph": 0,
R"\exp": 3,
R"\fbox": 4,
@ -102,6 +103,7 @@ TEX_TO_SYMBOL_COUNT = {
R"\makebox": 0,
R"\mapsto": 2,
R"\markright": 0,
R"\mathds": 0,
R"\max": 3,
R"\mbox": 0,
R"\medskip": 0,
@ -160,6 +162,7 @@ TEX_TO_SYMBOL_COUNT = {
R"\sup": 3,
R"\tan": 3,
R"\tanh": 4,
R"\text": 0,
R"\textbf": 0,
R"\textfraction": 2,
R"\textstyle": 0,

View file

@ -27,7 +27,7 @@ class Window(PygletWindow):
self,
scene: Scene,
size: tuple[int, int] = (1280, 720),
samples = 0
samples: int = 0
):
scene.window = self
super().__init__(size=size, samples=samples)
@ -47,9 +47,12 @@ class Window(PygletWindow):
self.to_default_position()
def to_default_position(self):
self.size = self.default_size
self.position = self.default_position
self.swap_buffers()
# Hack. Sometimes, namely when configured to open in a separate window,
# the window needs to be resized to display correctly.
w, h = self.default_size
self.size = (w - 1, h - 1)
self.size = (w, h)
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
custom_position = get_customization()["window_position"]