mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
commit
2c110790d2
31 changed files with 414 additions and 172 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,7 +25,7 @@ class ImageMobject(Mobject):
|
|||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
def __init__(
|
||||
self,
|
||||
filename: str,
|
||||
height: float = 4.0,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
44
manimlib/shaders/true_dot/geom.glsl
Normal file
44
manimlib/shaders/true_dot/geom.glsl
Normal 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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]
|
||||
|
|
Loading…
Add table
Reference in a new issue