mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Merge pull request #1323 from sahilmakhijani/shaders
Interactive Mobjects in Manim
This commit is contained in:
commit
c1ebb583c8
6 changed files with 667 additions and 4 deletions
|
@ -396,4 +396,42 @@ class SurfaceExample(Scene):
|
|||
self.wait()
|
||||
|
||||
|
||||
class ControlsExample(Scene):
|
||||
def setup(self):
|
||||
self.textbox = Textbox()
|
||||
self.checkbox = Checkbox()
|
||||
self.color_picker = ColorSliders()
|
||||
self.panel = ControlPanel(
|
||||
Text("Text", size=0.5), self.textbox, Line(),
|
||||
Text("Show/Hide Text", size=0.5), self.checkbox, Line(),
|
||||
Text("Color of Text", size=0.5), self.color_picker
|
||||
)
|
||||
self.add(self.panel)
|
||||
|
||||
def construct(self):
|
||||
text = Text("", size=2)
|
||||
|
||||
def text_updater(old_text):
|
||||
assert(isinstance(old_text, Text))
|
||||
new_text = Text(self.textbox.get_value(), size=old_text.size)
|
||||
new_text.align_data_and_family(old_text)
|
||||
new_text.move_to(old_text)
|
||||
if self.checkbox.get_value():
|
||||
new_text.set_fill(
|
||||
color=self.color_picker.get_picked_color(),
|
||||
opacity=self.color_picker.get_picked_opacity()
|
||||
)
|
||||
else:
|
||||
new_text.set_opacity(0)
|
||||
old_text.become(new_text)
|
||||
|
||||
text.add_updater(text_updater)
|
||||
|
||||
self.add(MotionMobject(text))
|
||||
|
||||
self.textbox.set_value("Manim")
|
||||
self.wait(60)
|
||||
self.embed()
|
||||
|
||||
|
||||
# See https://github.com/3b1b/videos for many, many more
|
||||
|
|
|
@ -43,6 +43,7 @@ from manimlib.mobject.number_line import *
|
|||
from manimlib.mobject.numbers import *
|
||||
from manimlib.mobject.probability import *
|
||||
from manimlib.mobject.shape_matchers import *
|
||||
from manimlib.mobject.interactive import *
|
||||
from manimlib.mobject.svg.brace import *
|
||||
from manimlib.mobject.svg.drawings import *
|
||||
from manimlib.mobject.svg.svg_mobject import *
|
||||
|
|
517
manimlib/mobject/interactive.py
Normal file
517
manimlib/mobject/interactive.py
Normal file
|
@ -0,0 +1,517 @@
|
|||
import numpy as np
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import LEFT, RIGHT, UP, DOWN, ORIGIN
|
||||
from manimlib.constants import SMALL_BUFF, MED_SMALL_BUFF, MED_LARGE_BUFF
|
||||
from manimlib.constants import BLACK, GREY_A, GREY_C, RED, GREEN, BLUE, WHITE
|
||||
from manimlib.mobject.mobject import Mobject, Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.geometry import Dot, Line, Square, Rectangle, RoundedRectangle, Circle
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.value_tracker import ValueTracker
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm, get_closest_point_on_line
|
||||
from manimlib.utils.color import rgb_to_color, color_to_rgba, rgb_to_hex
|
||||
|
||||
|
||||
# Interactive Mobjects
|
||||
|
||||
class MotionMobject(Mobject):
|
||||
"""
|
||||
You could hold and drag this object to any position
|
||||
"""
|
||||
|
||||
CONFIG = {
|
||||
"listen_to_events": True
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.mobject = mobject
|
||||
self.mobject.add_updater(lambda mob: None) # To avoid locking it as static mobject
|
||||
self.add(mobject)
|
||||
|
||||
def on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
if self.mobject.is_point_touching(point):
|
||||
self.mobject.move_to(point)
|
||||
return False
|
||||
|
||||
|
||||
class Button(Mobject):
|
||||
"""
|
||||
Pass any mobject and register an on_click method
|
||||
"""
|
||||
|
||||
CONFIG = {
|
||||
"listen_to_events": True
|
||||
}
|
||||
|
||||
def __init__(self, mobject, on_click, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.mobject, self.on_click = mobject, on_click
|
||||
self.add(self.mobject)
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
if self.mobject.is_point_touching(point):
|
||||
self.on_click()
|
||||
return False
|
||||
|
||||
|
||||
# Controls
|
||||
|
||||
class ContolMobject(ValueTracker):
|
||||
CONFIG = {
|
||||
"listen_to_events": True
|
||||
}
|
||||
|
||||
def __init__(self, value, *mobjects, **kwargs):
|
||||
super().__init__(value=value, **kwargs)
|
||||
self.add(*mobjects)
|
||||
|
||||
self.add_updater(lambda mob: None) # To avoid lock_static_mobject_data while waiting in scene
|
||||
self.fix_in_frame()
|
||||
|
||||
def set_value(self, value):
|
||||
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(ContolMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.dtype(bool),
|
||||
"rect_kwargs": {
|
||||
"width": 0.5,
|
||||
"height": 0.5,
|
||||
"fill_opacity": 1.0
|
||||
},
|
||||
"enable_color": GREEN,
|
||||
"disable_color": RED
|
||||
}
|
||||
|
||||
def __init__(self, value=True, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.box = Rectangle(**self.rect_kwargs)
|
||||
super().__init__(value, self.box, **kwargs)
|
||||
|
||||
def assert_value(self, value):
|
||||
assert(value == True or value == False)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
if value == True:
|
||||
self.box.set_fill(self.enable_color)
|
||||
elif value == False:
|
||||
self.box.set_fill(self.disable_color)
|
||||
|
||||
def toggle_value(self):
|
||||
super().set_value(not self.get_value())
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
self.toggle_value()
|
||||
return False
|
||||
|
||||
|
||||
class Checkbox(ContolMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.dtype(bool),
|
||||
"rect_kwargs": {
|
||||
"width": 0.5,
|
||||
"height": 0.5,
|
||||
"fill_opacity": 0.0
|
||||
},
|
||||
|
||||
"checkmark_kwargs": {
|
||||
"stroke_color": GREEN,
|
||||
"stroke_width": 6,
|
||||
},
|
||||
"cross_kwargs": {
|
||||
"stroke_color": RED,
|
||||
"stroke_width": 6,
|
||||
},
|
||||
"box_content_buff": SMALL_BUFF
|
||||
}
|
||||
|
||||
def __init__(self, value=True, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
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)
|
||||
|
||||
def assert_value(self, value):
|
||||
assert(value == True or value == False)
|
||||
|
||||
def toggle_value(self):
|
||||
super().set_value(not self.get_value())
|
||||
|
||||
def set_value_anim(self, value):
|
||||
if value == True:
|
||||
self.box_content.become(self.get_checkmark())
|
||||
elif value == False:
|
||||
self.box_content.become(self.get_cross())
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
self.toggle_value()
|
||||
return False
|
||||
|
||||
# Helper methods
|
||||
|
||||
def get_checkmark(self):
|
||||
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):
|
||||
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(ContolMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.float64,
|
||||
|
||||
"min_value": -10.0,
|
||||
"max_value": 10.0,
|
||||
"step": 1.0,
|
||||
|
||||
"rounded_rect_kwargs": {
|
||||
"height": 0.075,
|
||||
"width": 2,
|
||||
"corner_radius": 0.0375
|
||||
},
|
||||
"circle_kwargs": {
|
||||
"radius": 0.1,
|
||||
"stroke_color": GREY_A,
|
||||
"fill_color": GREY_A,
|
||||
"fill_opacity": 1.0
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
digest_config(self, 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)
|
||||
|
||||
super().__init__(value, self.bar, self.slider, self.slider_axis, ** kwargs)
|
||||
|
||||
def assert_value(self, value):
|
||||
assert(self.min_value <= value <= self.max_value)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
prop = (value - self.min_value) / (self.max_value - self.min_value)
|
||||
self.slider.move_to(self.slider_axis.point_from_proportion(prop))
|
||||
|
||||
def on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
if self.slider.is_point_touching(point):
|
||||
self.set_value(self.get_value_from_point(point))
|
||||
return False
|
||||
|
||||
# Helper Methods
|
||||
|
||||
def get_value_from_point(self, point):
|
||||
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):
|
||||
CONFIG = {
|
||||
"sliders_kwargs": {},
|
||||
"rect_kwargs": {
|
||||
"width": 2.0,
|
||||
"height": 0.5,
|
||||
"stroke_opacity": 1.0
|
||||
},
|
||||
"background_grid_kwargs": {
|
||||
"colors": [GREY_A, GREY_C],
|
||||
"single_square_len": 0.1
|
||||
},
|
||||
"sliders_buff": MED_LARGE_BUFF,
|
||||
"default_rgb_value": 255,
|
||||
"default_a_value": 1,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
|
||||
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):
|
||||
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, g, b, a):
|
||||
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):
|
||||
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 color_to_rgba(rgb_to_color((r, g, b)), alpha=alpha)
|
||||
|
||||
def get_picked_color(self):
|
||||
rgba = self.get_value()
|
||||
return rgb_to_hex(rgba[:3])
|
||||
|
||||
def get_picked_opacity(self):
|
||||
rgba = self.get_value()
|
||||
return rgba[3]
|
||||
|
||||
|
||||
class Textbox(ContolMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.dtype(object),
|
||||
|
||||
"box_kwargs": {
|
||||
"width": 2.0,
|
||||
"height": 1.0,
|
||||
"fill_color": WHITE,
|
||||
"fill_opacity": 1.0,
|
||||
},
|
||||
"text_kwargs": {
|
||||
"color": BLUE
|
||||
},
|
||||
"text_buff": MED_SMALL_BUFF,
|
||||
"isInitiallyActive": False,
|
||||
"active_color": BLUE,
|
||||
"deactive_color": RED,
|
||||
}
|
||||
|
||||
def __init__(self, value="", **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.isActive = self.isInitiallyActive
|
||||
self.box = Rectangle(**self.box_kwargs)
|
||||
self.text = Text(value, **self.text_kwargs)
|
||||
super().__init__(value, self.box, self.text, **kwargs)
|
||||
self.update_text(value)
|
||||
self.active_anim(self.isActive)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
self.update_text(value)
|
||||
|
||||
def update_text(self, value):
|
||||
self.remove(self.text)
|
||||
self.text.__init__(value, **self.text_kwargs)
|
||||
self.text.set_width(self.box.get_width() - 2*self.text_buff, stretch=True)
|
||||
self.text.add_updater(lambda mob: mob.move_to(self.box))
|
||||
self.text.fix_in_frame()
|
||||
self.add(self.text)
|
||||
|
||||
def active_anim(self, isActive):
|
||||
if isActive:
|
||||
self.box.set_stroke(self.active_color)
|
||||
else:
|
||||
self.box.set_stroke(self.deactive_color)
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
if self.box.is_point_touching(point):
|
||||
self.isActive = not self.isActive
|
||||
self.active_anim(self.isActive)
|
||||
return False
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
char = chr(symbol)
|
||||
if self.isActive:
|
||||
old_value = self.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 ''
|
||||
self.set_value(new_value)
|
||||
return False
|
||||
|
||||
class ControlPanel(Group):
|
||||
CONFIG = {
|
||||
"listen_to_events": True,
|
||||
|
||||
"panel_kwargs": {
|
||||
"width": FRAME_WIDTH / 4,
|
||||
"height": MED_SMALL_BUFF + FRAME_HEIGHT,
|
||||
"fill_color": GREY_C,
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0.0
|
||||
},
|
||||
"opener_kwargs": {
|
||||
"width": FRAME_WIDTH / 8,
|
||||
"height": 0.5,
|
||||
"fill_color": GREY_C,
|
||||
"fill_opacity": 1.0
|
||||
},
|
||||
"opener_text_kwargs": {
|
||||
"text": "Control Panel",
|
||||
"size": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, *controls, **kwargs):
|
||||
digest_config(self, 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_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.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):
|
||||
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):
|
||||
self.controls.add(*new_controls)
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
|
||||
def remove_controls(self, *controls_to_remove):
|
||||
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 on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
if self.panel_opener.is_point_touching(point):
|
||||
self.panel_opener.match_y(Dot(point))
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
return False
|
||||
|
||||
def on_mouse_scroll(self, point, offset):
|
||||
if self.panel.is_point_touching(point):
|
||||
factor = 10 * offset[1]
|
||||
self.controls.set_y(self.controls.get_y() + factor)
|
||||
return False
|
||||
|
|
@ -52,7 +52,9 @@ class Mobject(object):
|
|||
# Must match in attributes of vert shader
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
]
|
||||
],
|
||||
# Event listener
|
||||
"listen_to_events": False
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
@ -215,6 +217,14 @@ class Mobject(object):
|
|||
parent.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def is_point_touching(self, point, buff=MED_SMALL_BUFF):
|
||||
self.refresh_bounding_box()
|
||||
bb = self.get_bounding_box()
|
||||
if np.all(point >= (bb[0] - buff)) and np.all(point <= (bb[2] + buff)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Family matters
|
||||
|
||||
def __getitem__(self, value):
|
||||
|
@ -1434,6 +1444,41 @@ class Mobject(object):
|
|||
def get_shader_vert_indices(self):
|
||||
return self.shader_indices
|
||||
|
||||
# Event Handlers
|
||||
"""
|
||||
Event handling follows the Event Bubbling model of DOM in javascript.
|
||||
Return false to stop the event bubbling.
|
||||
To learn more visit https://www.quirksmode.org/js/events_order.html
|
||||
"""
|
||||
|
||||
def on_mouse_motion(self, point, d_point):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def on_mouse_release(self, point, button, mods):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def on_mouse_scroll(self, point, offset):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
# Errors
|
||||
|
||||
def throw_error_if_no_points(self):
|
||||
|
|
|
@ -513,8 +513,26 @@ class Scene(object):
|
|||
self.mobjects = mobjects
|
||||
|
||||
# Event handling
|
||||
def get_event_listeners_mobjects(self):
|
||||
"""
|
||||
This method returns all the mobjects that listen to events
|
||||
in reversed order. So the top most mobject's event is called first.
|
||||
This helps in event bubbling.
|
||||
"""
|
||||
return filter(
|
||||
lambda mob: mob.listen_to_events,
|
||||
reversed(self.get_mobject_family_members())
|
||||
)
|
||||
|
||||
def on_mouse_motion(self, point, d_point):
|
||||
self.mouse_point.move_to(point)
|
||||
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
if mob_listener.is_point_touching(point):
|
||||
propagate_event = mob_listener.on_mouse_motion(point, d_point)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("d")):
|
||||
frame.increment_theta(-d_point[0])
|
||||
|
@ -530,13 +548,33 @@ class Scene(object):
|
|||
def on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
self.mouse_drag_point.move_to(point)
|
||||
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
if mob_listener.is_point_touching(point):
|
||||
propagate_event = mob_listener.on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
pass
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
if mob_listener.is_point_touching(point):
|
||||
propagate_event = mob_listener.on_mouse_press(point, button, mods)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_release(self, point, button, mods):
|
||||
pass
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
if mob_listener.is_point_touching(point):
|
||||
propagate_event = mob_listener.on_mouse_release(point, button, mods)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_scroll(self, point, offset):
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
if mob_listener.is_point_touching(point):
|
||||
propagate_event = mob_listener.on_mouse_scroll(point, offset)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("z")):
|
||||
factor = 1 + np.arctan(10 * offset[1])
|
||||
|
@ -547,7 +585,10 @@ class Scene(object):
|
|||
frame.shift(-20.0 * shift)
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
pass
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
propagate_event = mob_listener.on_key_release(symbol, modifiers)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
try:
|
||||
|
@ -555,6 +596,12 @@ class Scene(object):
|
|||
except OverflowError:
|
||||
print(" Warning: The value of the pressed key is too large.")
|
||||
return
|
||||
|
||||
for mob_listener in self.get_event_listeners_mobjects():
|
||||
propagate_event = mob_listener.on_key_press(symbol, modifiers)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
if char == "r":
|
||||
self.camera.frame.to_default_state()
|
||||
elif char == "q":
|
||||
|
|
|
@ -290,6 +290,21 @@ def find_intersection(p0, v0, p1, v1, threshold=1e-5):
|
|||
return p0 + ratio * v0
|
||||
|
||||
|
||||
def get_closest_point_on_line(a, b, p):
|
||||
"""
|
||||
It returns point x such that
|
||||
x is on line ab and xp is perpendicular to ab.
|
||||
If x lies beyond ab line, then it returns nearest edge(a or b).
|
||||
"""
|
||||
# x = b + t*(a-b) = t*a + (1-t)*b
|
||||
t = np.dot(p - b, a - b) / np.dot(a - b, a - b)
|
||||
if t < 0:
|
||||
t = 0
|
||||
if t > 1:
|
||||
t = 1
|
||||
return ((t * a) + ((1 - t) * b))
|
||||
|
||||
|
||||
def get_winding_number(points):
|
||||
total_angle = 0
|
||||
for p1, p2 in adjacent_pairs(points):
|
||||
|
|
Loading…
Add table
Reference in a new issue