mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Reimplement SpeechBubble and ThoughtBubble
This commit is contained in:
parent
0509e824c6
commit
1d6aa47933
1 changed files with 159 additions and 58 deletions
|
@ -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)
|
if only_body:
|
||||||
|
self.body.flip(axis=axis, **kwargs)
|
||||||
|
else:
|
||||||
|
super().flip(axis=axis, **kwargs)
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue