From 9475fcd19eaaf2eff564368cd4468f8b93b275ef Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 8 Nov 2024 14:27:20 -0600 Subject: [PATCH 01/18] Have clip plane recurse through family --- manimlib/mobject/mobject.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index a8ebcba4..b95acdbb 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1950,12 +1950,14 @@ class Mobject(object): def set_clip_plane( self, vect: Vect3 | None = None, - threshold: float | None = None + threshold: float | None = None, + recurse=True ) -> Self: - if vect is not None: - self.uniforms["clip_plane"][:3] = vect - if threshold is not None: - self.uniforms["clip_plane"][3] = threshold + for submob in self.get_family(recurse): + if vect is not None: + submob.uniforms["clip_plane"][:3] = vect + if threshold is not None: + submob.uniforms["clip_plane"][3] = threshold return self def deactivate_clip_plane(self) -> Self: From b84376d6fd08435f5ab7b55c0592e3232f822c4f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 8 Nov 2024 14:28:17 -0600 Subject: [PATCH 02/18] Add Cone --- manimlib/mobject/three_dimensions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index d29b7ec2..b0fd4b47 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -169,6 +169,20 @@ class Cylinder(Surface): return np.array([np.cos(u), np.sin(u), v]) +class Cone(Cylinder): + def __init__( + self, + u_range: Tuple[float, float] = (0, TAU), + v_range: Tuple[float, float] = (0, 1), + *args, + **kwargs, + ): + super().__init__(u_range=u_range, v_range=v_range, *args, **kwargs) + + def uv_func(self, u: float, v: float) -> np.ndarray: + return np.array([(1 - v) * np.cos(u), (1 - v) * np.sin(u), v]) + + class Line3D(Cylinder): def __init__( self, From 64ae1364ca25550873c5cc37e2ef30756de9469d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 12 Nov 2024 11:21:19 -0800 Subject: [PATCH 03/18] Update the Vector Field interface --- manimlib/mobject/coordinate_systems.py | 12 ++- manimlib/mobject/vector_field.py | 127 ++++++++++++++----------- manimlib/utils/color.py | 31 +++++- 3 files changed, 108 insertions(+), 62 deletions(-) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 38bab433..6863fee5 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -45,6 +45,12 @@ DEFAULT_X_RANGE = (-8.0, 8.0, 1.0) DEFAULT_Y_RANGE = (-4.0, 4.0, 1.0) +def full_range_specifier(range_args): + if len(range_args) == 2: + return (*range_args, 1) + return range_args + + class CoordinateSystem(ABC): """ Abstract class for Axes and NumberPlane @@ -57,8 +63,8 @@ class CoordinateSystem(ABC): y_range: RangeSpecifier = DEFAULT_Y_RANGE, num_sampled_graph_points_per_tick: int = 5, ): - self.x_range = x_range - self.y_range = y_range + self.x_range = full_range_specifier(x_range) + self.y_range = full_range_specifier(y_range) self.num_sampled_graph_points_per_tick = num_sampled_graph_points_per_tick @abstractmethod @@ -536,7 +542,7 @@ class ThreeDAxes(Axes): ): Axes.__init__(self, x_range, y_range, **kwargs) - self.z_range = z_range + self.z_range = full_range_specifier(z_range) self.z_axis = self.create_axis( self.z_range, axis_config=merge_dicts_recursively( diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index df784190..0a1eb94a 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -15,6 +15,7 @@ from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.color import get_colormap_list from manimlib.utils.color import rgb_to_color +from manimlib.utils.color import get_color_map from manimlib.utils.dict_ops import merge_dicts_recursively from manimlib.utils.iterables import cartesian_product from manimlib.utils.rate_functions import linear @@ -25,7 +26,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, Iterable, Sequence, TypeVar, Tuple - from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array + from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array, Vect4Array from manimlib.mobject.coordinate_systems import CoordinateSystem from manimlib.mobject.mobject import Mobject @@ -33,6 +34,7 @@ if TYPE_CHECKING: T = TypeVar("T") +#### Delete these two ### def get_vectorized_rgb_gradient_function( min_value: T, max_value: T, @@ -52,6 +54,7 @@ def get_vectorized_rgb_gradient_function( inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3)) result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas) return result + return func @@ -62,6 +65,7 @@ def get_rgb_gradient_function( ) -> Callable[[float], Vect3]: vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map) return lambda value: vectorized_func(np.array([value]))[0] +#### def move_along_vector_field( @@ -106,16 +110,16 @@ def move_points_along_vector_field( return mobject -def get_sample_points_from_coordinate_system( +def get_sample_coords( coordinate_system: CoordinateSystem, - step_multiple: float + step_multiple: float = 1.0 ) -> it.product[tuple[Vect3, ...]]: ranges = [] for range_args in coordinate_system.get_all_ranges(): _min, _max, step = range_args step *= step_multiple ranges.append(np.arange(_min, _max + step, step)) - return it.product(*ranges) + return np.array(list(it.product(*ranges))) # Mobjects @@ -124,51 +128,53 @@ def get_sample_points_from_coordinate_system( class VectorField(VMobject): def __init__( self, - func, + func: Callable[Sequence[float], Sequence[float]], + coordinate_system: CoordinateSystem, + step_multiple: float = 0.5, + magnitude_range: Optional[Tuple[float, float]] = None, + color_map_name: Optional[str] = "3b1b_colormap", + color_map: Optional[Callable[Sequence[float]], Vect4Array] = None, stroke_color: ManimColor = BLUE, stroke_opacity: float = 1.0, - center: Vect3 = ORIGIN, - sample_points: Optional[Vect3Array] = None, - x_density: float = 2.0, - y_density: float = 2.0, - z_density: float = 2.0, - width: float = 14.0, - height: float = 8.0, - depth: float = 0.0, stroke_width: float = 2, tip_width_ratio: float = 4, tip_len_to_width: float = 0.01, max_vect_len: float | None = None, - min_drawn_norm: float = 1e-2, flat_stroke: bool = False, - norm_to_opacity_func=None, - norm_to_rgb_func=None, + norm_to_opacity_func=None, # TODO, check on this **kwargs ): self.func = func + self.coordinate_system = coordinate_system self.stroke_width = stroke_width self.tip_width_ratio = tip_width_ratio self.tip_len_to_width = tip_len_to_width - self.min_drawn_norm = min_drawn_norm self.norm_to_opacity_func = norm_to_opacity_func - self.norm_to_rgb_func = norm_to_rgb_func - if max_vect_len is not None: - self.max_vect_len = max_vect_len + # Search for sample_points + self.sample_coords = get_sample_coords(coordinate_system, step_multiple) + self.update_sample_points() + + if max_vect_len is None: + self.max_displayed_vect_len = get_norm(self.sample_points[1] - self.sample_points[0]) else: - densities = np.array([x_density, y_density, z_density]) - dims = np.array([width, height, depth]) - self.max_vect_len = 1.0 / densities[dims > 0].mean() + self.max_displayed_vect_len = max_vect_len * coordinate_system.get_x_unit_size() - if sample_points is None: - self.sample_points = self.get_sample_points( - center, width, height, depth, - x_density, y_density, z_density - ) + # Prepare the color map + if magnitude_range is None: + max_value = max(map(get_norm, func(self.sample_coords))) + magnitude_range = (0, max_value) + + self.magnitude_range = magnitude_range + + if color_map is not None: + self.color_map = color_map + elif color_map_name is not None: + self.color_map = get_color_map(color_map_name) else: - self.sample_points = sample_points + self.color_map = None - self.init_base_stroke_width_array(len(self.sample_points)) + self.init_base_stroke_width_array(len(self.sample_coords)) super().__init__( stroke_color=stroke_color, @@ -177,7 +183,7 @@ class VectorField(VMobject): **kwargs ) - n_samples = len(self.sample_points) + n_samples = len(self.sample_coords) self.set_points(np.zeros((8 * n_samples - 1, 3))) self.set_stroke(width=stroke_width) self.set_joint_type('no_joint') @@ -211,8 +217,8 @@ class VectorField(VMobject): arr[7::8] = 0 self.base_stroke_width_array = arr - def set_sample_points(self, sample_points: Vect3Array): - self.sample_points = sample_points + def set_sample_coords(self, sample_points: VectArray): + self.sample_coords = sample_coords return self def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True): @@ -227,35 +233,40 @@ class VectorField(VMobject): self.stroke_width = width return self + def update_sample_points(self): + self.sample_points = self.coordinate_system.c2p(*self.sample_coords.T) + def update_vectors(self): tip_width = self.tip_width_ratio * self.stroke_width tip_len = self.tip_len_to_width * tip_width - samples = self.sample_points - # Get raw outputs and lengths - outputs = self.func(samples) - norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis] + # Outputs in the coordinate system + outputs = self.func(self.sample_coords) + output_norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis] - # How long should the arrows be drawn? - max_len = self.max_vect_len + # Corresponding vector values in global coordinates + out_vects = self.coordinate_system.c2p(*outputs.T) - self.coordinate_system.get_origin() + out_vect_norms = np.linalg.norm(out_vects, axis=1)[:, np.newaxis] + unit_outputs = np.zeros_like(out_vects) + np.true_divide(out_vects, out_vect_norms, out=unit_outputs, where=(out_vect_norms > 0)) + + # How long should the arrows be drawn, in global coordinates + max_len = self.max_displayed_vect_len if max_len < np.inf: - drawn_norms = max_len * np.tanh(norms / max_len) + drawn_norms = max_len * np.tanh(out_vect_norms / max_len) else: - drawn_norms = norms + drawn_norms = out_vect_norms # What's the distance from the base of an arrow to # the base of its head? - dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf) + dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf) # Mixing units! # Set all points - unit_outputs = np.zeros_like(outputs) - np.true_divide(outputs, norms, out=unit_outputs, where=(norms > self.min_drawn_norm)) - points = self.get_points() - points[0::8] = samples - points[2::8] = samples + dist_to_head_base * unit_outputs + points[0::8] = self.sample_points + points[2::8] = self.sample_points + dist_to_head_base * unit_outputs points[4::8] = points[2::8] - points[6::8] = samples + drawn_norms * unit_outputs + points[6::8] = self.sample_points + drawn_norms * unit_outputs for i in (1, 3, 5): points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8]) points[7::8] = points[6:-1:8] @@ -267,14 +278,16 @@ class VectorField(VMobject): self.get_stroke_widths()[:] = width_scalars * width_arr # Potentially adjust opacity and color + if self.color_map is not None: + self.get_stroke_colors() # Ensures the array is updated to appropriate length + low, high = self.magnitude_range + self.data['stroke_rgba'][:] = self.color_map( + inverse_interpolate(low, high, np.repeat(output_norms, 8)[:-1]) + ) + if self.norm_to_opacity_func is not None: self.get_stroke_opacities()[:] = self.norm_to_opacity_func( - np.repeat(norms, 8)[:-1] - ) - if self.norm_to_rgb_func is not None: - self.get_stroke_colors() - self.data['stroke_rgba'][:, :3] = self.norm_to_rgb_func( - np.repeat(norms, 8)[:-1] + np.repeat(output_norms, 8)[:-1] ) self.note_changed_data() @@ -321,11 +334,11 @@ class OldVectorField(VGroup): self.opacity = opacity self.vector_config = dict(vector_config) - self.value_to_rgb = get_rgb_gradient_function( + self.value_to_rgb = get_vectorized_rgb_gradient_function( *self.magnitude_range, self.color_map, ) - samples = get_sample_points_from_coordinate_system( + samples = get_sample_coords( coordinate_system, self.step_multiple ) self.add(*( @@ -438,7 +451,7 @@ class StreamLines(VGroup): def get_start_points(self) -> Vect3Array: cs = self.coordinate_system - sample_coords = get_sample_points_from_coordinate_system( + sample_coords = get_sample_coords( cs, self.step_multiple, ) diff --git a/manimlib/utils/color.py b/manimlib/utils/color.py index f9ceb21a..a511375e 100644 --- a/manimlib/utils/color.py +++ b/manimlib/utils/color.py @@ -14,8 +14,8 @@ from manimlib.utils.iterables import resize_with_interpolation from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Iterable, Sequence - from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array + from typing import Iterable, Sequence, Callable + from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array, NDArray def color_to_rgb(color: ManimColor) -> Vect3: @@ -134,6 +134,33 @@ def random_bright_color( )) +def get_colormap_from_colors(colors: Iterable[ManimColor]) -> Callable[[Sequence[float]], Vect4Array]: + """ + Returns a funciton which takes in values between 0 and 1, and returns + a corresponding list of rgba values + """ + rgbas = np.array([color_to_rgba(color) for color in colors]) + + def func(values): + alphas = np.clip(values, 0, 1) + scaled_alphas = alphas * (len(rgbas) - 1) + indices = scaled_alphas.astype(int) + next_indices = np.clip(indices + 1, 0, len(rgbas) - 1) + inter_alphas = scaled_alphas % 1 + inter_alphas = inter_alphas.repeat(4).reshape((len(indices), 4)) + result = interpolate(rgbas[indices], rgbas[next_indices], inter_alphas) + return result + + return func + + +def get_color_map(map_name: str) -> Callable[[Sequence[float]], Vect4Array]: + if map_name == "3b1b_colormap": + return get_colormap_from_colors(COLORMAP_3B1B) + return plt.get_cmap(map_name) + + +# Delete this? def get_colormap_list( map_name: str = "viridis", n_colors: int = 9 From 0ad5a0e76e8c075380b94be753efd6f1eb47bf65 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 15 Nov 2024 09:07:46 -0800 Subject: [PATCH 04/18] Further development on VectorField --- manimlib/mobject/vector_field.py | 332 ++++++++++++++++--------------- 1 file changed, 175 insertions(+), 157 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 0a1eb94a..1c882ba6 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -3,6 +3,7 @@ from __future__ import annotations import itertools as it import numpy as np +from scipy.integrate import solve_ivp from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH from manimlib.constants import BLUE, WHITE @@ -26,7 +27,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, Iterable, Sequence, TypeVar, Tuple - from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array, Vect4Array + from manimlib.typing import ManimColor, Vect3, VectN, Vect2Array, Vect3Array, Vect4Array from manimlib.mobject.coordinate_systems import CoordinateSystem from manimlib.mobject.mobject import Mobject @@ -68,6 +69,16 @@ def get_rgb_gradient_function( #### +def ode_solution_points(function, state0, time, dt=0.01): + solution = solve_ivp( + lambda t, state: function(state), + t_span=(0, time), + y0=state0, + t_eval=np.arange(0, time, dt) + ) + return solution.y.T + + def move_along_vector_field( mobject: Mobject, func: Callable[[Vect3], Vect3] @@ -128,12 +139,12 @@ def get_sample_coords( class VectorField(VMobject): def __init__( self, - func: Callable[Sequence[float], Sequence[float]], + func: Callable[[VectArray], VectArray], coordinate_system: CoordinateSystem, step_multiple: float = 0.5, magnitude_range: Optional[Tuple[float, float]] = None, color_map_name: Optional[str] = "3b1b_colormap", - color_map: Optional[Callable[Sequence[float]], Vect4Array] = None, + color_map: Optional[Callable[[Sequence[float]], Vect4Array]] = None, stroke_color: ManimColor = BLUE, stroke_opacity: float = 1.0, stroke_width: float = 2, @@ -281,9 +292,9 @@ class VectorField(VMobject): if self.color_map is not None: self.get_stroke_colors() # Ensures the array is updated to appropriate length low, high = self.magnitude_range - self.data['stroke_rgba'][:] = self.color_map( + self.data['stroke_rgba'][:, :3] = self.color_map( inverse_interpolate(low, high, np.repeat(output_norms, 8)[:-1]) - ) + )[:, :3] if self.norm_to_opacity_func is not None: self.get_stroke_opacities()[:] = self.norm_to_opacity_func( @@ -310,6 +321,165 @@ class TimeVaryingVectorField(VectorField): self.time += dt +class StreamLines(VGroup): + def __init__( + self, + func: Callable[[VectArray], VectArray], + coordinate_system: CoordinateSystem, + step_multiple: float = 0.5, + n_repeats: int = 1, + noise_factor: float | None = None, + # Config for drawing lines + solution_time: float = 3, + dt: float = 0.05, + arc_len: float = 3, + max_time_steps: int = 200, + n_samples_per_line: int = 10, + cutoff_norm: float = 15, + # Style info + stroke_width: float = 1.0, + stroke_color: ManimColor = WHITE, + stroke_opacity: float = 1, + color_by_magnitude: bool = True, + magnitude_range: Tuple[float, float] = (0, 2.0), + taper_stroke_width: bool = False, + color_map: str = "3b1b_colormap", + **kwargs + ): + super().__init__(**kwargs) + self.func = func + self.coordinate_system = coordinate_system + self.step_multiple = step_multiple + self.n_repeats = n_repeats + self.noise_factor = noise_factor + self.solution_time = solution_time + self.dt = dt + self.arc_len = arc_len + self.max_time_steps = max_time_steps + self.n_samples_per_line = n_samples_per_line + self.cutoff_norm = cutoff_norm + self.stroke_width = stroke_width + self.stroke_color = stroke_color + self.stroke_opacity = stroke_opacity + self.color_by_magnitude = color_by_magnitude + self.magnitude_range = magnitude_range + self.taper_stroke_width = taper_stroke_width + self.color_map = color_map + + self.draw_lines() + self.init_style() + + def point_func(self, points: Vect3Array) -> Vect3: + in_coords = np.array(self.coordinate_system.p2c(points)).T + out_coords = self.func(in_coords) + origin = self.coordinate_system.get_origin() + return self.coordinate_system.c2p(*out_coords.T) - origin + + def draw_lines(self) -> None: + lines = [] + origin = self.coordinate_system.get_origin() + + # Todo, it feels like coordinate system should just have + # the ODE solver built into it, no? + lines = [] + for coords in self.get_sample_coords(): + solution_coords = ode_solution_points(self.func, coords, self.solution_time, self.dt) + line = VMobject() + line.set_points_smoothly(self.coordinate_system.c2p(*solution_coords.T)) + # TODO, account for arc length somehow? + line.virtual_time = self.solution_time + lines.append(line) + self.set_submobjects(lines) + + def get_sample_coords(self): + cs = self.coordinate_system + sample_coords = get_sample_coords(cs, self.step_multiple) + + noise_factor = self.noise_factor + if noise_factor is None: + noise_factor = cs.get_x_unit_size() * self.step_multiple * 0.5 + + return np.array([ + coords + noise_factor * np.random.random(coords.shape) + for n in range(self.n_repeats) + for coords in sample_coords + ]) + + def get_start_points(self) -> Vect3Array: + cs = self.coordinate_system + sample_coords = get_sample_coords(cs, self.step_multiple) + + noise_factor = self.noise_factor + if noise_factor is None: + noise_factor = cs.get_x_unit_size() * self.step_multiple * 0.5 + + return np.array([ + cs.c2p(*coords) + noise_factor * np.random.random(3) + for n in range(self.n_repeats) + for coords in sample_coords + ]) + + def init_style(self) -> None: + if self.color_by_magnitude: + values_to_rgbs = get_vectorized_rgb_gradient_function( + *self.magnitude_range, self.color_map, + ) + cs = self.coordinate_system + for line in self.submobjects: + norms = [ + get_norm(self.func(*cs.p2c(point))) + for point in line.get_points() + ] + rgbs = values_to_rgbs(norms) + rgbas = np.zeros((len(rgbs), 4)) + rgbas[:, :3] = rgbs + rgbas[:, 3] = self.stroke_opacity + line.set_rgba_array(rgbas, "stroke_rgba") + else: + self.set_stroke(self.stroke_color, opacity=self.stroke_opacity) + + if self.taper_stroke_width: + width = [0, self.stroke_width, 0] + else: + width = self.stroke_width + self.set_stroke(width=width) + + +class AnimatedStreamLines(VGroup): + def __init__( + self, + stream_lines: StreamLines, + lag_range: float = 4, + rate_multiple: float = 1.0, + line_anim_config: dict = dict( + rate_func=linear, + time_width=1.0, + ), + **kwargs + ): + super().__init__(**kwargs) + self.stream_lines = stream_lines + + for line in stream_lines: + line.anim = VShowPassingFlash( + line, + run_time=line.virtual_time / rate_multiple, + **line_anim_config, + ) + line.anim.begin() + line.time = -lag_range * np.random.random() + self.add(line.anim.mobject) + + self.add_updater(lambda m, dt: m.update(dt)) + + def update(self, dt: float) -> None: + stream_lines = self.stream_lines + for line in stream_lines: + line.time += dt + adjusted_time = max(line.time, 0) % line.anim.run_time + line.anim.update(adjusted_time / line.anim.run_time) + + class OldVectorField(VGroup): def __init__( self, @@ -371,155 +541,3 @@ class OldVectorField(VGroup): opacity=self.opacity, ) return vect - - -class StreamLines(VGroup): - def __init__( - self, - func: Callable[[float, float], Sequence[float]], - coordinate_system: CoordinateSystem, - step_multiple: float = 0.5, - n_repeats: int = 1, - noise_factor: float | None = None, - # Config for drawing lines - dt: float = 0.05, - arc_len: float = 3, - max_time_steps: int = 200, - n_samples_per_line: int = 10, - cutoff_norm: float = 15, - # Style info - stroke_width: float = 1.0, - stroke_color: ManimColor = WHITE, - stroke_opacity: float = 1, - color_by_magnitude: bool = True, - magnitude_range: Tuple[float, float] = (0, 2.0), - taper_stroke_width: bool = False, - color_map: str = "3b1b_colormap", - **kwargs - ): - super().__init__(**kwargs) - self.func = func - self.coordinate_system = coordinate_system - self.step_multiple = step_multiple - self.n_repeats = n_repeats - self.noise_factor = noise_factor - self.dt = dt - self.arc_len = arc_len - self.max_time_steps = max_time_steps - self.n_samples_per_line = n_samples_per_line - self.cutoff_norm = cutoff_norm - self.stroke_width = stroke_width - self.stroke_color = stroke_color - self.stroke_opacity = stroke_opacity - self.color_by_magnitude = color_by_magnitude - self.magnitude_range = magnitude_range - self.taper_stroke_width = taper_stroke_width - self.color_map = color_map - - self.draw_lines() - self.init_style() - - def point_func(self, point: Vect3) -> Vect3: - in_coords = self.coordinate_system.p2c(point) - out_coords = self.func(*in_coords) - return self.coordinate_system.c2p(*out_coords) - - def draw_lines(self) -> None: - lines = [] - origin = self.coordinate_system.get_origin() - for point in self.get_start_points(): - points = [point] - total_arc_len = 0 - time = 0 - for x in range(self.max_time_steps): - time += self.dt - last_point = points[-1] - new_point = last_point + self.dt * (self.point_func(last_point) - origin) - points.append(new_point) - total_arc_len += get_norm(new_point - last_point) - if get_norm(last_point) > self.cutoff_norm: - break - if total_arc_len > self.arc_len: - break - line = VMobject() - line.virtual_time = time - step = max(1, int(len(points) / self.n_samples_per_line)) - line.set_points_as_corners(points[::step]) - line.make_smooth(approx=True) - lines.append(line) - self.set_submobjects(lines) - - def get_start_points(self) -> Vect3Array: - cs = self.coordinate_system - sample_coords = get_sample_coords( - cs, self.step_multiple, - ) - - noise_factor = self.noise_factor - if noise_factor is None: - noise_factor = cs.x_range[2] * self.step_multiple * 0.5 - - return np.array([ - cs.c2p(*coords) + noise_factor * np.random.random(3) - for n in range(self.n_repeats) - for coords in sample_coords - ]) - - def init_style(self) -> None: - if self.color_by_magnitude: - values_to_rgbs = get_vectorized_rgb_gradient_function( - *self.magnitude_range, self.color_map, - ) - cs = self.coordinate_system - for line in self.submobjects: - norms = [ - get_norm(self.func(*cs.p2c(point))) - for point in line.get_points() - ] - rgbs = values_to_rgbs(norms) - rgbas = np.zeros((len(rgbs), 4)) - rgbas[:, :3] = rgbs - rgbas[:, 3] = self.stroke_opacity - line.set_rgba_array(rgbas, "stroke_rgba") - else: - self.set_stroke(self.stroke_color, opacity=self.stroke_opacity) - - if self.taper_stroke_width: - width = [0, self.stroke_width, 0] - else: - width = self.stroke_width - self.set_stroke(width=width) - - -class AnimatedStreamLines(VGroup): - def __init__( - self, - stream_lines: StreamLines, - lag_range: float = 4, - line_anim_config: dict = dict( - rate_func=linear, - time_width=1.0, - ), - **kwargs - ): - super().__init__(**kwargs) - self.stream_lines = stream_lines - - for line in stream_lines: - line.anim = VShowPassingFlash( - line, - run_time=line.virtual_time, - **line_anim_config, - ) - line.anim.begin() - line.time = -lag_range * np.random.random() - self.add(line.anim.mobject) - - self.add_updater(lambda m, dt: m.update(dt)) - - def update(self, dt: float) -> None: - stream_lines = self.stream_lines - for line in stream_lines: - line.time += dt - adjusted_time = max(line.time, 0) % line.anim.run_time - line.anim.update(adjusted_time / line.anim.run_time) From e80b9d0e47fd2078630a6a77836ecc29988ab6e7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 09:31:15 -0700 Subject: [PATCH 05/18] Less collision-prone file names for downloads --- manimlib/utils/file_ops.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/manimlib/utils/file_ops.py b/manimlib/utils/file_ops.py index e1419e91..0e436b68 100644 --- a/manimlib/utils/file_ops.py +++ b/manimlib/utils/file_ops.py @@ -1,6 +1,8 @@ from __future__ import annotations import os +from pathlib import Path +import hashlib import numpy as np import validators @@ -35,9 +37,11 @@ def find_file( if validators.url(file_name): import urllib.request from manimlib.utils.directories import get_downloads_dir - stem, name = os.path.split(file_name) + suffix = Path(file_name).suffix + file_hash = hashlib.sha256(file_name.encode('utf-8')).hexdigest()[:32] folder = get_downloads_dir() - path = os.path.join(folder, name) + + path = Path(folder, file_hash).with_suffix(suffix) urllib.request.urlretrieve(file_name, path) return path From 55b12c902ca8560ce6fcb6708501b87d1e6167a9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 10:13:37 -0700 Subject: [PATCH 06/18] Use density as a parameter instead of step_multiple --- manimlib/mobject/vector_field.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 1c882ba6..32ff3877 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -123,25 +123,34 @@ def move_points_along_vector_field( def get_sample_coords( coordinate_system: CoordinateSystem, - step_multiple: float = 1.0 + density: float = 1.0 ) -> it.product[tuple[Vect3, ...]]: ranges = [] for range_args in coordinate_system.get_all_ranges(): _min, _max, step = range_args - step *= step_multiple + step /= density ranges.append(np.arange(_min, _max + step, step)) return np.array(list(it.product(*ranges))) +def vectorize(pointwise_function: Callable[Tuple, Tuple]): + def v_func(coords_array: VectArray) -> VectArray: + return np.array([pointwise_function(*coords) for coords in coords_array]) + + return v_func + + # Mobjects class VectorField(VMobject): def __init__( self, + # Vectorized function: Takes in an array of coordinates, returns an array of outputs. func: Callable[[VectArray], VectArray], + # Typically a set of Axes or NumberPlane coordinate_system: CoordinateSystem, - step_multiple: float = 0.5, + density: float = 2.0, # Describe as a density instead? magnitude_range: Optional[Tuple[float, float]] = None, color_map_name: Optional[str] = "3b1b_colormap", color_map: Optional[Callable[[Sequence[float]], Vect4Array]] = None, @@ -163,7 +172,7 @@ class VectorField(VMobject): self.norm_to_opacity_func = norm_to_opacity_func # Search for sample_points - self.sample_coords = get_sample_coords(coordinate_system, step_multiple) + self.sample_coords = get_sample_coords(coordinate_system, density) self.update_sample_points() if max_vect_len is None: @@ -326,7 +335,7 @@ class StreamLines(VGroup): self, func: Callable[[VectArray], VectArray], coordinate_system: CoordinateSystem, - step_multiple: float = 0.5, + density: float = 1.0, n_repeats: int = 1, noise_factor: float | None = None, # Config for drawing lines @@ -349,7 +358,7 @@ class StreamLines(VGroup): super().__init__(**kwargs) self.func = func self.coordinate_system = coordinate_system - self.step_multiple = step_multiple + self.density = density self.n_repeats = n_repeats self.noise_factor = noise_factor self.solution_time = solution_time @@ -393,11 +402,11 @@ class StreamLines(VGroup): def get_sample_coords(self): cs = self.coordinate_system - sample_coords = get_sample_coords(cs, self.step_multiple) + sample_coords = get_sample_coords(cs, self.density) noise_factor = self.noise_factor if noise_factor is None: - noise_factor = cs.get_x_unit_size() * self.step_multiple * 0.5 + noise_factor = (cs.get_x_unit_size() / self.density) * 0.5 return np.array([ coords + noise_factor * np.random.random(coords.shape) From 753a042dbe5d6987c6036216c69eee9bb7b90fe4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 10:13:44 -0700 Subject: [PATCH 07/18] Remove unused method --- manimlib/mobject/vector_field.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 32ff3877..bb5116dc 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -414,20 +414,6 @@ class StreamLines(VGroup): for coords in sample_coords ]) - def get_start_points(self) -> Vect3Array: - cs = self.coordinate_system - sample_coords = get_sample_coords(cs, self.step_multiple) - - noise_factor = self.noise_factor - if noise_factor is None: - noise_factor = cs.get_x_unit_size() * self.step_multiple * 0.5 - - return np.array([ - cs.c2p(*coords) + noise_factor * np.random.random(3) - for n in range(self.n_repeats) - for coords in sample_coords - ]) - def init_style(self) -> None: if self.color_by_magnitude: values_to_rgbs = get_vectorized_rgb_gradient_function( From 1d0deb8a335490697ed013093aaee21296851319 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 10:14:23 -0700 Subject: [PATCH 08/18] Remove OldVectorfield --- manimlib/mobject/vector_field.py | 63 -------------------------------- 1 file changed, 63 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index bb5116dc..5355cd74 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -473,66 +473,3 @@ class AnimatedStreamLines(VGroup): line.time += dt adjusted_time = max(line.time, 0) % line.anim.run_time line.anim.update(adjusted_time / line.anim.run_time) - - -class OldVectorField(VGroup): - def __init__( - self, - func: Callable[[float, float], Sequence[float]], - coordinate_system: CoordinateSystem, - step_multiple: float = 0.5, - magnitude_range: Tuple[float, float] = (0, 2), - color_map: str = "3b1b_colormap", - # Takes in actual norm, spits out displayed norm - length_func: Callable[[float], float] = lambda norm: 0.45 * sigmoid(norm), - opacity: float = 1.0, - vector_config: dict = dict(), - **kwargs - ): - super().__init__(**kwargs) - self.func = func - self.coordinate_system = coordinate_system - self.step_multiple = step_multiple - self.magnitude_range = magnitude_range - self.color_map = color_map - self.length_func = length_func - self.opacity = opacity - self.vector_config = dict(vector_config) - - self.value_to_rgb = get_vectorized_rgb_gradient_function( - *self.magnitude_range, self.color_map, - ) - - samples = get_sample_coords( - coordinate_system, self.step_multiple - ) - self.add(*( - self.get_vector(coords) - for coords in samples - )) - - def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow: - vector_config = merge_dicts_recursively( - self.vector_config, - kwargs - ) - - output = np.array(self.func(*coords)) - norm = get_norm(output) - if norm > 0: - output *= self.length_func(norm) / norm - - origin = self.coordinate_system.get_origin() - _input = self.coordinate_system.c2p(*coords) - _output = self.coordinate_system.c2p(*output) - - vect = Arrow( - origin, _output, buff=0, - **vector_config - ) - vect.shift(_input - origin) - vect.set_color( - rgb_to_color(self.value_to_rgb(norm)), - opacity=self.opacity, - ) - return vect From ed2f9f3305fd73dd5eb8bf7a4691fc735e2342b4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 10:49:05 -0700 Subject: [PATCH 09/18] Fix import of pyplot --- manimlib/utils/color.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/utils/color.py b/manimlib/utils/color.py index a511375e..25118d4f 100644 --- a/manimlib/utils/color.py +++ b/manimlib/utils/color.py @@ -5,6 +5,7 @@ from colour import hex2rgb from colour import rgb2hex import numpy as np import random +from matplotlib import pyplot from manimlib.constants import COLORMAP_3B1B from manimlib.constants import WHITE @@ -157,7 +158,7 @@ def get_colormap_from_colors(colors: Iterable[ManimColor]) -> Callable[[Sequence def get_color_map(map_name: str) -> Callable[[Sequence[float]], Vect4Array]: if map_name == "3b1b_colormap": return get_colormap_from_colors(COLORMAP_3B1B) - return plt.get_cmap(map_name) + return pyplot.get_cmap(map_name) # Delete this? From 90ab2f64bb781c31641b5d73ecff498cd36cc858 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 10:49:29 -0700 Subject: [PATCH 10/18] Clean up style arguments on VectorField --- manimlib/mobject/vector_field.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 5355cd74..876db25a 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -150,16 +150,17 @@ class VectorField(VMobject): func: Callable[[VectArray], VectArray], # Typically a set of Axes or NumberPlane coordinate_system: CoordinateSystem, - density: float = 2.0, # Describe as a density instead? + density: float = 2.0, magnitude_range: Optional[Tuple[float, float]] = None, + color: Optional[ManimColor] = None, color_map_name: Optional[str] = "3b1b_colormap", color_map: Optional[Callable[[Sequence[float]], Vect4Array]] = None, - stroke_color: ManimColor = BLUE, stroke_opacity: float = 1.0, - stroke_width: float = 2, + stroke_width: float = 3, tip_width_ratio: float = 4, tip_len_to_width: float = 0.01, max_vect_len: float | None = None, + max_vect_len_to_step_size: float = 0.8, flat_stroke: bool = False, norm_to_opacity_func=None, # TODO, check on this **kwargs @@ -176,7 +177,8 @@ class VectorField(VMobject): self.update_sample_points() if max_vect_len is None: - self.max_displayed_vect_len = get_norm(self.sample_points[1] - self.sample_points[0]) + step_size = get_norm(self.sample_points[1] - self.sample_points[0]) + self.max_displayed_vect_len = max_vect_len_to_step_size * step_size else: self.max_displayed_vect_len = max_vect_len * coordinate_system.get_x_unit_size() @@ -187,27 +189,25 @@ class VectorField(VMobject): self.magnitude_range = magnitude_range - if color_map is not None: - self.color_map = color_map - elif color_map_name is not None: - self.color_map = get_color_map(color_map_name) - else: + if color is not None: self.color_map = None + else: + self.color_map = color_map or get_color_map(color_map_name) self.init_base_stroke_width_array(len(self.sample_coords)) super().__init__( - stroke_color=stroke_color, stroke_opacity=stroke_opacity, flat_stroke=flat_stroke, **kwargs ) + self.set_stroke(color, stroke_width) + self.update_vectors() + def init_points(self): n_samples = len(self.sample_coords) self.set_points(np.zeros((8 * n_samples - 1, 3))) - self.set_stroke(width=stroke_width) self.set_joint_type('no_joint') - self.update_vectors() def get_sample_points( self, From c6b9826f847eafbde96539f828d060d916cae34d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 10:50:12 -0700 Subject: [PATCH 11/18] Update TimeVaryingVectorField to match new VectorField configuration --- manimlib/mobject/vector_field.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 876db25a..ec16c81a 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -318,13 +318,18 @@ class TimeVaryingVectorField(VectorField): def __init__( self, # Takes in an array of points and a float for time - time_func, + time_func: Callable[[VectArray, float], VectArray], + coordinate_system: CoordinateSystem, **kwargs ): self.time = 0 - super().__init__(func=lambda p: time_func(p, self.time), **kwargs) + + def func(coords): + return time_func(coords, self.time) + + super().__init__(func, coordinate_system, **kwargs) self.add_updater(lambda m, dt: m.increment_time(dt)) - always(self.update_vectors) + self.always.update_vectors() def increment_time(self, dt): self.time += dt From 5b88d2347cdf3ba1d1670c64a96568aa4602847f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 11:01:38 -0700 Subject: [PATCH 12/18] Allow for LaTeX in DecimalNumber, e.g. for units --- manimlib/mobject/numbers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index ce644055..30f6981e 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -22,7 +22,12 @@ if TYPE_CHECKING: @lru_cache() def char_to_cahced_mob(char: str, **text_config): - return Text(char, **text_config) + if "\\" in char: + # This is for when the "character" is a LaTeX command + # like ^\circ or \dots + return Tex(char, **text_config) + else: + return Text(char, **text_config) class DecimalNumber(VMobject): From e19ceaaff020b94825e928d2709e26aa4d863395 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 11:02:54 -0700 Subject: [PATCH 13/18] Have TexMobject keep track of font_size --- manimlib/mobject/svg/tex_mobject.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 54da7fa6..5211cbcb 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -48,6 +48,7 @@ class Tex(StringMobject): if not tex_string.strip(): tex_string = R"\\" + self.font_size = font_size self.tex_string = tex_string self.alignment = alignment self.template = template @@ -86,6 +87,10 @@ class Tex(StringMobject): content, self.template, self.additional_preamble, self.tex_string ) + def _handle_scale_side_effects(self, scale_factor: float) -> Self: + self.font_size *= scale_factor + return self + # Parsing @staticmethod From 54c8a9014b1b5bca7a3020335f79179e870b0d5b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 11:27:11 -0700 Subject: [PATCH 14/18] Add scale_stroke_with_zoom option to VMobject --- manimlib/mobject/types/vectorized_mobject.py | 18 +++++++++++++++--- .../shaders/quadratic_bezier/stroke/vert.glsl | 3 ++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 0256b875..f4678569 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -94,6 +94,7 @@ class VMobject(Mobject): # Could also be "no_joint", "bevel", "miter" joint_type: str = "auto", flat_stroke: bool = False, + scale_stroke_with_zoom: bool = False, use_simple_quadratic_approx: bool = False, # Measured in pixel widths anti_alias_width: float = 1.5, @@ -110,6 +111,7 @@ class VMobject(Mobject): self.long_lines = long_lines self.joint_type = joint_type self.flat_stroke = flat_stroke + self.scale_stroke_with_zoom = scale_stroke_with_zoom self.use_simple_quadratic_approx = use_simple_quadratic_approx self.anti_alias_width = anti_alias_width self.fill_border_width = fill_border_width @@ -126,9 +128,12 @@ class VMobject(Mobject): def init_uniforms(self): super().init_uniforms() - self.uniforms["anti_alias_width"] = self.anti_alias_width - self.uniforms["joint_type"] = JOINT_TYPE_MAP[self.joint_type] - self.uniforms["flat_stroke"] = float(self.flat_stroke) + self.uniforms.update( + anti_alias_width=self.anti_alias_width, + joint_type=JOINT_TYPE_MAP[self.joint_type], + flat_stroke=float(self.flat_stroke), + scale_stroke_with_zoom=float(self.scale_stroke_with_zoom) + ) def add(self, *vmobjects: VMobject) -> Self: if not all((isinstance(m, VMobject) for m in vmobjects)): @@ -399,6 +404,13 @@ class VMobject(Mobject): def get_flat_stroke(self) -> bool: return self.uniforms["flat_stroke"] == 1.0 + def set_scale_stroke_with_zoom(self, scale_stroke_with_zoom: bool = True, recurse: bool = True) -> Self: + self.set_uniform(recurse, scale_stroke_with_zoom=float(scale_stroke_with_zoom)) + pass + + def get_scale_stroke_with_zoom(self) -> bool: + return self.uniforms["flat_stroke"] == 1.0 + def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self: for mob in self.get_family(recurse): mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type] diff --git a/manimlib/shaders/quadratic_bezier/stroke/vert.glsl b/manimlib/shaders/quadratic_bezier/stroke/vert.glsl index 0e43ad38..c76d14f5 100644 --- a/manimlib/shaders/quadratic_bezier/stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier/stroke/vert.glsl @@ -2,6 +2,7 @@ uniform float frame_scale; uniform float is_fixed_in_frame; +uniform float scale_stroke_with_zoom; in vec3 point; in vec4 stroke_rgba; @@ -22,7 +23,7 @@ const float STROKE_WIDTH_CONVERSION = 0.01; void main(){ verts = point; v_color = stroke_rgba; - v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame); + v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, scale_stroke_with_zoom); v_joint_angle = joint_angle; v_unit_normal = unit_normal; } \ No newline at end of file From 9cd6a87ff88dd7f1805e0394661d79f63b67013c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 12:28:31 -0700 Subject: [PATCH 15/18] Make sure VMobject uniform flat_stroke matches the use inside the quadratic_bezier/stroke/geom.glsl code --- .../shaders/quadratic_bezier/stroke/geom.glsl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier/stroke/geom.glsl b/manimlib/shaders/quadratic_bezier/stroke/geom.glsl index 07483968..78b32ebf 100644 --- a/manimlib/shaders/quadratic_bezier/stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier/stroke/geom.glsl @@ -4,7 +4,7 @@ layout (triangles) in; layout (triangle_strip, max_vertices = 64) out; // Related to MAX_STEPS below uniform float anti_alias_width; -uniform float flat_stroke_float; +uniform float flat_stroke; uniform float pixel_size; uniform float joint_type; uniform float frame_scale; @@ -62,13 +62,13 @@ vec3 rotate_vector(vec3 vect, vec3 unit_normal, float angle){ } -vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angle, bool inside_curve, bool flat_stroke){ +vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angle, bool inside_curve, bool draw_flat){ /* 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 ? tangent : project(tangent, unit_normal)); + vec3 unit_tan = normalize(draw_flat ? tangent : project(tangent, unit_normal)); // Step to stroke width bound should be perpendicular // both to the tangent and the normal direction @@ -93,7 +93,7 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angl if (abs(cos_angle) > COS_THRESHOLD) return step; // Below here, figure out the adjustment to bevel or miter a joint - if (!flat_stroke){ + if (!draw_flat){ // 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 @@ -128,17 +128,17 @@ void emit_point_with_width( float width, vec4 joint_color, bool inside_curve, - bool flat_stroke + bool draw_flat ){ // Find unit normal - vec3 unit_normal = flat_stroke ? v_unit_normal[1] : normalize(camera_position - point); + vec3 unit_normal = draw_flat ? v_unit_normal[1] : normalize(camera_position - point); // Set styling color = finalize_color(joint_color, point, unit_normal); // 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_angle, inside_curve, flat_stroke); + vec3 step = step_to_corner(point, tangent, unit_normal, joint_angle, inside_curve, draw_flat); float aaw = max(anti_alias_width * pixel_size, 1e-8); // Emit two corners @@ -163,7 +163,7 @@ void main() { if (vec3(v_stroke_width[0], v_stroke_width[1], v_stroke_width[2]) == vec3(0.0, 0.0, 0.0)) return; if (vec3(v_color[0].a, v_color[1].a, v_color[2].a) == vec3(0.0, 0.0, 0.0)) return; - bool flat_stroke = bool(flat_stroke_float) || bool(is_fixed_in_frame); + bool draw_flat = bool(flat_stroke) || bool(is_fixed_in_frame); // Coefficients such that the quadratic bezier is c0 + c1 * t + c2 * t^2 vec3 c0 = verts[0]; @@ -207,7 +207,7 @@ void main() { emit_point_with_width( point, tangent, joint_angle, stroke_width, color, - inside_curve, flat_stroke + inside_curve, draw_flat ); } EndPrimitive(); From dd51b696e5c473c55dc20ed51ed7ad0cefee29b9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 12:35:32 -0700 Subject: [PATCH 16/18] Only apply non-flat-stroke correction in non-zero joint angle vertices --- manimlib/shaders/quadratic_bezier/stroke/geom.glsl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier/stroke/geom.glsl b/manimlib/shaders/quadratic_bezier/stroke/geom.glsl index 78b32ebf..d2ebb98b 100644 --- a/manimlib/shaders/quadratic_bezier/stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier/stroke/geom.glsl @@ -78,11 +78,13 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angl // 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(v_unit_normal[1], tangent)); - step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment)); + if(joint_angle != 0){ + 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(v_unit_normal[1], tangent)); + step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment)); + } } if (inside_curve || int(joint_type) == NO_JOINT) return step; From fbce0b132ccbdd05da903d7147b6583f10ef6cc4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 12:39:32 -0700 Subject: [PATCH 17/18] Temporary band-aide for degenerate normal vector calculations This solution is a bit too specific to the case of spheres. --- manimlib/shaders/surface/vert.glsl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/manimlib/shaders/surface/vert.glsl b/manimlib/shaders/surface/vert.glsl index 96b85bf4..8937282a 100644 --- a/manimlib/shaders/surface/vert.glsl +++ b/manimlib/shaders/surface/vert.glsl @@ -11,8 +11,14 @@ out vec4 v_color; #INSERT get_unit_normal.glsl #INSERT finalize_color.glsl +const float EPSILON = 1e-10; + void main(){ emit_gl_Position(point); - vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point)); - v_color = finalize_color(rgba, point, normalize(normal)); + vec3 du = (du_point - point); + vec3 dv = (dv_point - point); + vec3 normal = cross(du, dv); + float mag = length(normal); + vec3 unit_normal = (mag < EPSILON) ? vec3(0, 0, sign(point.z)) : normal / mag; + v_color = finalize_color(rgba, point, unit_normal); } \ No newline at end of file From 85638d88dc7f26d0e5de8074465c754821c295b9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Nov 2024 12:39:41 -0700 Subject: [PATCH 18/18] Update parameter range for sphere --- manimlib/mobject/three_dimensions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index b0fd4b47..32b5bbce 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -94,7 +94,7 @@ class Sphere(Surface): def __init__( self, u_range: Tuple[float, float] = (0, TAU), - v_range: Tuple[float, float] = (1e-5, PI - 1e-5), + v_range: Tuple[float, float] = (0, PI), resolution: Tuple[int, int] = (101, 51), radius: float = 1.0, **kwargs, @@ -158,7 +158,6 @@ class Cylinder(Surface): **kwargs ) - def init_points(self): super().init_points() self.scale(self.radius)