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

@ -25,7 +25,7 @@ class ImageMobject(Mobject):
('opacity', np.float32, (1,)),
]
def __init__(
def __init__(
self,
filename: str,
height: float = 4.0,

View file

@ -130,11 +130,13 @@ 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
left = indices - 1
right = indices + 1
up = indices - nv
down = indices + nv
up = indices - nv
down = indices + nv
left[0] = indices[0]
right[-1] = 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]
@ -423,7 +428,7 @@ class VMobject(Mobject):
self,
anti_alias_width: float = 0,
fill_border_width: float = 0,
recurse: bool=True
recurse: bool = True
) -> Self:
super().apply_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
@ -434,9 +439,9 @@ class VMobject(Mobject):
self,
anti_alias_width: float = 1.0,
fill_border_width: float = 0.5,
recurse: bool=True
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,8 +775,8 @@ 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())
return self.get_points()[2 * n : 2 * n + 3]
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]:
return bezier(self.get_nth_curve_points(n))
@ -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
points = np.array([
self.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))
if n_sample_points is not None:
points = np.array([
self.quick_point_from_proportion(a)
for a in np.linspace(0, 1, n_sample_points)
])
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:
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
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,8 +225,9 @@ 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()):
set_program_uniform(self.program, name, value)
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):
if refresh:

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()
@ -56,7 +57,7 @@ def get_shader_program(
vertex_shader: str,
fragment_shader: Optional[str] = None,
geometry_shader: Optional[str] = None,
) -> moderngl.Program:
) -> moderngl.Program:
return ctx.program(
vertex_shader=vertex_shader,
fragment_shader=fragment_shader,
@ -74,7 +75,7 @@ def set_program_uniform(
of previously set uniforms for that program so that it
doesn't needlessly reset it, requiring an exchange with gpu
memory, if it sees the same value again.
Returns True if changed the program, False if it left it as is.
"""
@ -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"]