mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Merge branch '3b1b:master' into fix-animation-time_span
This commit is contained in:
commit
644084d9a7
21 changed files with 408 additions and 323 deletions
|
@ -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,
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -222,6 +222,7 @@ class DecimalMatrix(Matrix):
|
|||
decimal_config: dict = dict(),
|
||||
**config
|
||||
):
|
||||
self.float_matrix = matrix
|
||||
super().__init__(
|
||||
matrix,
|
||||
element_config=dict(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'")
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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:]
|
||||
|
|
Loading…
Add table
Reference in a new issue