Reimplement SpeechBubble and ThoughtBubble

This commit is contained in:
Grant Sanderson 2024-03-21 14:36:17 -03:00
parent 0509e824c6
commit 1d6aa47933

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import numpy as np
import itertools as it
import random
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.rotation import Rotating
@ -24,6 +25,7 @@ from manimlib.constants import LEFT
from manimlib.constants import LEFT
from manimlib.constants import MED_LARGE_BUFF
from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import LARGE_BUFF
from manimlib.constants import ORIGIN
from manimlib.constants import OUT
from manimlib.constants import PI
@ -41,6 +43,7 @@ from manimlib.constants import WHITE
from manimlib.constants import YELLOW
from manimlib.constants import TAU
from manimlib.mobject.boolean_ops import Difference
from manimlib.mobject.boolean_ops import Union
from manimlib.mobject.geometry import Arc
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot
@ -51,6 +54,7 @@ from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import AnnularSector
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.numbers import Integer
from manimlib.mobject.shape_matchers import SurroundingRectangle
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
@ -59,9 +63,13 @@ from manimlib.mobject.three_dimensions import Prismify
from manimlib.mobject.three_dimensions import VCube
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.svg.text_mobject import Text
from manimlib.utils.bezier import interpolate
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.rate_functions import linear
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import compass_directions
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import midpoint
from manimlib.utils.space_ops import rotate_vector
@ -344,66 +352,76 @@ class ClockPassesTime(AnimationGroup):
)
class Bubble(SVGMobject):
class Bubble(VGroup):
file_name: str = "Bubbles_speech.svg"
bubble_center_adjustment_factor = 0.125
def __init__(
self,
content: str | VMobject | None = None,
buff: float = 1.0,
filler_shape: Tuple[float, float] = (3.0, 2.0),
pin_point: Vect3 | None = None,
direction: Vect3 = LEFT,
center_point: Vect3 = ORIGIN,
content_scale_factor: float = 0.7,
height: float = 4.0,
width: float = 8.0,
max_height: float | None = None,
max_width: float | None = None,
bubble_center_adjustment_factor: float = 0.125,
add_content: bool = True,
fill_color: ManimColor = BLACK,
fill_opacity: float = 0.8,
stroke_color: ManimColor = WHITE,
stroke_width: float = 3.0,
**kwargs
):
self.direction = LEFT # Possibly updated below by self.flip()
self.bubble_center_adjustment_factor = bubble_center_adjustment_factor
self.content_scale_factor = content_scale_factor
super().__init__(**kwargs)
self.direction = direction
super().__init__(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_color=stroke_color,
stroke_width=stroke_width,
**kwargs
)
if content is None:
content = Rectangle(*filler_shape)
content.set_fill(opacity=0)
content.set_stroke(width=0)
elif isinstance(content, str):
content = Text(content)
self.content = content
self.center()
self.set_height(height, stretch=True)
self.set_width(width, stretch=True)
if max_height:
self.set_max_height(max_height)
if max_width:
self.set_max_width(max_width)
self.body = self.get_body(content, direction, buff)
self.body.set_fill(fill_color, fill_opacity)
self.body.set_stroke(stroke_color, stroke_width)
self.add(self.body)
if add_content:
self.add(self.content)
if pin_point is not None:
self.pin_to(pin_point)
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
body = SVGMobject(self.file_name)
if direction[0] > 0:
self.flip()
self.content = VMobject()
body.flip()
# Resize
width = content.get_width()
height = content.get_height()
target_width = width + min(buff, height)
target_height = 1.35 * (height + buff) # Magic number?
body.set_shape(target_width, target_height)
body.move_to(content)
body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN)
return body
def get_tip(self):
# TODO, find a better way
return self.get_corner(DOWN + self.direction) - 0.6 * self.direction
return self.get_corner(DOWN + self.direction)
def get_bubble_center(self):
factor = self.bubble_center_adjustment_factor
return self.get_center() + factor * self.get_height() * UP
def move_tip_to(self, point):
mover = VGroup(self)
if self.content is not None:
mover.add(self.content)
mover.shift(point - self.get_tip())
self.shift(point - self.get_tip())
return self
def flip(self, axis=UP):
super().flip(axis=axis)
def flip(self, axis=UP, only_body=True, **kwargs):
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