mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +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 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):
|
||||
if only_body:
|
||||
self.body.flip(axis=axis, **kwargs)
|
||||
else:
|
||||
super().flip(axis=axis, **kwargs)
|
||||
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
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue