diff --git a/example_scenes.py b/example_scenes.py index 10e895f9..8ffa857d 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -251,7 +251,7 @@ class TexIndexing(Scene): self.play(FlashAround(part)) self.wait() self.play(FadeOut(equation)) - + # Indexing by substrings like this may not work when # the order in which Latex draws symbols does not match # the order in which they show up in the string. @@ -289,7 +289,7 @@ class UpdatersExample(Scene): brace = always_redraw(Brace, square, UP) label = TexText("Width = 0.00") - number = label.make_number_changable("0.00") + number = label.make_number_changeable("0.00") # This ensures that the method deicmal.next_to(square) # is called on every frame @@ -488,10 +488,7 @@ class GraphExample(Scene): # with the intent of having other mobjects update based # on the parameter x_tracker = ValueTracker(2) - f_always( - dot.move_to, - lambda: axes.i2gp(x_tracker.get_value(), parabola) - ) + dot.add_updater(lambda d: d.move_to(axes.i2gp(x_tracker.get_value(), parabola))) self.play(x_tracker.animate.set_value(4), run_time=3) self.play(x_tracker.animate.set_value(-2), run_time=3) @@ -515,7 +512,7 @@ class TexAndNumbersExample(Scene): # on them. tex = Tex("x^2 + y^2 = 4.00") tex.next_to(axes, UP, buff=0.5) - value = tex.make_number_changable("4.00") + value = tex.make_number_changeable("4.00") # This will tie the right hand side of our equation to @@ -537,10 +534,10 @@ class TexAndNumbersExample(Scene): rate_func=there_and_back, ) - # By default, tex.make_number_changable replaces the first occurance + # By default, tex.make_number_changeable replaces the first occurance # of the number,but by passing replace_all=True it replaces all and # returns a group of the results - exponents = tex.make_number_changable("2", replace_all=True) + exponents = tex.make_number_changeable("2", replace_all=True) self.play( LaggedStartMap( FlashAround, exponents, diff --git a/manimlib/__init__.py b/manimlib/__init__.py index ca4d4185..8467f980 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -43,7 +43,6 @@ from manimlib.mobject.probability import * from manimlib.mobject.shape_matchers import * from manimlib.mobject.svg.brace import * from manimlib.mobject.svg.drawings import * -from manimlib.mobject.svg.tex_mobject import * from manimlib.mobject.svg.string_mobject import * from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.special_tex import * diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index d23d90cb..a0a7255c 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -180,4 +180,5 @@ class LaggedStartMap(LaggedStart): *(anim_func(submob, **anim_kwargs) for submob in group), run_time=run_time, lag_ratio=lag_ratio, + group=group ) diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 54f183c3..207de90c 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -118,6 +118,7 @@ class FadeTransform(Transform): def ghost_to(self, source: Mobject, target: Mobject) -> None: source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match) + source.set_uniform(**target.get_uniforms()) source.set_opacity(0) def get_all_mobjects(self) -> list[Mobject]: @@ -134,7 +135,8 @@ class FadeTransform(Transform): Animation.clean_up_from_scene(self, scene) scene.remove(self.mobject) self.mobject[0].restore() - scene.add(self.to_add_on_completion) + if not self.remover: + scene.add(self.to_add_on_completion) class FadeTransformPieces(FadeTransform): diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py index 9c9c31ce..78a7d095 100644 --- a/manimlib/camera/camera_frame.py +++ b/manimlib/camera/camera_frame.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +import warnings import numpy as np from scipy.spatial.transform import Rotation @@ -9,8 +10,10 @@ from pyrr import Matrix44 from manimlib.constants import DEGREES, RADIANS from manimlib.constants import FRAME_SHAPE from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP +from manimlib.constants import PI from manimlib.mobject.mobject import Mobject from manimlib.utils.space_ops import normalize +from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING @@ -62,9 +65,19 @@ class CameraFrame(Mobject): def get_euler_angles(self) -> np.ndarray: orientation = self.get_orientation() - if all(orientation.as_quat() == [0, 0, 0, 1]): + if np.isclose(orientation.as_quat(), [0, 0, 0, 1]).all(): return np.zeros(3) - return orientation.as_euler(self.euler_axes)[::-1] + with warnings.catch_warnings(): + warnings.simplefilter('ignore', UserWarning) # Ignore UserWarnings + angles = orientation.as_euler(self.euler_axes)[::-1] + # Handle Gimble lock case + if np.isclose(angles[1], 0, atol=1e-2): + angles[0] = angles[0] + angles[2] + angles[2] = 0 + if np.isclose(angles[1], PI, atol=1e-2): + angles[0] = angles[0] - angles[2] + angles[2] = 0 + return angles def get_theta(self): return self.get_euler_angles()[0] @@ -134,16 +147,16 @@ class CameraFrame(Mobject): def increment_euler_angles( self, - dtheta: float | None = None, - dphi: float | None = None, - dgamma: float | None = None, + dtheta: float = 0, + dphi: float = 0, + dgamma: float = 0, units: float = RADIANS ): angles = self.get_euler_angles() - for i, value in enumerate([dtheta, dphi, dgamma]): - if value is not None: - angles[i] += value * units - self.set_euler_angles(*angles) + new_angles = angles + np.array([dtheta, dphi, dgamma]) * units + new_angles[1] = clip(new_angles[1], 0, PI) # Limit range for phi + new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1]) + self.set_orientation(new_rot) return self def set_euler_axes(self, seq: str): diff --git a/manimlib/config.py b/manimlib/config.py index 1de7bbd7..a6a8f62b 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -6,7 +6,7 @@ import colour import importlib import inspect import os -from screeninfo import get_monitors +import screeninfo import sys import yaml @@ -433,7 +433,10 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict: def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict: # Default to making window half the screen size # but make it full screen if -f is passed in - monitors = get_monitors() + try: + monitors = screeninfo.get_monitors() + except screeninfo.ScreenInfoError: + pass mon_index = custom_config["window_monitor"] monitor = monitors[min(mon_index, len(monitors) - 1)] aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"] diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 5206812d..01a73560 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -103,21 +103,16 @@ class TracedPath(VMobject): time_per_anchor: float = 1.0 / 15, stroke_width: float | Iterable[float] = 2.0, stroke_color: ManimColor = WHITE, - fill_opacity: float = 0.0, **kwargs ): - super().__init__( - stroke_width=stroke_width, - stroke_color=stroke_color, - fill_opacity=fill_opacity, - **kwargs - ) + super().__init__(**kwargs) self.traced_point_func = traced_point_func self.time_traced = time_traced self.time_per_anchor = time_per_anchor self.time: float = 0 self.traced_points: list[np.ndarray] = [] self.add_updater(lambda m, dt: m.update_path(dt)) + self.set_stroke(stroke_color, stroke_width) def update_path(self, dt: float) -> Self: if dt == 0: @@ -167,3 +162,4 @@ class TracingTail(TracedPath): stroke_color=stroke_color, **kwargs ) + self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity)) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index fcd11612..eb0c0b83 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -530,7 +530,6 @@ class ThreeDAxes(Axes): z_axis_config: dict = dict(), z_normal: Vect3 = DOWN, depth: float | None = None, - flat_stroke: bool = False, **kwargs ): Axes.__init__(self, x_range, y_range, **kwargs) @@ -555,8 +554,6 @@ class ThreeDAxes(Axes): self.axes.add(self.z_axis) self.add(self.z_axis) - self.set_flat_stroke(flat_stroke) - def get_all_ranges(self) -> list[Sequence[float]]: return [self.x_range, self.y_range, self.z_range] @@ -603,9 +600,6 @@ class ThreeDAxes(Axes): **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) diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 9b93381c..cb5991bf 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -222,6 +222,7 @@ class DecimalMatrix(Matrix): decimal_config: dict = dict(), **config ): + self.float_matrix = matrix super().__init__( matrix, element_config=dict( diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index d42bde90..72942e14 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -122,13 +122,9 @@ class Underline(Line): stretch_factor=1.2, **kwargs ): - super().__init__( - LEFT, RIGHT, - stroke_color=stroke_color, - stroke_width=stroke_width, - **kwargs - ) - self.insert_n_curves(30) + super().__init__(LEFT, RIGHT, **kwargs) + if not isinstance(stroke_width, (float, int)): + self.insert_n_curves(len(stroke_width) - 2) self.set_stroke(stroke_color, stroke_width) self.set_width(mobject.get_width() * stretch_factor) self.next_to(mobject, DOWN, buff=buff) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index f620b47a..198b3560 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -2,6 +2,7 @@ from __future__ import annotations import numpy as np import itertools as it +import random from manimlib.animation.composition import AnimationGroup from manimlib.animation.rotation import Rotating @@ -24,6 +25,7 @@ from manimlib.constants import LEFT from manimlib.constants import LEFT from manimlib.constants import MED_LARGE_BUFF from manimlib.constants import MED_SMALL_BUFF +from manimlib.constants import LARGE_BUFF from manimlib.constants import ORIGIN from manimlib.constants import OUT from manimlib.constants import PI @@ -41,6 +43,7 @@ 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.boolean_ops import Union from manimlib.mobject.geometry import Arc from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Dot @@ -51,6 +54,7 @@ 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.shape_matchers import SurroundingRectangle from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import TexText @@ -59,9 +63,13 @@ from manimlib.mobject.three_dimensions import Prismify from manimlib.mobject.three_dimensions import VCube from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.svg.text_mobject import Text +from manimlib.utils.bezier import interpolate +from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.rate_functions import linear from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import compass_directions +from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import midpoint from manimlib.utils.space_ops import rotate_vector @@ -344,66 +352,76 @@ class ClockPassesTime(AnimationGroup): ) -class Bubble(SVGMobject): +class Bubble(VGroup): file_name: str = "Bubbles_speech.svg" + bubble_center_adjustment_factor = 0.125 def __init__( self, + content: str | VMobject | None = None, + buff: float = 1.0, + filler_shape: Tuple[float, float] = (3.0, 2.0), + pin_point: Vect3 | None = None, direction: Vect3 = LEFT, - center_point: Vect3 = ORIGIN, - content_scale_factor: float = 0.7, - height: float = 4.0, - width: float = 8.0, - max_height: float | None = None, - max_width: float | None = None, - bubble_center_adjustment_factor: float = 0.125, + add_content: bool = True, fill_color: ManimColor = BLACK, fill_opacity: float = 0.8, stroke_color: ManimColor = WHITE, stroke_width: float = 3.0, **kwargs ): - 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 + super().__init__(**kwargs) + self.direction = direction - super().__init__( - fill_color=fill_color, - fill_opacity=fill_opacity, - stroke_color=stroke_color, - stroke_width=stroke_width, - **kwargs - ) + if content is None: + content = Rectangle(*filler_shape) + content.set_fill(opacity=0) + content.set_stroke(width=0) + elif isinstance(content, str): + content = Text(content) + self.content = content - self.center() - self.set_height(height, stretch=True) - self.set_width(width, stretch=True) - if max_height: - self.set_max_height(max_height) - if max_width: - self.set_max_width(max_width) + self.body = self.get_body(content, direction, buff) + self.body.set_fill(fill_color, fill_opacity) + self.body.set_stroke(stroke_color, stroke_width) + self.add(self.body) + + if add_content: + self.add(self.content) + + if pin_point is not None: + self.pin_to(pin_point) + + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + body = SVGMobject(self.file_name) if direction[0] > 0: - self.flip() - - self.content = VMobject() + body.flip() + # Resize + width = content.get_width() + height = content.get_height() + target_width = width + min(buff, height) + target_height = 1.35 * (height + buff) # Magic number? + body.set_shape(target_width, target_height) + body.move_to(content) + body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN) + return body def get_tip(self): - # TODO, find a better way - return self.get_corner(DOWN + self.direction) - 0.6 * self.direction + return self.get_corner(DOWN + self.direction) def get_bubble_center(self): factor = self.bubble_center_adjustment_factor return self.get_center() + factor * self.get_height() * UP def move_tip_to(self, point): - mover = VGroup(self) - if self.content is not None: - mover.add(self.content) - mover.shift(point - self.get_tip()) + self.shift(point - self.get_tip()) return self - def flip(self, axis=UP): - super().flip(axis=axis) + def flip(self, axis=UP, only_body=True, **kwargs): + super().flip(axis=axis, **kwargs) + if only_body: + # Flip in place, don't use kwargs + self.content.flip(axis=axis) if abs(axis[1]) > 0: self.direction = -np.array(self.direction) return self @@ -418,9 +436,9 @@ class Bubble(SVGMobject): self.move_tip_to(mob_center + vector_from_center) return self - def position_mobject_inside(self, mobject): - mobject.set_max_width(self.content_scale_factor * self.get_width()) - mobject.set_max_height(self.content_scale_factor * self.get_height() / 1.5) + def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF): + mobject.set_max_width(self.body.get_width() - 2 * buff) + mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff) mobject.shift(self.get_bubble_center() - mobject.get_center()) return mobject @@ -429,26 +447,110 @@ class Bubble(SVGMobject): self.content = mobject return self.content - def write(self, *text): - self.add_content(TexText(*text)) + def write(self, text): + self.add_content(Text(text)) return self - def resize_to_content(self, buff=0.75): - width = self.content.get_width() - height = self.content.get_height() - target_width = width + min(buff, height) - target_height = 1.35 * (self.content.get_height() + buff) - tip_point = self.get_tip() - self.stretch_to_fit_width(target_width, about_point=tip_point) - self.stretch_to_fit_height(target_height, about_point=tip_point) - self.position_mobject_inside(self.content) + def resize_to_content(self, buff=1.0): # TODO + self.body.match_points(self.get_body( + self.content, self.direction, buff + )) def clear(self): - self.add_content(VMobject()) + self.remove(self.content) return self class SpeechBubble(Bubble): + def __init__( + self, + content: str | VMobject | None = None, + buff: float = MED_SMALL_BUFF, + filler_shape: Tuple[float, float] = (2.0, 1.0), + stem_height_to_bubble_height: float = 0.5, + stem_top_x_props: Tuple[float, float] = (0.2, 0.3), + **kwargs + ): + self.stem_height_to_bubble_height = stem_height_to_bubble_height + self.stem_top_x_props = stem_top_x_props + super().__init__(content, buff, filler_shape, **kwargs) + + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + rect = SurroundingRectangle(content, buff=buff) + rect.round_corners() + lp = rect.get_corner(DL) + rp = rect.get_corner(DR) + stem_height = self.stem_height_to_bubble_height * rect.get_height() + low_prop, high_prop = self.stem_top_x_props + triangle = Polygon( + interpolate(lp, rp, low_prop), + interpolate(lp, rp, high_prop), + lp + stem_height * DOWN, + ) + result = Union(rect, triangle) + result.insert_n_curves(20) + if direction[0] > 0: + result.flip() + + return result + + +class ThoughtBubble(Bubble): + def __init__( + self, + content: str | VMobject | None = None, + buff: float = SMALL_BUFF, + filler_shape: Tuple[float, float] = (2.0, 1.0), + bulge_radius: float = 0.35, + bulge_overlap: float = 0.25, + noise_factor: float = 0.1, + circle_radii: list[float] = [0.1, 0.15, 0.2], + **kwargs + ): + self.bulge_radius = bulge_radius + self.bulge_overlap = bulge_overlap + self.noise_factor = noise_factor + self.circle_radii = circle_radii + super().__init__(content, buff, filler_shape, **kwargs) + + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + rect = SurroundingRectangle(content, buff) + perimeter = rect.get_arc_length() + radius = self.bulge_radius + step = (1 - self.bulge_overlap) * (2 * radius) + nf = self.noise_factor + corners = [rect.get_corner(v) for v in [DL, UL, UR, DR]] + points = [] + for c1, c2 in adjacent_pairs(corners): + n_alphas = int(get_norm(c1 - c2) / step) + 1 + for alpha in np.linspace(0, 1, n_alphas): + points.append(interpolate( + c1, c2, alpha + nf * (step / n_alphas) * (random.random() - 0.5) + )) + + cloud = Union(rect, *( + # Add bulges + Circle(radius=radius * (1 + nf * random.random())).move_to(point) + for point in points + )) + cloud.set_stroke(WHITE, 2) + + circles = VGroup(Circle(radius=radius) for radius in self.circle_radii) + circ_buff = 0.25 * self.circle_radii[0] + circles.arrange(UR, buff=circ_buff) + circles[1].shift(circ_buff * DR) + circles.next_to(cloud, DOWN, 4 * circ_buff, aligned_edge=LEFT) + circles.set_stroke(WHITE, 2) + + result = VGroup(*circles, cloud) + + if direction[0] > 0: + result.flip() + + return result + + +class OldSpeechBubble(Bubble): file_name: str = "Bubbles_speech.svg" @@ -456,17 +558,16 @@ class DoubleSpeechBubble(Bubble): file_name: str = "Bubbles_double_speech.svg" -class ThoughtBubble(Bubble): +class OldThoughtBubble(Bubble): file_name: str = "Bubbles_thought.svg" - def __init__(self, **kwargs): - Bubble.__init__(self, **kwargs) - self.submobjects.sort( - key=lambda m: m.get_bottom()[1] - ) + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + body = super().get_body(content, direction, buff) + body.sort(lambda p: p[1]) + return body def make_green_screen(self): - self.submobjects[-1].set_fill(GREEN_SCREEN, opacity=1) + self.body[-1].set_fill(GREEN_SCREEN, opacity=1) return self diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 144ca66e..54da7fa6 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -231,7 +231,7 @@ class Tex(StringMobject): )) return re.findall(pattern, self.string) - def make_number_changable( + def make_number_changeable( self, value: float | int | str, index: int = 0, @@ -241,7 +241,7 @@ class Tex(StringMobject): substr = str(value) parts = self.select_parts(substr) if len(parts) == 0: - log.warning(f"{value} not found in Tex.make_number_changable call") + log.warning(f"{value} not found in Tex.make_number_changeable call") return VMobject() if index > len(parts) - 1: log.warning(f"Requested {index}th occurance of {value}, but only {len(parts)} exist") diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 05a12ede..d29b7ec2 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -38,7 +38,6 @@ class SurfaceMesh(VGroup): normal_nudge: float = 1e-2, depth_test: bool = True, joint_type: str = 'no_joint', - flat_stroke: bool = False, **kwargs ): self.uv_surface = uv_surface @@ -52,7 +51,6 @@ class SurfaceMesh(VGroup): joint_type=joint_type, **kwargs ) - self.set_flat_stroke(flat_stroke) def init_points(self) -> None: uv_surface = self.uv_surface diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index b49872de..605c2aeb 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -71,5 +71,5 @@ class ImageMobject(Mobject): rgb = self.image.getpixel(( int((pw - 1) * x_alpha), int((ph - 1) * y_alpha), - )) + ))[:3] return np.array(rgb) / 255 diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 7286ff6f..753e6399 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -97,10 +97,10 @@ class VMobject(Mobject): long_lines: bool = False, # Could also be "no_joint", "bevel", "miter" joint_type: str = "auto", - flat_stroke: bool = True, + flat_stroke: bool = False, use_simple_quadratic_approx: bool = False, # Measured in pixel widths - anti_alias_width: float = 1.0, + anti_alias_width: float = 1.5, fill_border_width: float = 0.5, use_winding_fill: bool = True, **kwargs @@ -190,9 +190,10 @@ class VMobject(Mobject): recurse: bool = True ) -> Self: self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse) - if border_width is not None: - for mob in self.get_family(recurse): - mob.data["fill_border_width"] = border_width + if border_width is None: + border_width = 0 if self.get_fill_opacity() < 1 else 0.5 + for mob in self.get_family(recurse): + mob.data["fill_border_width"] = border_width self.note_changed_fill() return self @@ -202,6 +203,7 @@ class VMobject(Mobject): width: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None, background: bool | None = None, + flat: bool | None = None, recurse: bool = True ) -> Self: self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse) @@ -220,6 +222,9 @@ class VMobject(Mobject): for mob in self.get_family(recurse): mob.stroke_behind = background + if flat is not None: + self.set_flat_stroke(flat) + self.note_changed_stroke() return self @@ -672,7 +677,7 @@ class VMobject(Mobject): return bool((dots > 1 - 1e-3).all()) def change_anchor_mode(self, mode: str) -> Self: - assert(mode in ("jagged", "approx_smooth", "true_smooth")) + assert mode in ("jagged", "approx_smooth", "true_smooth") if self.get_num_points() == 0: return self subpaths = self.get_subpaths() @@ -696,7 +701,7 @@ class VMobject(Mobject): self.add_subpath(new_subpath) return self - def make_smooth(self, approx=False, recurse=True) -> Self: + def make_smooth(self, approx=True, recurse=True) -> Self: """ Edits the path so as to pass smoothly through all the current anchor points. @@ -721,7 +726,7 @@ class VMobject(Mobject): return self def add_subpath(self, points: Vect3Array) -> Self: - assert(len(points) % 2 == 1 or len(points) == 0) + assert len(points) % 2 == 1 or len(points) == 0 if not self.has_points(): self.set_points(points) return self @@ -1200,7 +1205,7 @@ class VMobject(Mobject): points = self.get_points() - if(len(points) < 3): + if len(points) < 3: return self.data["joint_product"] # Find all the unit tangent vectors at each joint diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 683e6f8a..7c32f2f6 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -48,6 +48,7 @@ RESIZE_KEY = 't' COLOR_KEY = 'c' INFORMATION_KEY = 'i' CURSOR_KEY = 'k' +COPY_FRAME_POSITION_KEY = 'p' # Note, a lot of the functionality here is still buggy and very much a work in progress. @@ -504,7 +505,7 @@ class InteractiveScene(Scene): self.toggle_selection_mode() elif char == "s" and modifiers == COMMAND_MODIFIER: self.save_selection_to_file() - elif char == PAN_3D_KEY and modifiers == COMMAND_MODIFIER: + elif char == "d" and modifiers == SHIFT_MODIFIER: self.copy_frame_positioning() elif symbol in ARROW_SYMBOLS: self.nudge_selection( diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a80d1866..6035c0a2 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -213,7 +213,8 @@ class Scene(object): show_animation_progress: bool = False, ) -> None: if not self.preview: - return # Embed is only relevant with a preview + # Embed is only relevant with a preview + return self.stop_skipping() self.update_frame() self.save_state() @@ -239,6 +240,8 @@ class Scene(object): i2g=self.i2g, i2m=self.i2m, checkpoint_paste=self.checkpoint_paste, + touch=lambda: shell.enable_gui("manim"), + notouch=lambda: shell.enable_gui(None), ) # Enables gui interactions during the embed @@ -260,20 +263,19 @@ class Scene(object): # namespace, since this is just a shell session anyway. shell.events.register( "pre_run_cell", - lambda: shell.user_global_ns.update(shell.user_ns) + lambda *args, **kwargs: shell.user_global_ns.update(shell.user_ns) ) # Operation to run after each ipython command - def post_cell_func(): + def post_cell_func(*args, **kwargs): if not self.is_window_closing(): self.update_frame(dt=0, ignore_skipping=True) - self.save_state() shell.events.register("post_run_cell", post_cell_func) # Flash border, and potentially play sound, on exceptions def custom_exc(shell, etype, evalue, tb, tb_offset=None): - # still show the error don't just swallow it + # Show the error don't just swallow it shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) if self.embed_error_sound: os.system("printf '\a'") diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index fb9a483e..db453855 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -1,66 +1,17 @@ #version 330 -in vec2 uv_coords; - -in float uv_stroke_width; -in float uv_anti_alias_width; +// Value between -1 and 1 +in float scaled_signed_dist_to_curve; +in float scaled_anti_alias_width; in vec4 color; -in float is_linear; - out vec4 frag_color; -const float QUICK_DIST_WIDTH = 0.2; - -float dist_to_curve(){ - // In the linear case, the curve will have - // been set to equal the x axis - if(bool(is_linear)) return abs(uv_coords.y); - - // Otherwise, find the distance from uv_coords to the curve y = x^2 - float x0 = uv_coords.x; - float y0 = uv_coords.y; - - // This is a quick approximation for computing - // the distance to the curve. - // Evaluate F(x, y) = y - x^2 - // divide by its gradient's magnitude - float Fxy = y0 - x0 * x0; - float approx_dist = abs(Fxy) * inversesqrt(1.0 + 4 * x0 * x0); - if(approx_dist < QUICK_DIST_WIDTH) return approx_dist; - - // Otherwise, solve for the minimal distance. - // The distance squared between (x0, y0) and a point (x, x^2) looks like - // - // (x0 - x)^2 + (y0 - x^2)^2 = x^4 + (1 - 2y0)x^2 - 2x0 * x + (x0^2 + y0^2) - // - // Setting the derivative equal to zero (and rescaling) looks like - // - // x^3 + (0.5 - y0) * x - 0.5 * x0 = 0 - // - // Adapted from https://www.shadertoy.com/view/ws3GD7 - x0 = abs(x0); - float p = (0.5 - y0) / 3.0; // p / 3 in usual Cardano's formula notation - float q = 0.25 * x0; // -q / 2 in usual Cardano's formula notation - float disc = q*q + p*p*p; - float r = sqrt(abs(disc)); - - float x = (disc > 0.0) ? - // 1 root - pow(q + r, 1.0 / 3.0) + pow(abs(q - r), 1.0 / 3.0) * sign(-p) : - // 3 roots - 2.0 * cos(atan(r, q) / 3.0) * sqrt(-p); - - return length(vec2(x0 - x, y0 - x * x)); -} - - void main() { - if (uv_stroke_width == 0) discard; + if(scaled_anti_alias_width < 0) discard; frag_color = color; // sdf for the region around the curve we wish to color. - float signed_dist = dist_to_curve() - 0.5 * uv_stroke_width; - - frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width); + float signed_dist_to_region = abs(scaled_signed_dist_to_curve) - 1.0; + frag_color.a *= smoothstep(0, -scaled_anti_alias_width, signed_dist_to_region); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 3297d62b..fbaeb901 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -1,12 +1,13 @@ #version 330 layout (triangles) in; -layout (triangle_strip, max_vertices = 6) out; +layout (triangle_strip, max_vertices = 64) out; // Related to MAX_STEPS below uniform float anti_alias_width; uniform float flat_stroke; uniform float pixel_size; uniform float joint_type; +uniform float frame_scale; in vec3 verts[3]; @@ -15,12 +16,8 @@ in float v_stroke_width[3]; in vec4 v_color[3]; out vec4 color; -out float uv_stroke_width; -out float uv_anti_alias_width; - -out float is_linear; - -out vec2 uv_coords; +out float scaled_anti_alias_width; +out float scaled_signed_dist_to_curve; // Codes for joint types const int NO_JOINT = 0; @@ -31,179 +28,207 @@ const int MITER_JOINT = 3; // When the cosine of the angle between // two vectors is larger than this, we // consider them aligned -const float COS_THRESHOLD = 0.99; - -vec3 unit_normal = vec3(0.0, 0.0, 1.0); +const float COS_THRESHOLD = 0.999; +// Used to determine how many lines to break the curve into +const float POLYLINE_FACTOR = 100; +const int MAX_STEPS = 32; +const float MITER_COS_ANGLE_THRESHOLD = -0.8; #INSERT emit_gl_Position.glsl -#INSERT get_xyz_to_uv.glsl #INSERT finalize_color.glsl vec3 get_joint_unit_normal(vec4 joint_product){ - vec3 result = (joint_product.w < COS_THRESHOLD) ? - joint_product.xyz : v_joint_product[1].xyz; - float norm = length(result); - return (norm > 1e-5) ? result / norm : vec3(0.0, 0.0, 1.0); + float tol = 1e-8; + if (length(joint_product.xyz) > tol){ + return normalize(joint_product.xyz); + } + if (length(v_joint_product[1].xyz) > tol){ + return normalize(v_joint_product[1].xyz); + } + return vec3(0.0, 0.0, 1.0); } -vec4 normalized_joint_product(vec4 joint_product){ +vec4 unit_joint_product(vec4 joint_product){ + float tol = 1e-8; float norm = length(joint_product); - return (norm > 1e-10) ? joint_product / norm : vec4(0.0, 0.0, 0.0, 1.0); + return (norm < tol) ? vec4(0.0, 0.0, 0.0, 1.0) : joint_product / norm; } -void create_joint( - vec4 joint_product, - vec3 unit_tan, - float buff, - vec3 static_c0, - out vec3 changing_c0, - vec3 static_c1, - out vec3 changing_c1 -){ - float cos_angle = joint_product.w; - if(abs(cos_angle) > COS_THRESHOLD || int(joint_type) == NO_JOINT){ - // No joint - changing_c0 = static_c0; - changing_c1 = static_c1; - return; - } - - float shift; - float sin_angle = length(joint_product.xyz) * sign(joint_product.z); - if(int(joint_type) == MITER_JOINT){ - shift = buff * (-1.0 - cos_angle) / sin_angle; - }else{ - // For a Bevel joint - shift = buff * (1.0 - cos_angle) / sin_angle; - } - changing_c0 = static_c0 - shift * unit_tan; - changing_c1 = static_c1 + shift * unit_tan; +vec3 point_on_quadratic(float t, vec3 c0, vec3 c1, vec3 c2){ + return c0 + c1 * t + c2 * t * t; } -vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw){ - /* - Perpendicular vectors to the left of the curve + +vec3 tangent_on_quadratic(float t, vec3 c1, vec3 c2){ + return c1 + 2 * c2 * t; +} + + +vec4 get_joint_product(vec3 v1, vec3 v2){ + return vec4(cross(v1, v2), dot(v1, v2)); +} + + +vec3 project(vec3 vect, vec3 unit_normal){ + /* Project the vector onto the plane perpendicular to a given unit normal */ + return vect - dot(vect, unit_normal) * unit_normal; +} + +vec3 inverse_vector_product(vec3 vect, vec3 cross_product, float dot_product){ + /* + Suppose cross(v1, v2) = cross_product and dot(v1, v2) = dot_product. + Given v1, this function return v2. */ - float buff = 0.5 * v_stroke_width[index] + aaw; - // Add correction for sharp angles to prevent weird bevel effects - if(joint_product.w < -0.75) buff *= 4 * (joint_product.w + 1.0); - vec3 normal = get_joint_unit_normal(joint_product); - // Set global unit normal - unit_normal = normal; - // Choose the "outward" normal direction - if(normal.z < 0) normal *= -1; - if(bool(flat_stroke)){ - return buff * normalize(cross(normal, tangent)); - }else{ - return buff * normalize(cross(camera_position - point, tangent)); - } + return (vect * dot_product - cross(vect, cross_product)) / dot(vect, vect); } -// This function is responsible for finding the corners of -// a bounding region around the bezier curve, which can be -// emitted as a triangle fan, with vertices vaguely close -// to control points so that the passage of vert data to -// frag shaders is most natural. -void get_corners( - // Control points for a bezier curve - vec3 p0, - vec3 p1, - vec3 p2, - // Unit tangent vectors at p0 and p2 - vec3 v01, - vec3 v12, - // Anti-alias width - float aaw, - out vec3 corners[6] + +vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_product, bool inside_curve){ + /* + Step the the left of a curve. + First a perpendicular direction is calculated, then it is adjusted + so as to make a joint. + */ + vec3 unit_tan = normalize(flat_stroke == 0.0 ? project(tangent, unit_normal) : tangent); + + // Step to stroke width bound should be perpendicular + // both to the tangent and the normal direction + vec3 step = normalize(cross(unit_normal, unit_tan)); + + // For non-flat stroke, there can be glitches when the tangent direction + // lines up very closely with the direction to the camera, treated here + // as the unit normal. To avoid those, this smoothly transitions to a step + // direction perpendicular to the true curve normal. + float alignment = abs(dot(normalize(tangent), unit_normal)); + float alignment_threshold = 0.97; // This could maybe be chosen in a more principled way based on stroke width + if (alignment > alignment_threshold) { + vec3 perp = normalize(cross(get_joint_unit_normal(joint_product), tangent)); + step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment)); + } + + if (inside_curve || int(joint_type) == NO_JOINT) return step; + + vec4 unit_jp = unit_joint_product(joint_product); + float cos_angle = unit_jp.w; + + if (cos_angle > COS_THRESHOLD) return step; + + // Below here, figure out the adjustment to bevel or miter a joint + if (flat_stroke == 0){ + // Figure out what joint product would be for everything projected onto + // the plane perpendicular to the normal direction (which here would be to_camera) + step = normalize(cross(unit_normal, unit_tan)); // Back to original step + vec3 adj_tan = inverse_vector_product(tangent, unit_jp.xyz, unit_jp.w); + adj_tan = project(adj_tan, unit_normal); + vec4 flat_jp = get_joint_product(unit_tan, adj_tan); + cos_angle = unit_joint_product(flat_jp).w; + } + + // If joint type is auto, it will bevel for cos(angle) > MITER_COS_ANGLE_THRESHOLD, + // and smoothly transition to miter for those with sharper angles + float miter_factor; + if (joint_type == BEVEL_JOINT){ + miter_factor = 0.0; + }else if (joint_type == MITER_JOINT){ + miter_factor = 1.0; + }else { + float mcat1 = MITER_COS_ANGLE_THRESHOLD; + float mcat2 = 0.5 * (mcat1 - 1.0); + miter_factor = smoothstep(mcat1, mcat2, cos_angle); + } + + float sin_angle = sqrt(1 - cos_angle * cos_angle) * sign(dot(joint_product.xyz, unit_normal)); + float shift = (cos_angle + mix(-1, 1, miter_factor)) / sin_angle; + + return step + shift * unit_tan; +} + + +void emit_point_with_width( + vec3 point, + vec3 tangent, + vec4 joint_product, + float width, + vec4 joint_color, + bool inside_curve ){ - bool linear = bool(is_linear); - vec4 jp0 = normalized_joint_product(v_joint_product[0]); - vec4 jp2 = normalized_joint_product(v_joint_product[2]); - vec3 p0_perp = get_perp(0, jp0, p0, v01, aaw); - vec3 p2_perp = get_perp(2, jp2, p2, v12, aaw); - vec3 p1_perp = 0.5 * (p0_perp + p2_perp); - if(linear){ - p1_perp *= (0.5 * v_stroke_width[1] + aaw) / length(p1_perp); + // Find unit normal + vec3 to_camera = camera_position - point; + vec3 unit_normal; + if (flat_stroke == 0.0){ + unit_normal = normalize(to_camera); + }else{ + unit_normal = get_joint_unit_normal(joint_product); + unit_normal *= sign(dot(unit_normal, to_camera)); // Choose the "outward" normal direction } - // The order of corners should be for a triangle_strip. - vec3 c0 = p0 + p0_perp; - vec3 c1 = p0 - p0_perp; - vec3 c2 = p1 + p1_perp; - vec3 c3 = p1 - p1_perp; - vec3 c4 = p2 + p2_perp; - vec3 c5 = p2 - p2_perp; - // Move the inner middle control point to make - // room for the curve - // float orientation = dot(unit_normal, v_joint_product[1].xyz); - float orientation = v_joint_product[1].z; - if(!linear && orientation >= 0.0) c2 = 0.5 * (c0 + c4); - else if(!linear && orientation < 0.0) c3 = 0.5 * (c1 + c5); + // Set styling + color = finalize_color(joint_color, point, unit_normal); + scaled_anti_alias_width = (width == 0) ? + -1.0 : // Signal to discard in the frag shader + 2.0 * anti_alias_width * pixel_size / width; - // Account for previous and next control points - if(bool(flat_stroke)){ - create_joint(jp0, v01, length(p0_perp), c1, c1, c0, c0); - create_joint(jp2, -v12, length(p2_perp), c5, c5, c4, c4); + // Figure out the step from the point to the corners of the + // triangle strip around the polyline + vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inside_curve); + + // Emit two corners + // The frag shader will receive a value from -1 to 1, + // reflecting where in the stroke that point is + for (int sign = -1; sign <= 1; sign += 2){ + scaled_signed_dist_to_curve = sign; + emit_gl_Position(point + 0.5 * width * sign * step); + EmitVertex(); } - - corners = vec3[6](c0, c1, c2, c3, c4, c5); } + void main() { // Curves are marked as ended when the handle after // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - vec3 p0 = verts[0]; - vec3 p1 = verts[1]; - vec3 p2 = verts[2]; - vec3 v01 = normalize(p1 - p0); - vec3 v12 = normalize(p2 - p1); + // Coefficients such that the quadratic bezier is c0 + c1 * t + c2 * t^2 + vec3 c0 = verts[0]; + vec3 c1 = 2 * (verts[1] - verts[0]); + vec3 c2 = verts[0] - 2 * verts[1] + verts[2]; + // Estimate how many line segment the curve should be divided into + // based on the area of the triangle defined by these control points + float area = 0.5 * length(v_joint_product[1].xzy); + int count = int(round(POLYLINE_FACTOR * sqrt(area) / frame_scale)); + int n_steps = min(2 + count, MAX_STEPS); - vec4 jp1 = normalized_joint_product(v_joint_product[1]); - is_linear = float(jp1.w > COS_THRESHOLD); + // Emit vertex pairs aroudn subdivided points + for (int i = 0; i < MAX_STEPS; i++){ + if (i >= n_steps) break; + float t = float(i) / (n_steps - 1); - // We want to change the coordinates to a space where the curve - // coincides with y = x^2, between some values x0 and x2. Or, in - // the case of a linear curve just put it on the x-axis - mat4 xyz_to_uv; - float uv_scale_factor; - if(!bool(is_linear)){ - bool too_steep; - xyz_to_uv = get_xyz_to_uv(p0, p1, p2, 2.0, too_steep); - is_linear = float(too_steep); - uv_scale_factor = length(xyz_to_uv[0].xyz); - } + // Point and tangent + vec3 point = point_on_quadratic(t, c0, c1, c2); + vec3 tangent = tangent_on_quadratic(t, c1, c2); - float scaled_aaw = anti_alias_width * pixel_size; - vec3 corners[6]; - get_corners(p0, p1, p2, v01, v12, scaled_aaw, corners); + // Style + float stroke_width = mix(v_stroke_width[0], v_stroke_width[2], t); + vec4 color = mix(v_color[0], v_color[2], t); - // Emit each corner - float max_sw = max(v_stroke_width[0], v_stroke_width[2]); - for(int i = 0; i < 6; i++){ - float stroke_width = v_stroke_width[i / 2]; + // This is sent along to prevent needless joint creation + bool inside_curve = (i > 0 && i < n_steps - 1); - if(bool(is_linear)){ - float sign = vec2(-1, 1)[i % 2]; - // In this case, we only really care about - // the v coordinate - uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw)); - uv_anti_alias_width = scaled_aaw; - uv_stroke_width = stroke_width; - }else{ - uv_coords = (xyz_to_uv * vec4(corners[i], 1.0)).xy; - uv_stroke_width = uv_scale_factor * stroke_width; - uv_anti_alias_width = uv_scale_factor * scaled_aaw; - } + // Use middle joint product for inner points, flip sign for first one's cross product component + vec4 joint_product; + if (i == 0) joint_product = v_joint_product[0] * vec4(-1, -1, -1, 1); + else if (inside_curve) joint_product = v_joint_product[1]; + else joint_product = v_joint_product[2]; - color = finalize_color(v_color[i / 2], corners[i], unit_normal); - emit_gl_Position(corners[i]); - EmitVertex(); + emit_point_with_width( + point, tangent, joint_product, + stroke_width, color, + inside_curve + ); } EndPrimitive(); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index ea290bfa..6bd488d5 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -6,7 +6,6 @@ uniform float is_fixed_in_frame; in vec3 point; in vec4 stroke_rgba; in float stroke_width; -in vec3 joint_normal; in vec4 joint_product; // Bezier control point @@ -16,12 +15,11 @@ out vec4 v_joint_product; out float v_stroke_width; out vec4 v_color; -const float STROKE_WIDTH_CONVERSION = 0.01; +const float STROKE_WIDTH_CONVERSION = 0.015; void main(){ verts = point; - v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width; - v_stroke_width *= mix(frame_scale, 1, is_fixed_in_frame); + v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame); v_joint_product = joint_product; v_color = stroke_rgba; } \ No newline at end of file diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index accf0012..779210a8 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -198,7 +198,9 @@ def approx_smooth_quadratic_bezier_handles( another that would produce a parabola passing through P0, call it smooth_to_left, and use the midpoint between the two. """ - if len(points) == 2: + if len(points) == 1: + return points[0] + elif len(points) == 2: return midpoint(*points) smooth_to_right, smooth_to_left = [ 0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:]