Merge branch '3b1b:master' into fix-animation-time_span

This commit is contained in:
osMrPigHead 2024-08-15 15:11:30 +08:00 committed by GitHub
commit 644084d9a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 408 additions and 323 deletions

View file

@ -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,

View file

@ -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 *

View file

@ -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
)

View file

@ -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,6 +135,7 @@ class FadeTransform(Transform):
Animation.clean_up_from_scene(self, scene)
scene.remove(self.mobject)
self.mobject[0].restore()
if not self.remover:
scene.add(self.to_add_on_completion)

View file

@ -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):

View file

@ -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"]

View file

@ -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))

View file

@ -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)

View file

@ -222,6 +222,7 @@ class DecimalMatrix(Matrix):
decimal_config: dict = dict(),
**config
):
self.float_matrix = matrix
super().__init__(
matrix,
element_config=dict(

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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,7 +190,8 @@ class VMobject(Mobject):
recurse: bool = True
) -> Self:
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
if border_width is not None:
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()
@ -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

View file

@ -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(

View file

@ -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'")

View file

@ -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);
}

View file

@ -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){
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){
/*
Perpendicular vectors to the left of the curve
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();
}

View file

@ -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;
}

View file

@ -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:]