123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- from __future__ import annotations
- import numpy as np
- from pyglet.window import key as PygletWindowKeys
- from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
- from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP
- from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF
- from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE
- from manimlib.mobject.mobject import Group
- from manimlib.mobject.mobject import Mobject
- from manimlib.mobject.geometry import Circle
- from manimlib.mobject.geometry import Dot
- from manimlib.mobject.geometry import Line
- from manimlib.mobject.geometry import Rectangle
- from manimlib.mobject.geometry import RoundedRectangle
- from manimlib.mobject.geometry import Square
- from manimlib.mobject.svg.text_mobject import Text
- from manimlib.mobject.types.vectorized_mobject import VGroup
- from manimlib.mobject.value_tracker import ValueTracker
- from manimlib.utils.color import rgb_to_hex
- from manimlib.utils.space_ops import get_closest_point_on_line
- from manimlib.utils.space_ops import get_norm
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from typing import Callable
- from manimlib.typing import ManimColor
- # Interactive Mobjects
- class MotionMobject(Mobject):
- """
- You could hold and drag this object to any position
- """
- def __init__(self, mobject: Mobject, **kwargs):
- super().__init__(**kwargs)
- assert isinstance(mobject, Mobject)
- self.mobject = mobject
- self.mobject.add_mouse_drag_listner(self.mob_on_mouse_drag)
- # To avoid locking it as static mobject
- self.mobject.add_updater(lambda mob: None)
- self.add(mobject)
- def mob_on_mouse_drag(self, mob: Mobject, event_data: dict[str, np.ndarray]) -> bool:
- mob.move_to(event_data["point"])
- return False
- class Button(Mobject):
- """
- Pass any mobject and register an on_click method
- The on_click method takes mobject as argument like updater
- """
- def __init__(self, mobject: Mobject, on_click: Callable[[Mobject]], **kwargs):
- super().__init__(**kwargs)
- assert isinstance(mobject, Mobject)
- self.on_click = on_click
- self.mobject = mobject
- self.mobject.add_mouse_press_listner(self.mob_on_mouse_press)
- self.add(self.mobject)
- def mob_on_mouse_press(self, mob: Mobject, event_data) -> bool:
- self.on_click(mob)
- return False
- # Controls
- class ControlMobject(ValueTracker):
- def __init__(self, value: float, *mobjects: Mobject, **kwargs):
- super().__init__(value=value, **kwargs)
- self.add(*mobjects)
- # To avoid lock_static_mobject_data while waiting in scene
- self.add_updater(lambda mob: None)
- self.fix_in_frame()
- def set_value(self, value: float):
- self.assert_value(value)
- self.set_value_anim(value)
- return ValueTracker.set_value(self, value)
- def assert_value(self, value):
- # To be implemented in subclasses
- pass
- def set_value_anim(self, value):
- # To be implemented in subclasses
- pass
- class EnableDisableButton(ControlMobject):
- def __init__(
- self,
- value: bool = True,
- value_type: np.dtype = np.dtype(bool),
- rect_kwargs: dict = {
- "width": 0.5,
- "height": 0.5,
- "fill_opacity": 1.0
- },
- enable_color: ManimColor = GREEN,
- disable_color: ManimColor = RED,
- **kwargs
- ):
- self.value = value
- self.value_type = value_type
- self.rect_kwargs = rect_kwargs
- self.enable_color = enable_color
- self.disable_color = disable_color
- self.box = Rectangle(**self.rect_kwargs)
- super().__init__(value, self.box, **kwargs)
- self.add_mouse_press_listner(self.on_mouse_press)
- def assert_value(self, value: bool) -> None:
- assert isinstance(value, bool)
- def set_value_anim(self, value: bool) -> None:
- if value:
- self.box.set_fill(self.enable_color)
- else:
- self.box.set_fill(self.disable_color)
- def toggle_value(self) -> None:
- super().set_value(not self.get_value())
- def on_mouse_press(self, mob: Mobject, event_data) -> bool:
- mob.toggle_value()
- return False
- class Checkbox(ControlMobject):
- def __init__(
- self,
- value: bool = True,
- value_type: np.dtype = np.dtype(bool),
- rect_kwargs: dict = {
- "width": 0.5,
- "height": 0.5,
- "fill_opacity": 0.0
- },
- checkmark_kwargs: dict = {
- "stroke_color": GREEN,
- "stroke_width": 6,
- },
- cross_kwargs: dict = {
- "stroke_color": RED,
- "stroke_width": 6,
- },
- box_content_buff: float = SMALL_BUFF,
- **kwargs
- ):
- self.value_type = value_type
- self.rect_kwargs = rect_kwargs
- self.checkmark_kwargs = checkmark_kwargs
- self.cross_kwargs = cross_kwargs
- self.box_content_buff = box_content_buff
- self.box = Rectangle(**self.rect_kwargs)
- self.box_content = self.get_checkmark() if value else self.get_cross()
- super().__init__(value, self.box, self.box_content, **kwargs)
- self.add_mouse_press_listner(self.on_mouse_press)
- def assert_value(self, value: bool) -> None:
- assert isinstance(value, bool)
- def toggle_value(self) -> None:
- super().set_value(not self.get_value())
- def set_value_anim(self, value: bool) -> None:
- if value:
- self.box_content.become(self.get_checkmark())
- else:
- self.box_content.become(self.get_cross())
- def on_mouse_press(self, mob: Mobject, event_data) -> None:
- mob.toggle_value()
- return False
- # Helper methods
- def get_checkmark(self) -> VGroup:
- checkmark = VGroup(
- Line(UP / 2 + 2 * LEFT, DOWN + LEFT, **self.checkmark_kwargs),
- Line(DOWN + LEFT, UP + RIGHT, **self.checkmark_kwargs)
- )
- checkmark.stretch_to_fit_width(self.box.get_width())
- checkmark.stretch_to_fit_height(self.box.get_height())
- checkmark.scale(0.5)
- checkmark.move_to(self.box)
- return checkmark
- def get_cross(self) -> VGroup:
- cross = VGroup(
- Line(UP + LEFT, DOWN + RIGHT, **self.cross_kwargs),
- Line(UP + RIGHT, DOWN + LEFT, **self.cross_kwargs)
- )
- cross.stretch_to_fit_width(self.box.get_width())
- cross.stretch_to_fit_height(self.box.get_height())
- cross.scale(0.5)
- cross.move_to(self.box)
- return cross
- class LinearNumberSlider(ControlMobject):
- def __init__(
- self,
- value: float = 0,
- value_type: type = np.float64,
- min_value: float = -10.0,
- max_value: float = 10.0,
- step: float = 1.0,
- rounded_rect_kwargs: dict = {
- "height": 0.075,
- "width": 2,
- "corner_radius": 0.0375
- },
- circle_kwargs: dict = {
- "radius": 0.1,
- "stroke_color": GREY_A,
- "fill_color": GREY_A,
- "fill_opacity": 1.0
- },
- **kwargs
- ):
- self.value_type = value_type
- self.min_value = min_value
- self.max_value = max_value
- self.step = step
- self.rounded_rect_kwargs = rounded_rect_kwargs
- self.circle_kwargs = circle_kwargs
- self.bar = RoundedRectangle(**self.rounded_rect_kwargs)
- self.slider = Circle(**self.circle_kwargs)
- self.slider_axis = Line(
- start=self.bar.get_bounding_box_point(LEFT),
- end=self.bar.get_bounding_box_point(RIGHT)
- )
- self.slider_axis.set_opacity(0.0)
- self.slider.move_to(self.slider_axis)
- self.slider.add_mouse_drag_listner(self.slider_on_mouse_drag)
- super().__init__(value, self.bar, self.slider, self.slider_axis, **kwargs)
- def assert_value(self, value: float) -> None:
- assert self.min_value <= value <= self.max_value
- def set_value_anim(self, value: float) -> None:
- prop = (value - self.min_value) / (self.max_value - self.min_value)
- self.slider.move_to(self.slider_axis.point_from_proportion(prop))
- def slider_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
- self.set_value(self.get_value_from_point(event_data["point"]))
- return False
- # Helper Methods
- def get_value_from_point(self, point: np.ndarray) -> float:
- start, end = self.slider_axis.get_start_and_end()
- point_on_line = get_closest_point_on_line(start, end, point)
- prop = get_norm(point_on_line - start) / get_norm(end - start)
- value = self.min_value + prop * (self.max_value - self.min_value)
- no_of_steps = int((value - self.min_value) / self.step)
- value_nearest_to_step = self.min_value + no_of_steps * self.step
- return value_nearest_to_step
- class ColorSliders(Group):
- def __init__(
- self,
- sliders_kwargs: dict = {},
- rect_kwargs: dict = {
- "width": 2.0,
- "height": 0.5,
- "stroke_opacity": 1.0
- },
- background_grid_kwargs: dict = {
- "colors": [GREY_A, GREY_C],
- "single_square_len": 0.1
- },
- sliders_buff: float = MED_LARGE_BUFF,
- default_rgb_value: int = 255,
- default_a_value: int = 1,
- **kwargs
- ):
- self.sliders_kwargs = sliders_kwargs
- self.rect_kwargs = rect_kwargs
- self.background_grid_kwargs = background_grid_kwargs
- self.sliders_buff = sliders_buff
- self.default_rgb_value = default_rgb_value
- self.default_a_value = default_a_value
- rgb_kwargs = {"value": self.default_rgb_value, "min_value": 0, "max_value": 255, "step": 1}
- a_kwargs = {"value": self.default_a_value, "min_value": 0, "max_value": 1, "step": 0.04}
- self.r_slider = LinearNumberSlider(**self.sliders_kwargs, **rgb_kwargs)
- self.g_slider = LinearNumberSlider(**self.sliders_kwargs, **rgb_kwargs)
- self.b_slider = LinearNumberSlider(**self.sliders_kwargs, **rgb_kwargs)
- self.a_slider = LinearNumberSlider(**self.sliders_kwargs, **a_kwargs)
- self.sliders = Group(
- self.r_slider,
- self.g_slider,
- self.b_slider,
- self.a_slider
- )
- self.sliders.arrange(DOWN, buff=self.sliders_buff)
- self.r_slider.slider.set_color(RED)
- self.g_slider.slider.set_color(GREEN)
- self.b_slider.slider.set_color(BLUE)
- self.a_slider.slider.set_color_by_gradient(BLACK, WHITE)
- self.selected_color_box = Rectangle(**self.rect_kwargs)
- self.selected_color_box.add_updater(
- lambda mob: mob.set_fill(
- self.get_picked_color(), self.get_picked_opacity()
- )
- )
- self.background = self.get_background()
- super().__init__(
- Group(self.background, self.selected_color_box).fix_in_frame(),
- self.sliders,
- **kwargs
- )
- self.arrange(DOWN)
- def get_background(self) -> VGroup:
- single_square_len = self.background_grid_kwargs["single_square_len"]
- colors = self.background_grid_kwargs["colors"]
- width = self.rect_kwargs["width"]
- height = self.rect_kwargs["height"]
- rows = int(height / single_square_len)
- cols = int(width / single_square_len)
- cols = (cols + 1) if (cols % 2 == 0) else cols
- single_square = Square(single_square_len)
- grid = single_square.get_grid(n_rows=rows, n_cols=cols, buff=0.0)
- grid.stretch_to_fit_width(width)
- grid.stretch_to_fit_height(height)
- grid.move_to(self.selected_color_box)
- for idx, square in enumerate(grid):
- assert isinstance(square, Square)
- square.set_stroke(width=0.0, opacity=0.0)
- square.set_fill(colors[idx % len(colors)], 1.0)
- return grid
- def set_value(self, r: float, g: float, b: float, a: float):
- self.r_slider.set_value(r)
- self.g_slider.set_value(g)
- self.b_slider.set_value(b)
- self.a_slider.set_value(a)
- def get_value(self) -> np.ndarary:
- r = self.r_slider.get_value() / 255
- g = self.g_slider.get_value() / 255
- b = self.b_slider.get_value() / 255
- alpha = self.a_slider.get_value()
- return np.array((r, g, b, alpha))
- def get_picked_color(self) -> str:
- rgba = self.get_value()
- return rgb_to_hex(rgba[:3])
- def get_picked_opacity(self) -> float:
- rgba = self.get_value()
- return rgba[3]
- class Textbox(ControlMobject):
- def __init__(
- self,
- value: str = "",
- value_type: np.dtype = np.dtype(object),
- box_kwargs: dict = {
- "width": 2.0,
- "height": 1.0,
- "fill_color": WHITE,
- "fill_opacity": 1.0,
- },
- text_kwargs: dict = {
- "color": BLUE
- },
- text_buff: float = MED_SMALL_BUFF,
- isInitiallyActive: bool = False,
- active_color: ManimColor = BLUE,
- deactive_color: ManimColor = RED,
- **kwargs
- ):
- self.value_type = value_type
- self.box_kwargs = box_kwargs
- self.text_kwargs = text_kwargs
- self.text_buff = text_buff
- self.isInitiallyActive = isInitiallyActive
- self.active_color = active_color
- self.deactive_color = deactive_color
- self.isActive = self.isInitiallyActive
- self.box = Rectangle(**self.box_kwargs)
- self.box.add_mouse_press_listner(self.box_on_mouse_press)
- self.text = Text(value, **self.text_kwargs)
- super().__init__(value, self.box, self.text, **kwargs)
- self.update_text(value)
- self.active_anim(self.isActive)
- self.add_key_press_listner(self.on_key_press)
- def set_value_anim(self, value: str) -> None:
- self.update_text(value)
- def update_text(self, value: str) -> None:
- text = self.text
- self.remove(text)
- text.__init__(value, **self.text_kwargs)
- height = text.get_height()
- text.set_width(self.box.get_width() - 2 * self.text_buff)
- if text.get_height() > height:
- text.set_height(height)
- text.add_updater(lambda mob: mob.move_to(self.box))
- text.fix_in_frame()
- self.add(text)
- def active_anim(self, isActive: bool) -> None:
- if isActive:
- self.box.set_stroke(self.active_color)
- else:
- self.box.set_stroke(self.deactive_color)
- def box_on_mouse_press(self, mob, event_data) -> bool:
- self.isActive = not self.isActive
- self.active_anim(self.isActive)
- return False
- def on_key_press(self, mob: Mobject, event_data: dict[str, int]) -> bool | None:
- symbol = event_data["symbol"]
- modifiers = event_data["modifiers"]
- char = chr(symbol)
- if mob.isActive:
- old_value = mob.get_value()
- new_value = old_value
- if char.isalnum():
- if (modifiers & PygletWindowKeys.MOD_SHIFT) or (modifiers & PygletWindowKeys.MOD_CAPSLOCK):
- new_value = old_value + char.upper()
- else:
- new_value = old_value + char.lower()
- elif symbol in [PygletWindowKeys.SPACE]:
- new_value = old_value + char
- elif symbol == PygletWindowKeys.TAB:
- new_value = old_value + '\t'
- elif symbol == PygletWindowKeys.BACKSPACE:
- new_value = old_value[:-1] or ''
- mob.set_value(new_value)
- return False
- class ControlPanel(Group):
- def __init__(
- self,
- *controls: ControlMobject,
- panel_kwargs: dict = {
- "width": FRAME_WIDTH / 4,
- "height": MED_SMALL_BUFF + FRAME_HEIGHT,
- "fill_color": GREY_C,
- "fill_opacity": 1.0,
- "stroke_width": 0.0
- },
- opener_kwargs: dict = {
- "width": FRAME_WIDTH / 8,
- "height": 0.5,
- "fill_color": GREY_C,
- "fill_opacity": 1.0
- },
- opener_text_kwargs: dict = {
- "text": "Control Panel",
- "font_size": 20
- },
- **kwargs
- ):
- self.panel_kwargs = panel_kwargs
- self.opener_kwargs = opener_kwargs
- self.opener_text_kwargs = opener_text_kwargs
- self.panel = Rectangle(**self.panel_kwargs)
- self.panel.to_corner(UP + LEFT, buff=0)
- self.panel.shift(self.panel.get_height() * UP)
- self.panel.add_mouse_scroll_listner(self.panel_on_mouse_scroll)
- self.panel_opener_rect = Rectangle(**self.opener_kwargs)
- self.panel_info_text = Text(**self.opener_text_kwargs)
- self.panel_info_text.move_to(self.panel_opener_rect)
- self.panel_opener = Group(self.panel_opener_rect, self.panel_info_text)
- self.panel_opener.next_to(self.panel, DOWN, aligned_edge=DOWN)
- self.panel_opener.add_mouse_drag_listner(self.panel_opener_on_mouse_drag)
- self.controls = Group(*controls)
- self.controls.arrange(DOWN, center=False, aligned_edge=ORIGIN)
- self.controls.move_to(self.panel)
- super().__init__(
- self.panel, self.panel_opener,
- self.controls,
- **kwargs
- )
- self.move_panel_and_controls_to_panel_opener()
- self.fix_in_frame()
- def move_panel_and_controls_to_panel_opener(self) -> None:
- self.panel.next_to(
- self.panel_opener_rect,
- direction=UP,
- buff=0
- )
- controls_old_x = self.controls.get_x()
- self.controls.next_to(
- self.panel_opener_rect,
- direction=UP,
- buff=MED_SMALL_BUFF
- )
- self.controls.set_x(controls_old_x)
- def add_controls(self, *new_controls: ControlMobject) -> None:
- self.controls.add(*new_controls)
- self.move_panel_and_controls_to_panel_opener()
- def remove_controls(self, *controls_to_remove: ControlMobject) -> None:
- self.controls.remove(*controls_to_remove)
- self.move_panel_and_controls_to_panel_opener()
- def open_panel(self):
- panel_opener_x = self.panel_opener.get_x()
- self.panel_opener.to_corner(DOWN + LEFT, buff=0.0)
- self.panel_opener.set_x(panel_opener_x)
- self.move_panel_and_controls_to_panel_opener()
- return self
- def close_panel(self):
- panel_opener_x = self.panel_opener.get_x()
- self.panel_opener.to_corner(UP + LEFT, buff=0.0)
- self.panel_opener.set_x(panel_opener_x)
- self.move_panel_and_controls_to_panel_opener()
- return self
- def panel_opener_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
- point = event_data["point"]
- self.panel_opener.match_y(Dot(point))
- self.move_panel_and_controls_to_panel_opener()
- return False
- def panel_on_mouse_scroll(self, mob, event_data: dict[str, np.ndarray]) -> bool:
- offset = event_data["offset"]
- factor = 10 * offset[1]
- self.controls.set_y(self.controls.get_y() + factor)
- return False
|