Video work (#2318)

* Only use -no-pdf for xelatex rendering

* Instead of tracking du and dv points on surface, track points off the surface in the normal direction

This means that surface shading will not necessarily work well for arbitrary transformations of the surface. But the existing solution was flimsy anyway, and caused annoying issues with singularity points.

* Have density of anchor points on arcs depend on arc length

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

* Have BackgroundRectangle.set_style absorb (and ignore) added configuration

Note, this feels suboptimal

* Add LineBrace

* Update font_size adjustment in Tex
This commit is contained in:
Grant Sanderson 2025-02-26 09:52:59 -06:00 committed by GitHub
parent 7a7bf83f11
commit db421e3981
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 65 additions and 41 deletions

View file

@ -208,12 +208,16 @@ class Arc(TipableVMobject):
start_angle: float = 0, start_angle: float = 0,
angle: float = TAU / 4, angle: float = TAU / 4,
radius: float = 1.0, radius: float = 1.0,
n_components: int = 8, n_components: Optional[int] = None,
arc_center: Vect3 = ORIGIN, arc_center: Vect3 = ORIGIN,
**kwargs **kwargs
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
if n_components is None:
# 16 components for a full circle
n_components = int(15 * (abs(angle) / TAU)) + 1
self.set_points(quadratic_bezier_points_for_arc(angle, n_components)) self.set_points(quadratic_bezier_points_for_arc(angle, n_components))
self.rotate(start_angle, about_point=ORIGIN) self.rotate(start_angle, about_point=ORIGIN)
self.scale(radius, about_point=ORIGIN) self.scale(radius, about_point=ORIGIN)
@ -597,6 +601,9 @@ class DashedLine(Line):
else: else:
return Line.get_end(self) return Line.get_end(self)
def get_start_and_end(self) -> Tuple[Vect3, Vect3]:
return self.get_start(), self.get_end()
def get_first_handle(self) -> Vect3: def get_first_handle(self) -> Vect3:
return self.submobjects[0].get_points()[1] return self.submobjects[0].get_points()[1]

View file

@ -40,6 +40,7 @@ class DecimalNumber(VMobject):
fill_opacity: float = 1.0, fill_opacity: float = 1.0,
fill_border_width: float = 0.5, fill_border_width: float = 0.5,
num_decimal_places: int = 2, num_decimal_places: int = 2,
min_total_width: Optional[int] = 0,
include_sign: bool = False, include_sign: bool = False,
group_with_commas: bool = True, group_with_commas: bool = True,
digit_buff_per_font_unit: float = 0.001, digit_buff_per_font_unit: float = 0.001,
@ -54,6 +55,7 @@ class DecimalNumber(VMobject):
self.num_decimal_places = num_decimal_places self.num_decimal_places = num_decimal_places
self.include_sign = include_sign self.include_sign = include_sign
self.group_with_commas = group_with_commas self.group_with_commas = group_with_commas
self.min_total_width = min_total_width
self.digit_buff_per_font_unit = digit_buff_per_font_unit self.digit_buff_per_font_unit = digit_buff_per_font_unit
self.show_ellipsis = show_ellipsis self.show_ellipsis = show_ellipsis
self.unit = unit self.unit = unit
@ -167,6 +169,7 @@ class DecimalNumber(VMobject):
"include_sign", "include_sign",
"group_with_commas", "group_with_commas",
"num_decimal_places", "num_decimal_places",
"min_total_width",
] ]
]) ])
config.update(kwargs) config.update(kwargs)
@ -176,6 +179,7 @@ class DecimalNumber(VMobject):
config.get("field_name", ""), config.get("field_name", ""),
":", ":",
"+" if config["include_sign"] else "", "+" if config["include_sign"] else "",
"0" + str(config.get("min_total_width", "")) if config.get("min_total_width") else "",
"," if config["group_with_commas"] else "", "," if config["group_with_commas"] else "",
f".{ndp}f" if ndp > 0 else "d", f".{ndp}f" if ndp > 0 else "d",
"}", "}",

View file

@ -79,7 +79,8 @@ class BackgroundRectangle(SurroundingRectangle):
stroke_width: float | None = None, stroke_width: float | None = None,
fill_color: ManimColor | None = None, fill_color: ManimColor | None = None,
fill_opacity: float | None = None, fill_opacity: float | None = None,
family: bool = True family: bool = True,
**kwargs
) -> Self: ) -> Self:
# Unchangeable style, except for fill_opacity # Unchangeable style, except for fill_opacity
VMobject.set_style( VMobject.set_style(

View file

@ -6,7 +6,7 @@ import copy
import numpy as np import numpy as np
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL, UP
from manimlib.constants import PI from manimlib.constants import PI
from manimlib.animation.composition import AnimationGroup from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeIn from manimlib.animation.fading import FadeIn
@ -174,3 +174,12 @@ class BraceLabel(VMobject):
class BraceText(BraceLabel): class BraceText(BraceLabel):
label_constructor: type = TexText label_constructor: type = TexText
class LineBrace(Brace):
def __init__(self, line: Line, direction=UP, **kwargs):
angle = line.get_angle()
line.rotate(-angle)
super().__init__(line, direction, **kwargs)
line.rotate(angle)
self.rotate(angle, about_point=line.get_center())

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
from manimlib.typing import ManimColor, Span, Selector, Self from manimlib.typing import ManimColor, Span, Selector, Self
SCALE_FACTOR_PER_FONT_POINT = 0.001 TEX_MOB_SCALE_FACTOR = 0.001
class Tex(StringMobject): class Tex(StringMobject):
@ -49,7 +49,6 @@ class Tex(StringMobject):
if not tex_string.strip(): if not tex_string.strip():
tex_string = R"\\" tex_string = R"\\"
self.font_size = font_size
self.tex_string = tex_string self.tex_string = tex_string
self.alignment = alignment self.alignment = alignment
self.template = template self.template = template
@ -64,13 +63,16 @@ class Tex(StringMobject):
) )
self.set_color_by_tex_to_color_map(self.tex_to_color_map) self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size) self.scale(TEX_MOB_SCALE_FACTOR * font_size)
self.font_size = font_size # Important for this to go after the scale call
def get_svg_string_by_content(self, content: str) -> str: def get_svg_string_by_content(self, content: str) -> str:
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string) return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
def _handle_scale_side_effects(self, scale_factor: float) -> Self: def _handle_scale_side_effects(self, scale_factor: float) -> Self:
self.font_size *= scale_factor if hasattr(self, "font_size"):
self.font_size *= scale_factor
return self return self
# Parsing # Parsing

View file

@ -97,20 +97,27 @@ class Sphere(Surface):
v_range: Tuple[float, float] = (0, PI), v_range: Tuple[float, float] = (0, PI),
resolution: Tuple[int, int] = (101, 51), resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0, radius: float = 1.0,
true_normals: bool = True,
clockwise=False,
**kwargs, **kwargs,
): ):
self.radius = radius self.radius = radius
self.clockwise = clockwise
super().__init__( super().__init__(
u_range=u_range, u_range=u_range,
v_range=v_range, v_range=v_range,
resolution=resolution, resolution=resolution,
**kwargs **kwargs
) )
# Add bespoke normal specification to avoid issue at poles
if true_normals:
self.data['d_normal_point'] = self.data['point'] * ((radius + self.normal_nudge) / radius)
def uv_func(self, u: float, v: float) -> np.ndarray: def uv_func(self, u: float, v: float) -> np.ndarray:
sign = -1 if self.clockwise else +1
return self.radius * np.array([ return self.radius * np.array([
math.cos(u) * math.sin(v), math.cos(sign * u) * math.sin(v),
math.sin(u) * math.sin(v), math.sin(sign * u) * math.sin(v),
-math.cos(v) -math.cos(v)
]) ])

View file

@ -30,11 +30,10 @@ class Surface(Mobject):
shader_folder: str = "surface" shader_folder: str = "surface"
data_dtype: np.dtype = np.dtype([ data_dtype: np.dtype = np.dtype([
('point', np.float32, (3,)), ('point', np.float32, (3,)),
('du_point', np.float32, (3,)), ('d_normal_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('rgba', np.float32, (4,)), ('rgba', np.float32, (4,)),
]) ])
pointlike_data_keys = ['point', 'du_point', 'dv_point'] pointlike_data_keys = ['point', 'd_normal_point']
def __init__( def __init__(
self, self,
@ -48,9 +47,11 @@ class Surface(Mobject):
# rows/columns of approximating squares # rows/columns of approximating squares
resolution: Tuple[int, int] = (101, 101), resolution: Tuple[int, int] = (101, 101),
prefered_creation_axis: int = 1, prefered_creation_axis: int = 1,
# For du and dv steps. Much smaller and numerical error # For du and dv steps.
# can crop up in the shaders. epsilon: float = 1e-3,
epsilon: float = 1e-4, # Step off the surface to a new point which will
# be used to determine the normal direction
normal_nudge: float = 1e-3,
**kwargs **kwargs
): ):
self.u_range = u_range self.u_range = u_range
@ -58,6 +59,7 @@ class Surface(Mobject):
self.resolution = resolution self.resolution = resolution
self.prefered_creation_axis = prefered_creation_axis self.prefered_creation_axis = prefered_creation_axis
self.epsilon = epsilon self.epsilon = epsilon
self.normal_nudge = normal_nudge
super().__init__( super().__init__(
**kwargs, **kwargs,
@ -94,9 +96,11 @@ class Surface(Mobject):
).reshape((nu * nv, dim)) ).reshape((nu * nv, dim))
for grid in (uv_grid, uv_plus_du, uv_plus_dv) for grid in (uv_grid, uv_plus_du, uv_plus_dv)
] ]
crosses = cross(du_points - points, dv_points - points)
normals = normalize_along_axis(crosses, 1)
self.set_points(points) self.set_points(points)
self.data['du_point'][:] = du_points self.data['d_normal_point'] = points + self.normal_nudge * normals
self.data['dv_point'][:] = dv_points
def uv_to_point(self, u, v): def uv_to_point(self, u, v):
nu, nv = self.resolution nu, nv = self.resolution
@ -152,12 +156,8 @@ class Surface(Mobject):
return self.triangle_indices return self.triangle_indices
def get_unit_normals(self) -> Vect3Array: def get_unit_normals(self) -> Vect3Array:
points = self.get_points() # TOOD, I could try a more resiliant way to compute this using the neighboring grid values
crosses = cross( return normalize_along_axis(self.data['d_normal_point'] - self.data['point'], 1)
self.data['du_point'] - points,
self.data['dv_point'] - points,
)
return normalize_along_axis(crosses, 1)
@Mobject.affects_data @Mobject.affects_data
def pointwise_become_partial( def pointwise_become_partial(
@ -276,8 +276,7 @@ class TexturedSurface(Surface):
shader_folder: str = "textured_surface" shader_folder: str = "textured_surface"
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)), ('point', np.float32, (3,)),
('du_point', np.float32, (3,)), ('d_normal_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('im_coords', np.float32, (2,)), ('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)), ('opacity', np.float32, (1,)),
] ]
@ -321,8 +320,7 @@ class TexturedSurface(Surface):
self.resize_points(surf.get_num_points()) self.resize_points(surf.get_num_points())
self.resolution = surf.resolution self.resolution = surf.resolution
self.data['point'][:] = surf.data['point'] self.data['point'][:] = surf.data['point']
self.data['du_point'][:] = surf.data['du_point'] self.data['d_normal_point'][:] = surf.data['d_normal_point']
self.data['dv_point'][:] = surf.data['dv_point']
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3] self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
self.data["im_coords"] = np.array([ self.data["im_coords"] = np.array([
[u, v] [u, v]

View file

@ -33,7 +33,7 @@ const float COS_THRESHOLD = 0.999;
// Used to determine how many lines to break the curve into // Used to determine how many lines to break the curve into
const float POLYLINE_FACTOR = 100; const float POLYLINE_FACTOR = 100;
const int MAX_STEPS = 32; const int MAX_STEPS = 32;
const float MITER_COS_ANGLE_THRESHOLD = -0.9; const float MITER_COS_ANGLE_THRESHOLD = -0.8;
#INSERT emit_gl_Position.glsl #INSERT emit_gl_Position.glsl
#INSERT finalize_color.glsl #INSERT finalize_color.glsl

View file

@ -1,8 +1,7 @@
#version 330 #version 330
in vec3 point; in vec3 point;
in vec3 du_point; in vec3 d_normal_point;
in vec3 dv_point;
in vec4 rgba; in vec4 rgba;
out vec4 v_color; out vec4 v_color;
@ -15,10 +14,6 @@ const float EPSILON = 1e-10;
void main(){ void main(){
emit_gl_Position(point); emit_gl_Position(point);
vec3 du = (du_point - point); vec3 unit_normal = normalize(d_normal_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); v_color = finalize_color(rgba, point, unit_normal);
} }

View file

@ -1,8 +1,7 @@
#version 330 #version 330
in vec3 point; in vec3 point;
in vec3 du_point; in vec3 d_normal_point;
in vec3 dv_point;
in vec2 im_coords; in vec2 im_coords;
in float opacity; in float opacity;
@ -11,15 +10,17 @@ out vec3 v_unit_normal;
out vec2 v_im_coords; out vec2 v_im_coords;
out float v_opacity; out float v_opacity;
uniform float is_sphere;
uniform vec3 center;
#INSERT emit_gl_Position.glsl #INSERT emit_gl_Position.glsl
#INSERT get_unit_normal.glsl #INSERT get_unit_normal.glsl
const float EPSILON = 1e-10;
void main(){ void main(){
v_point = point; v_point = point;
v_unit_normal = normalize(cross( v_unit_normal = normalize(d_normal_point - point);;
normalize(du_point - point),
normalize(dv_point - point)
));
v_im_coords = im_coords; v_im_coords = im_coords;
v_opacity = opacity; v_opacity = opacity;
emit_gl_Position(point); emit_gl_Position(point);