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) brace = always_redraw(Brace, square, UP)
label = TexText("Width = 0.00") 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) # This ensures that the method deicmal.next_to(square)
# is called on every frame # is called on every frame
@ -488,10 +488,7 @@ class GraphExample(Scene):
# with the intent of having other mobjects update based # with the intent of having other mobjects update based
# on the parameter # on the parameter
x_tracker = ValueTracker(2) x_tracker = ValueTracker(2)
f_always( dot.add_updater(lambda d: d.move_to(axes.i2gp(x_tracker.get_value(), parabola)))
dot.move_to,
lambda: 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(4), run_time=3)
self.play(x_tracker.animate.set_value(-2), run_time=3) self.play(x_tracker.animate.set_value(-2), run_time=3)
@ -515,7 +512,7 @@ class TexAndNumbersExample(Scene):
# on them. # on them.
tex = Tex("x^2 + y^2 = 4.00") tex = Tex("x^2 + y^2 = 4.00")
tex.next_to(axes, UP, buff=0.5) 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 # This will tie the right hand side of our equation to
@ -537,10 +534,10 @@ class TexAndNumbersExample(Scene):
rate_func=there_and_back, 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 # of the number,but by passing replace_all=True it replaces all and
# returns a group of the results # 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( self.play(
LaggedStartMap( LaggedStartMap(
FlashAround, exponents, FlashAround, exponents,

View file

@ -43,7 +43,6 @@ from manimlib.mobject.probability import *
from manimlib.mobject.shape_matchers import * from manimlib.mobject.shape_matchers import *
from manimlib.mobject.svg.brace import * from manimlib.mobject.svg.brace import *
from manimlib.mobject.svg.drawings 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.string_mobject import *
from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.svg_mobject import *
from manimlib.mobject.svg.special_tex 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), *(anim_func(submob, **anim_kwargs) for submob in group),
run_time=run_time, run_time=run_time,
lag_ratio=lag_ratio, lag_ratio=lag_ratio,
group=group
) )

View file

@ -118,6 +118,7 @@ class FadeTransform(Transform):
def ghost_to(self, source: Mobject, target: Mobject) -> None: def ghost_to(self, source: Mobject, target: Mobject) -> None:
source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match) source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)
source.set_uniform(**target.get_uniforms())
source.set_opacity(0) source.set_opacity(0)
def get_all_mobjects(self) -> list[Mobject]: def get_all_mobjects(self) -> list[Mobject]:
@ -134,6 +135,7 @@ class FadeTransform(Transform):
Animation.clean_up_from_scene(self, scene) Animation.clean_up_from_scene(self, scene)
scene.remove(self.mobject) scene.remove(self.mobject)
self.mobject[0].restore() self.mobject[0].restore()
if not self.remover:
scene.add(self.to_add_on_completion) scene.add(self.to_add_on_completion)

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import math import math
import warnings
import numpy as np import numpy as np
from scipy.spatial.transform import Rotation from scipy.spatial.transform import Rotation
@ -9,8 +10,10 @@ from pyrr import Matrix44
from manimlib.constants import DEGREES, RADIANS from manimlib.constants import DEGREES, RADIANS
from manimlib.constants import FRAME_SHAPE from manimlib.constants import FRAME_SHAPE
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import PI
from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Mobject
from manimlib.utils.space_ops import normalize from manimlib.utils.space_ops import normalize
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -62,9 +65,19 @@ class CameraFrame(Mobject):
def get_euler_angles(self) -> np.ndarray: def get_euler_angles(self) -> np.ndarray:
orientation = self.get_orientation() 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 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): def get_theta(self):
return self.get_euler_angles()[0] return self.get_euler_angles()[0]
@ -134,16 +147,16 @@ class CameraFrame(Mobject):
def increment_euler_angles( def increment_euler_angles(
self, self,
dtheta: float | None = None, dtheta: float = 0,
dphi: float | None = None, dphi: float = 0,
dgamma: float | None = None, dgamma: float = 0,
units: float = RADIANS units: float = RADIANS
): ):
angles = self.get_euler_angles() angles = self.get_euler_angles()
for i, value in enumerate([dtheta, dphi, dgamma]): new_angles = angles + np.array([dtheta, dphi, dgamma]) * units
if value is not None: new_angles[1] = clip(new_angles[1], 0, PI) # Limit range for phi
angles[i] += value * units new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1])
self.set_euler_angles(*angles) self.set_orientation(new_rot)
return self return self
def set_euler_axes(self, seq: str): def set_euler_axes(self, seq: str):

View file

@ -6,7 +6,7 @@ import colour
import importlib import importlib
import inspect import inspect
import os import os
from screeninfo import get_monitors import screeninfo
import sys import sys
import yaml 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: def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict:
# Default to making window half the screen size # Default to making window half the screen size
# but make it full screen if -f is passed in # 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"] mon_index = custom_config["window_monitor"]
monitor = monitors[min(mon_index, len(monitors) - 1)] monitor = monitors[min(mon_index, len(monitors) - 1)]
aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"] 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, time_per_anchor: float = 1.0 / 15,
stroke_width: float | Iterable[float] = 2.0, stroke_width: float | Iterable[float] = 2.0,
stroke_color: ManimColor = WHITE, stroke_color: ManimColor = WHITE,
fill_opacity: float = 0.0,
**kwargs **kwargs
): ):
super().__init__( super().__init__(**kwargs)
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_opacity=fill_opacity,
**kwargs
)
self.traced_point_func = traced_point_func self.traced_point_func = traced_point_func
self.time_traced = time_traced self.time_traced = time_traced
self.time_per_anchor = time_per_anchor self.time_per_anchor = time_per_anchor
self.time: float = 0 self.time: float = 0
self.traced_points: list[np.ndarray] = [] self.traced_points: list[np.ndarray] = []
self.add_updater(lambda m, dt: m.update_path(dt)) self.add_updater(lambda m, dt: m.update_path(dt))
self.set_stroke(stroke_color, stroke_width)
def update_path(self, dt: float) -> Self: def update_path(self, dt: float) -> Self:
if dt == 0: if dt == 0:
@ -167,3 +162,4 @@ class TracingTail(TracedPath):
stroke_color=stroke_color, stroke_color=stroke_color,
**kwargs **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_axis_config: dict = dict(),
z_normal: Vect3 = DOWN, z_normal: Vect3 = DOWN,
depth: float | None = None, depth: float | None = None,
flat_stroke: bool = False,
**kwargs **kwargs
): ):
Axes.__init__(self, x_range, y_range, **kwargs) Axes.__init__(self, x_range, y_range, **kwargs)
@ -555,8 +554,6 @@ class ThreeDAxes(Axes):
self.axes.add(self.z_axis) self.axes.add(self.z_axis)
self.add(self.z_axis) self.add(self.z_axis)
self.set_flat_stroke(flat_stroke)
def get_all_ranges(self) -> list[Sequence[float]]: def get_all_ranges(self) -> list[Sequence[float]]:
return [self.x_range, self.y_range, self.z_range] return [self.x_range, self.y_range, self.z_range]
@ -603,9 +600,6 @@ class ThreeDAxes(Axes):
**kwargs **kwargs
) -> ParametricSurface: ) -> ParametricSurface:
surface = ParametricSurface(func, color=color, opacity=opacity, **kwargs) 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] axes = [self.x_axis, self.y_axis, self.z_axis]
for dim, axis in zip(range(3), axes): for dim, axis in zip(range(3), axes):
surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN) surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN)

View file

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

View file

@ -122,13 +122,9 @@ class Underline(Line):
stretch_factor=1.2, stretch_factor=1.2,
**kwargs **kwargs
): ):
super().__init__( super().__init__(LEFT, RIGHT, **kwargs)
LEFT, RIGHT, if not isinstance(stroke_width, (float, int)):
stroke_color=stroke_color, self.insert_n_curves(len(stroke_width) - 2)
stroke_width=stroke_width,
**kwargs
)
self.insert_n_curves(30)
self.set_stroke(stroke_color, stroke_width) self.set_stroke(stroke_color, stroke_width)
self.set_width(mobject.get_width() * stretch_factor) self.set_width(mobject.get_width() * stretch_factor)
self.next_to(mobject, DOWN, buff=buff) self.next_to(mobject, DOWN, buff=buff)

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import numpy as np import numpy as np
import itertools as it import itertools as it
import random
from manimlib.animation.composition import AnimationGroup from manimlib.animation.composition import AnimationGroup
from manimlib.animation.rotation import Rotating from manimlib.animation.rotation import Rotating
@ -24,6 +25,7 @@ from manimlib.constants import LEFT
from manimlib.constants import LEFT from manimlib.constants import LEFT
from manimlib.constants import MED_LARGE_BUFF from manimlib.constants import MED_LARGE_BUFF
from manimlib.constants import MED_SMALL_BUFF from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import LARGE_BUFF
from manimlib.constants import ORIGIN from manimlib.constants import ORIGIN
from manimlib.constants import OUT from manimlib.constants import OUT
from manimlib.constants import PI from manimlib.constants import PI
@ -41,6 +43,7 @@ from manimlib.constants import WHITE
from manimlib.constants import YELLOW from manimlib.constants import YELLOW
from manimlib.constants import TAU from manimlib.constants import TAU
from manimlib.mobject.boolean_ops import Difference 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 Arc
from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot 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.geometry import AnnularSector
from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Mobject
from manimlib.mobject.numbers import Integer 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.svg_mobject import SVGMobject
from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText 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.three_dimensions import VCube
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject 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.rate_functions import linear
from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import compass_directions 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 midpoint
from manimlib.utils.space_ops import rotate_vector 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" file_name: str = "Bubbles_speech.svg"
bubble_center_adjustment_factor = 0.125
def __init__( def __init__(
self, 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, direction: Vect3 = LEFT,
center_point: Vect3 = ORIGIN, add_content: bool = True,
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,
fill_color: ManimColor = BLACK, fill_color: ManimColor = BLACK,
fill_opacity: float = 0.8, fill_opacity: float = 0.8,
stroke_color: ManimColor = WHITE, stroke_color: ManimColor = WHITE,
stroke_width: float = 3.0, stroke_width: float = 3.0,
**kwargs **kwargs
): ):
self.direction = LEFT # Possibly updated below by self.flip() super().__init__(**kwargs)
self.bubble_center_adjustment_factor = bubble_center_adjustment_factor self.direction = direction
self.content_scale_factor = content_scale_factor
super().__init__( if content is None:
fill_color=fill_color, content = Rectangle(*filler_shape)
fill_opacity=fill_opacity, content.set_fill(opacity=0)
stroke_color=stroke_color, content.set_stroke(width=0)
stroke_width=stroke_width, elif isinstance(content, str):
**kwargs content = Text(content)
) self.content = content
self.center() self.body = self.get_body(content, direction, buff)
self.set_height(height, stretch=True) self.body.set_fill(fill_color, fill_opacity)
self.set_width(width, stretch=True) self.body.set_stroke(stroke_color, stroke_width)
if max_height: self.add(self.body)
self.set_max_height(max_height)
if max_width: if add_content:
self.set_max_width(max_width) 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: if direction[0] > 0:
self.flip() body.flip()
# Resize
self.content = VMobject() 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): def get_tip(self):
# TODO, find a better way return self.get_corner(DOWN + self.direction)
return self.get_corner(DOWN + self.direction) - 0.6 * self.direction
def get_bubble_center(self): def get_bubble_center(self):
factor = self.bubble_center_adjustment_factor factor = self.bubble_center_adjustment_factor
return self.get_center() + factor * self.get_height() * UP return self.get_center() + factor * self.get_height() * UP
def move_tip_to(self, point): def move_tip_to(self, point):
mover = VGroup(self) self.shift(point - self.get_tip())
if self.content is not None:
mover.add(self.content)
mover.shift(point - self.get_tip())
return self return self
def flip(self, axis=UP): def flip(self, axis=UP, only_body=True, **kwargs):
super().flip(axis=axis) 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: if abs(axis[1]) > 0:
self.direction = -np.array(self.direction) self.direction = -np.array(self.direction)
return self return self
@ -418,9 +436,9 @@ class Bubble(SVGMobject):
self.move_tip_to(mob_center + vector_from_center) self.move_tip_to(mob_center + vector_from_center)
return self return self
def position_mobject_inside(self, mobject): def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF):
mobject.set_max_width(self.content_scale_factor * self.get_width()) mobject.set_max_width(self.body.get_width() - 2 * buff)
mobject.set_max_height(self.content_scale_factor * self.get_height() / 1.5) mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff)
mobject.shift(self.get_bubble_center() - mobject.get_center()) mobject.shift(self.get_bubble_center() - mobject.get_center())
return mobject return mobject
@ -429,26 +447,110 @@ class Bubble(SVGMobject):
self.content = mobject self.content = mobject
return self.content return self.content
def write(self, *text): def write(self, text):
self.add_content(TexText(*text)) self.add_content(Text(text))
return self return self
def resize_to_content(self, buff=0.75): def resize_to_content(self, buff=1.0): # TODO
width = self.content.get_width() self.body.match_points(self.get_body(
height = self.content.get_height() self.content, self.direction, buff
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 clear(self): def clear(self):
self.add_content(VMobject()) self.remove(self.content)
return self return self
class SpeechBubble(Bubble): 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" file_name: str = "Bubbles_speech.svg"
@ -456,17 +558,16 @@ class DoubleSpeechBubble(Bubble):
file_name: str = "Bubbles_double_speech.svg" file_name: str = "Bubbles_double_speech.svg"
class ThoughtBubble(Bubble): class OldThoughtBubble(Bubble):
file_name: str = "Bubbles_thought.svg" file_name: str = "Bubbles_thought.svg"
def __init__(self, **kwargs): def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
Bubble.__init__(self, **kwargs) body = super().get_body(content, direction, buff)
self.submobjects.sort( body.sort(lambda p: p[1])
key=lambda m: m.get_bottom()[1] return body
)
def make_green_screen(self): 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 return self

View file

@ -231,7 +231,7 @@ class Tex(StringMobject):
)) ))
return re.findall(pattern, self.string) return re.findall(pattern, self.string)
def make_number_changable( def make_number_changeable(
self, self,
value: float | int | str, value: float | int | str,
index: int = 0, index: int = 0,
@ -241,7 +241,7 @@ class Tex(StringMobject):
substr = str(value) substr = str(value)
parts = self.select_parts(substr) parts = self.select_parts(substr)
if len(parts) == 0: 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() return VMobject()
if index > len(parts) - 1: if index > len(parts) - 1:
log.warning(f"Requested {index}th occurance of {value}, but only {len(parts)} exist") 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, normal_nudge: float = 1e-2,
depth_test: bool = True, depth_test: bool = True,
joint_type: str = 'no_joint', joint_type: str = 'no_joint',
flat_stroke: bool = False,
**kwargs **kwargs
): ):
self.uv_surface = uv_surface self.uv_surface = uv_surface
@ -52,7 +51,6 @@ class SurfaceMesh(VGroup):
joint_type=joint_type, joint_type=joint_type,
**kwargs **kwargs
) )
self.set_flat_stroke(flat_stroke)
def init_points(self) -> None: def init_points(self) -> None:
uv_surface = self.uv_surface uv_surface = self.uv_surface

View file

@ -71,5 +71,5 @@ class ImageMobject(Mobject):
rgb = self.image.getpixel(( rgb = self.image.getpixel((
int((pw - 1) * x_alpha), int((pw - 1) * x_alpha),
int((ph - 1) * y_alpha), int((ph - 1) * y_alpha),
)) ))[:3]
return np.array(rgb) / 255 return np.array(rgb) / 255

View file

@ -97,10 +97,10 @@ class VMobject(Mobject):
long_lines: bool = False, long_lines: bool = False,
# Could also be "no_joint", "bevel", "miter" # Could also be "no_joint", "bevel", "miter"
joint_type: str = "auto", joint_type: str = "auto",
flat_stroke: bool = True, flat_stroke: bool = False,
use_simple_quadratic_approx: bool = False, use_simple_quadratic_approx: bool = False,
# Measured in pixel widths # Measured in pixel widths
anti_alias_width: float = 1.0, anti_alias_width: float = 1.5,
fill_border_width: float = 0.5, fill_border_width: float = 0.5,
use_winding_fill: bool = True, use_winding_fill: bool = True,
**kwargs **kwargs
@ -190,7 +190,8 @@ class VMobject(Mobject):
recurse: bool = True recurse: bool = True
) -> Self: ) -> Self:
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse) 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): for mob in self.get_family(recurse):
mob.data["fill_border_width"] = border_width mob.data["fill_border_width"] = border_width
self.note_changed_fill() self.note_changed_fill()
@ -202,6 +203,7 @@ class VMobject(Mobject):
width: float | Iterable[float] | None = None, width: float | Iterable[float] | None = None,
opacity: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None,
background: bool | None = None, background: bool | None = None,
flat: bool | None = None,
recurse: bool = True recurse: bool = True
) -> Self: ) -> Self:
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse) 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): for mob in self.get_family(recurse):
mob.stroke_behind = background mob.stroke_behind = background
if flat is not None:
self.set_flat_stroke(flat)
self.note_changed_stroke() self.note_changed_stroke()
return self return self
@ -672,7 +677,7 @@ class VMobject(Mobject):
return bool((dots > 1 - 1e-3).all()) return bool((dots > 1 - 1e-3).all())
def change_anchor_mode(self, mode: str) -> Self: 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: if self.get_num_points() == 0:
return self return self
subpaths = self.get_subpaths() subpaths = self.get_subpaths()
@ -696,7 +701,7 @@ class VMobject(Mobject):
self.add_subpath(new_subpath) self.add_subpath(new_subpath)
return self 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 Edits the path so as to pass smoothly through all
the current anchor points. the current anchor points.
@ -721,7 +726,7 @@ class VMobject(Mobject):
return self return self
def add_subpath(self, points: Vect3Array) -> 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(): if not self.has_points():
self.set_points(points) self.set_points(points)
return self return self
@ -1200,7 +1205,7 @@ class VMobject(Mobject):
points = self.get_points() points = self.get_points()
if(len(points) < 3): if len(points) < 3:
return self.data["joint_product"] return self.data["joint_product"]
# Find all the unit tangent vectors at each joint # Find all the unit tangent vectors at each joint

View file

@ -48,6 +48,7 @@ RESIZE_KEY = 't'
COLOR_KEY = 'c' COLOR_KEY = 'c'
INFORMATION_KEY = 'i' INFORMATION_KEY = 'i'
CURSOR_KEY = 'k' 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. # 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() self.toggle_selection_mode()
elif char == "s" and modifiers == COMMAND_MODIFIER: elif char == "s" and modifiers == COMMAND_MODIFIER:
self.save_selection_to_file() 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() self.copy_frame_positioning()
elif symbol in ARROW_SYMBOLS: elif symbol in ARROW_SYMBOLS:
self.nudge_selection( self.nudge_selection(

View file

@ -213,7 +213,8 @@ class Scene(object):
show_animation_progress: bool = False, show_animation_progress: bool = False,
) -> None: ) -> None:
if not self.preview: if not self.preview:
return # Embed is only relevant with a preview # Embed is only relevant with a preview
return
self.stop_skipping() self.stop_skipping()
self.update_frame() self.update_frame()
self.save_state() self.save_state()
@ -239,6 +240,8 @@ class Scene(object):
i2g=self.i2g, i2g=self.i2g,
i2m=self.i2m, i2m=self.i2m,
checkpoint_paste=self.checkpoint_paste, checkpoint_paste=self.checkpoint_paste,
touch=lambda: shell.enable_gui("manim"),
notouch=lambda: shell.enable_gui(None),
) )
# Enables gui interactions during the embed # Enables gui interactions during the embed
@ -260,20 +263,19 @@ class Scene(object):
# namespace, since this is just a shell session anyway. # namespace, since this is just a shell session anyway.
shell.events.register( shell.events.register(
"pre_run_cell", "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 # Operation to run after each ipython command
def post_cell_func(): def post_cell_func(*args, **kwargs):
if not self.is_window_closing(): if not self.is_window_closing():
self.update_frame(dt=0, ignore_skipping=True) self.update_frame(dt=0, ignore_skipping=True)
self.save_state()
shell.events.register("post_run_cell", post_cell_func) shell.events.register("post_run_cell", post_cell_func)
# Flash border, and potentially play sound, on exceptions # Flash border, and potentially play sound, on exceptions
def custom_exc(shell, etype, evalue, tb, tb_offset=None): 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) shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
if self.embed_error_sound: if self.embed_error_sound:
os.system("printf '\a'") os.system("printf '\a'")

View file

@ -1,66 +1,17 @@
#version 330 #version 330
in vec2 uv_coords; // Value between -1 and 1
in float scaled_signed_dist_to_curve;
in float uv_stroke_width; in float scaled_anti_alias_width;
in float uv_anti_alias_width;
in vec4 color; in vec4 color;
in float is_linear;
out vec4 frag_color; 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() { void main() {
if (uv_stroke_width == 0) discard; if(scaled_anti_alias_width < 0) discard;
frag_color = color; frag_color = color;
// sdf for the region around the curve we wish to color. // sdf for the region around the curve we wish to color.
float signed_dist = dist_to_curve() - 0.5 * uv_stroke_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);
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width);
} }

View file

@ -1,12 +1,13 @@
#version 330 #version 330
layout (triangles) in; 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 anti_alias_width;
uniform float flat_stroke; uniform float flat_stroke;
uniform float pixel_size; uniform float pixel_size;
uniform float joint_type; uniform float joint_type;
uniform float frame_scale;
in vec3 verts[3]; in vec3 verts[3];
@ -15,12 +16,8 @@ in float v_stroke_width[3];
in vec4 v_color[3]; in vec4 v_color[3];
out vec4 color; out vec4 color;
out float uv_stroke_width; out float scaled_anti_alias_width;
out float uv_anti_alias_width; out float scaled_signed_dist_to_curve;
out float is_linear;
out vec2 uv_coords;
// Codes for joint types // Codes for joint types
const int NO_JOINT = 0; const int NO_JOINT = 0;
@ -31,179 +28,207 @@ const int MITER_JOINT = 3;
// When the cosine of the angle between // When the cosine of the angle between
// two vectors is larger than this, we // two vectors is larger than this, we
// consider them aligned // consider them aligned
const float COS_THRESHOLD = 0.99; const float COS_THRESHOLD = 0.999;
// Used to determine how many lines to break the curve into
vec3 unit_normal = vec3(0.0, 0.0, 1.0); const float POLYLINE_FACTOR = 100;
const int MAX_STEPS = 32;
const float MITER_COS_ANGLE_THRESHOLD = -0.8;
#INSERT emit_gl_Position.glsl #INSERT emit_gl_Position.glsl
#INSERT get_xyz_to_uv.glsl
#INSERT finalize_color.glsl #INSERT finalize_color.glsl
vec3 get_joint_unit_normal(vec4 joint_product){ vec3 get_joint_unit_normal(vec4 joint_product){
vec3 result = (joint_product.w < COS_THRESHOLD) ? float tol = 1e-8;
joint_product.xyz : v_joint_product[1].xyz; if (length(joint_product.xyz) > tol){
float norm = length(result); return normalize(joint_product.xyz);
return (norm > 1e-5) ? result / norm : vec3(0.0, 0.0, 1.0); }
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); 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( vec3 point_on_quadratic(float t, vec3 c0, vec3 c1, vec3 c2){
vec4 joint_product, return c0 + c1 * t + c2 * t * t;
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); vec3 tangent_on_quadratic(float t, vec3 c1, vec3 c2){
if(int(joint_type) == MITER_JOINT){ return c1 + 2 * c2 * t;
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 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw){
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; return (vect * dot_product - cross(vect, cross_product)) / dot(vect, vect);
// 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 vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_product, bool inside_curve){
unit_normal = normal; /*
// Choose the "outward" normal direction Step the the left of a curve.
if(normal.z < 0) normal *= -1; First a perpendicular direction is calculated, then it is adjusted
if(bool(flat_stroke)){ so as to make a joint.
return buff * normalize(cross(normal, tangent)); */
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 { }else {
return buff * normalize(cross(camera_position - point, tangent)); float mcat1 = MITER_COS_ANGLE_THRESHOLD;
} float mcat2 = 0.5 * (mcat1 - 1.0);
miter_factor = smoothstep(mcat1, mcat2, cos_angle);
} }
// This function is responsible for finding the corners of float sin_angle = sqrt(1 - cos_angle * cos_angle) * sign(dot(joint_product.xyz, unit_normal));
// a bounding region around the bezier curve, which can be float shift = (cos_angle + mix(-1, 1, miter_factor)) / sin_angle;
// emitted as a triangle fan, with vertices vaguely close
// to control points so that the passage of vert data to return step + shift * unit_tan;
// frag shaders is most natural. }
void get_corners(
// Control points for a bezier curve
vec3 p0, void emit_point_with_width(
vec3 p1, vec3 point,
vec3 p2, vec3 tangent,
// Unit tangent vectors at p0 and p2 vec4 joint_product,
vec3 v01, float width,
vec3 v12, vec4 joint_color,
// Anti-alias width bool inside_curve
float aaw,
out vec3 corners[6]
){ ){
bool linear = bool(is_linear); // Find unit normal
vec4 jp0 = normalized_joint_product(v_joint_product[0]); vec3 to_camera = camera_position - point;
vec4 jp2 = normalized_joint_product(v_joint_product[2]); vec3 unit_normal;
vec3 p0_perp = get_perp(0, jp0, p0, v01, aaw); if (flat_stroke == 0.0){
vec3 p2_perp = get_perp(2, jp2, p2, v12, aaw); unit_normal = normalize(to_camera);
vec3 p1_perp = 0.5 * (p0_perp + p2_perp); }else{
if(linear){ unit_normal = get_joint_unit_normal(joint_product);
p1_perp *= (0.5 * v_stroke_width[1] + aaw) / length(p1_perp); unit_normal *= sign(dot(unit_normal, to_camera)); // Choose the "outward" normal direction
} }
// The order of corners should be for a triangle_strip. // Set styling
vec3 c0 = p0 + p0_perp; color = finalize_color(joint_color, point, unit_normal);
vec3 c1 = p0 - p0_perp; scaled_anti_alias_width = (width == 0) ?
vec3 c2 = p1 + p1_perp; -1.0 : // Signal to discard in the frag shader
vec3 c3 = p1 - p1_perp; 2.0 * anti_alias_width * pixel_size / width;
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);
// Account for previous and next control points // Figure out the step from the point to the corners of the
if(bool(flat_stroke)){ // triangle strip around the polyline
create_joint(jp0, v01, length(p0_perp), c1, c1, c0, c0); vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inside_curve);
create_joint(jp2, -v12, length(p2_perp), c5, c5, c4, c4);
// 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() { void main() {
// Curves are marked as ended when the handle after // Curves are marked as ended when the handle after
// the first anchor is set equal to that anchor // the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return; if (verts[0] == verts[1]) return;
vec3 p0 = verts[0]; // Coefficients such that the quadratic bezier is c0 + c1 * t + c2 * t^2
vec3 p1 = verts[1]; vec3 c0 = verts[0];
vec3 p2 = verts[2]; vec3 c1 = 2 * (verts[1] - verts[0]);
vec3 v01 = normalize(p1 - p0); vec3 c2 = verts[0] - 2 * verts[1] + verts[2];
vec3 v12 = normalize(p2 - p1);
// 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]); // Emit vertex pairs aroudn subdivided points
is_linear = float(jp1.w > COS_THRESHOLD); 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 // Point and tangent
// coincides with y = x^2, between some values x0 and x2. Or, in vec3 point = point_on_quadratic(t, c0, c1, c2);
// the case of a linear curve just put it on the x-axis vec3 tangent = tangent_on_quadratic(t, c1, c2);
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);
}
float scaled_aaw = anti_alias_width * pixel_size; // Style
vec3 corners[6]; float stroke_width = mix(v_stroke_width[0], v_stroke_width[2], t);
get_corners(p0, p1, p2, v01, v12, scaled_aaw, corners); vec4 color = mix(v_color[0], v_color[2], t);
// Emit each corner // This is sent along to prevent needless joint creation
float max_sw = max(v_stroke_width[0], v_stroke_width[2]); bool inside_curve = (i > 0 && i < n_steps - 1);
for(int i = 0; i < 6; i++){
float stroke_width = v_stroke_width[i / 2];
if(bool(is_linear)){ // Use middle joint product for inner points, flip sign for first one's cross product component
float sign = vec2(-1, 1)[i % 2]; vec4 joint_product;
// In this case, we only really care about if (i == 0) joint_product = v_joint_product[0] * vec4(-1, -1, -1, 1);
// the v coordinate else if (inside_curve) joint_product = v_joint_product[1];
uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw)); else joint_product = v_joint_product[2];
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;
}
color = finalize_color(v_color[i / 2], corners[i], unit_normal); emit_point_with_width(
emit_gl_Position(corners[i]); point, tangent, joint_product,
EmitVertex(); stroke_width, color,
inside_curve
);
} }
EndPrimitive(); EndPrimitive();
} }

View file

@ -6,7 +6,6 @@ uniform float is_fixed_in_frame;
in vec3 point; in vec3 point;
in vec4 stroke_rgba; in vec4 stroke_rgba;
in float stroke_width; in float stroke_width;
in vec3 joint_normal;
in vec4 joint_product; in vec4 joint_product;
// Bezier control point // Bezier control point
@ -16,12 +15,11 @@ out vec4 v_joint_product;
out float v_stroke_width; out float v_stroke_width;
out vec4 v_color; out vec4 v_color;
const float STROKE_WIDTH_CONVERSION = 0.01; const float STROKE_WIDTH_CONVERSION = 0.015;
void main(){ void main(){
verts = point; verts = point;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width; v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame);
v_stroke_width *= mix(frame_scale, 1, is_fixed_in_frame);
v_joint_product = joint_product; v_joint_product = joint_product;
v_color = stroke_rgba; 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, another that would produce a parabola passing through P0, call it smooth_to_left,
and use the midpoint between the two. 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) return midpoint(*points)
smooth_to_right, smooth_to_left = [ smooth_to_right, smooth_to_left = [
0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:] 0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:]