mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
To allow keyboard and mouse interaction with the
scene. This allows things like panning or zooming. Also modified the message shown after a scene finishes rendering giving clear tips on how to perform the interaction.
This commit is contained in:
parent
733a1fa3d3
commit
f0bf3f5833
5 changed files with 191 additions and 24 deletions
|
|
@ -56,6 +56,11 @@ class Camera(Mobject, InvisibleMobject):
|
|||
self.set_height(self.initial_frame_shape[1], stretch=True)
|
||||
self.move_to(self.center_point)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""restores camera to default orientation and position"""
|
||||
self.set_euler_angles(theta=-TAU / 4, phi=0.0, gamma=0.0)
|
||||
self.init_points()
|
||||
|
||||
def interpolate(
|
||||
self,
|
||||
mobject1: Mobject,
|
||||
|
|
@ -248,7 +253,7 @@ class Camera(Mobject, InvisibleMobject):
|
|||
:class:`Camera`
|
||||
The camera after incrementing its phi angle.
|
||||
"""
|
||||
return self.set_phi(self._phi + dgamma)
|
||||
return self.set_phi(self._phi + dphi)
|
||||
|
||||
def get_gamma(self) -> float:
|
||||
"""Get the angle gamma by which the camera is rotated while standing on
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class Manager(Generic[SceneT]):
|
|||
-------
|
||||
A window if previewing, else None
|
||||
"""
|
||||
return Window() if config.preview else None
|
||||
return Window(self.scene) if config.preview else None
|
||||
|
||||
def create_file_writer(self) -> FileWriterProtocol:
|
||||
"""Create and return a file writer instance.
|
||||
|
|
@ -242,9 +242,15 @@ class Manager(Generic[SceneT]):
|
|||
if self.window is None:
|
||||
return
|
||||
logger.info(
|
||||
"\nTips: Using the keys `d`, `f`, or `z` "
|
||||
"you can interact with the scene. "
|
||||
"Press `command + q` or `esc` to quit"
|
||||
"""\nTips: To interact with the scene:
|
||||
press the key `d` and drag the mouse to 3D pan,
|
||||
press the key `f` and drag the mouse to shift the frame,
|
||||
press the key `z` and scroll the mouse wheel to zoom,
|
||||
press the key `r` to reset the camera to its default settings,
|
||||
press the key `o` to print the current camera configuration that may be copied and pasted into self.camera.set_orientation() for quick configuration,
|
||||
scroll the mouse wheel to shift the scene vertically,
|
||||
press `command + q` or `esc` to quit
|
||||
"""
|
||||
)
|
||||
last_time = time.perf_counter()
|
||||
while not self.window.is_closing:
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from manim.data_structures import MethodWithArgs
|
|||
from manim.event_handler import EVENT_DISPATCHER
|
||||
from manim.event_handler.event_listener import EventListener
|
||||
from manim.event_handler.event_type import EventType
|
||||
from manim.typing import Point3D
|
||||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.exceptions import MultiAnimationOverrideException
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import moderngl_window as mglw
|
||||
import numpy as np
|
||||
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
||||
from moderngl_window.timers.clock import Timer
|
||||
from screeninfo import get_monitors
|
||||
|
||||
from manim import __version__, config
|
||||
from manim.event_handler.window import WindowProtocol
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypeGuard
|
||||
|
|
@ -29,7 +31,7 @@ class Window(PygletWindow, WindowProtocol):
|
|||
vsync: bool = True
|
||||
cursor: bool = True
|
||||
|
||||
def __init__(self, window_size: str | tuple[int, ...] = config.window_size):
|
||||
def __init__(self, scene: Scene, window_size: str | tuple[int, ...] = config.window_size):
|
||||
# TODO: remove size argument from window init,
|
||||
# move size computation below to config
|
||||
|
||||
|
|
@ -65,6 +67,7 @@ class Window(PygletWindow, WindowProtocol):
|
|||
raise ValueError(invalid_window_size_error_message)
|
||||
|
||||
super().__init__(size=size)
|
||||
self.scene = scene
|
||||
self.pressed_keys: set = set()
|
||||
self.title = f"Manim Community {__version__}"
|
||||
self.size = size
|
||||
|
|
@ -109,6 +112,121 @@ class Window(PygletWindow, WindowProtocol):
|
|||
-monitor.y + char_to_n[custom_position[0]] * height_diff // 2,
|
||||
)
|
||||
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> bool:
|
||||
"""tie key pressing to the scene response
|
||||
|
||||
Parameters
|
||||
----------
|
||||
symbol
|
||||
the key that is pressed
|
||||
modifiers
|
||||
keys like shift or ctrl
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
whether pyglet handled the event or not
|
||||
"""
|
||||
self.scene.on_key_press(symbol, modifiers)
|
||||
return super().on_key_press(symbol, modifiers)
|
||||
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
"""tie key release events to the scene response
|
||||
|
||||
Parameters
|
||||
----------
|
||||
symbol
|
||||
the key that is released
|
||||
modifiers
|
||||
keys like shift or ctrl
|
||||
"""
|
||||
return super().on_key_release(symbol, modifiers)
|
||||
|
||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||
"""tie mouse motion events to the scene response
|
||||
|
||||
Parmeters
|
||||
---------
|
||||
x
|
||||
x pixel coordinate
|
||||
y
|
||||
y pixel coordinate
|
||||
dx
|
||||
change of x pixel coordinates
|
||||
dy
|
||||
change of y pixel coordinates
|
||||
"""
|
||||
self.scene.on_mouse_motion(np.array([x, y, 0]), np.array([dx, dy, 0]))
|
||||
|
||||
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
|
||||
"""tie mouse drag events to the scene response
|
||||
|
||||
Parmeters
|
||||
---------
|
||||
x
|
||||
x pixel coordinate
|
||||
y
|
||||
y pixel coordinate
|
||||
dx
|
||||
change of x pixel coordinates
|
||||
dy
|
||||
change of y pixel coordinates
|
||||
buttons
|
||||
the mouse buttons currently pressed
|
||||
modifiers
|
||||
keys like shift or ctrl
|
||||
"""
|
||||
self.scene.on_mouse_drag(
|
||||
np.array([x, y, 0]), np.array([dx, dy, 0]), buttons, modifiers
|
||||
)
|
||||
|
||||
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
"""tie mouse press events to the scene response
|
||||
|
||||
Parmeters
|
||||
---------
|
||||
x
|
||||
x pixel coordinate
|
||||
y
|
||||
y pixel coordinate
|
||||
button
|
||||
the mouse button that was pressed
|
||||
mods
|
||||
keys like shift or ctrl
|
||||
"""
|
||||
self.scene.on_mouse_press(np.array([x, y, 0]), button, mods)
|
||||
|
||||
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
"""tie mouse release events to the scene response
|
||||
|
||||
Parmeters
|
||||
---------
|
||||
x
|
||||
x pixel coordinate
|
||||
y
|
||||
y pixel coordinate
|
||||
button
|
||||
the mouse button that was released
|
||||
mods
|
||||
keys like shift or ctrl
|
||||
"""
|
||||
self.scene.on_mouse_release(np.array([x, y, 0]), button, mods)
|
||||
|
||||
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
||||
"""tie mouse scroll events to the scene response
|
||||
|
||||
Parmeters
|
||||
---------
|
||||
x
|
||||
x pixel coordinate
|
||||
y
|
||||
y pixel coordinate
|
||||
x_offset
|
||||
number of horizontal wheel ticks (not useful for most mice)
|
||||
y_offset
|
||||
number of vertical wheel ticks
|
||||
"""
|
||||
self.scene.on_mouse_scroll(np.array([x, y, 0]), np.array([x_offset, y_offset, 0]))
|
||||
|
||||
def tuple_len_2(pos: tuple[T, ...]) -> TypeGuard[tuple[T, T]]:
|
||||
return len(pos) == 2
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import numpy as np
|
|||
from pyglet.window import key
|
||||
|
||||
from manim import config, logger
|
||||
from manim.animation import transform
|
||||
from manim.animation.animation import Wait, prepare_animation
|
||||
from manim.animation.scene_buffer import SceneBuffer, SceneOperation
|
||||
from manim.camera.camera import Camera
|
||||
|
|
@ -51,6 +52,7 @@ FRAME_SHIFT_KEY = "f"
|
|||
ZOOM_KEY = "z"
|
||||
RESET_FRAME_KEY = "r"
|
||||
QUIT_KEY = "q"
|
||||
GET_ORIENTATION_KEY = "o"
|
||||
|
||||
|
||||
class Scene:
|
||||
|
|
@ -525,6 +527,9 @@ class Scene:
|
|||
# Event handling
|
||||
|
||||
def on_mouse_motion(self, point: Point3D, d_point: Vector3D) -> None:
|
||||
point = self._pos_window_to_camera(point)
|
||||
d_point = self._d_pos_window_to_camera(d_point)
|
||||
|
||||
self.mouse_point.move_to(point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point}
|
||||
|
|
@ -534,25 +539,25 @@ class Scene:
|
|||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
# TODO
|
||||
return
|
||||
frame = self.camera.frame
|
||||
# Handle perspective changes
|
||||
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
|
||||
frame.increment_theta(-self.pan_sensitivity * d_point[0])
|
||||
frame.increment_phi(self.pan_sensitivity * d_point[1])
|
||||
if EVENT_DISPATCHER.is_key_pressed(ord(PAN_3D_KEY)):
|
||||
self.camera.increment_theta(-self.pan_sensitivity * d_point[0])
|
||||
self.camera.increment_phi(self.pan_sensitivity * d_point[1])
|
||||
# Handle frame movements
|
||||
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
||||
elif EVENT_DISPATCHER.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
||||
shift = -d_point
|
||||
shift[0] *= frame.get_width() / 2
|
||||
shift[1] *= frame.get_height() / 2
|
||||
transform = frame.get_inverse_camera_rotation_matrix()
|
||||
shift[0] *= self.camera.get_width() / 2
|
||||
shift[1] *= self.camera.get_height() / 2
|
||||
transform = self.camera.get_inverse_rotation_matrix()
|
||||
shift = np.dot(np.transpose(transform), shift)
|
||||
frame.shift(shift)
|
||||
self.camera.shift(shift)
|
||||
|
||||
def on_mouse_drag(
|
||||
self, point: Point3D, d_point: Vector3D, buttons: int, modifiers: int
|
||||
) -> None:
|
||||
point = self._pos_window_to_camera(point)
|
||||
d_point = self._d_pos_window_to_camera(d_point)
|
||||
|
||||
self.mouse_drag_point.move_to(point)
|
||||
|
||||
event_data = {
|
||||
|
|
@ -568,6 +573,8 @@ class Scene:
|
|||
return
|
||||
|
||||
def on_mouse_press(self, point: Point3D, button: int, mods: int) -> None:
|
||||
point = self._pos_window_to_camera(point)
|
||||
|
||||
self.mouse_drag_point.move_to(point)
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(
|
||||
|
|
@ -577,6 +584,8 @@ class Scene:
|
|||
return
|
||||
|
||||
def on_mouse_release(self, point: Point3D, button: int, mods: int) -> None:
|
||||
point = self._pos_window_to_camera(point)
|
||||
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(
|
||||
EventType.MouseReleaseEvent, **event_data
|
||||
|
|
@ -585,6 +594,8 @@ class Scene:
|
|||
return
|
||||
|
||||
def on_mouse_scroll(self, point: Point3D, offset: Vector3D) -> None:
|
||||
point = self._pos_window_to_camera(point)
|
||||
|
||||
event_data = {"point": point, "offset": offset}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(
|
||||
EventType.MouseScrollEvent, **event_data
|
||||
|
|
@ -592,14 +603,13 @@ class Scene:
|
|||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord(ZOOM_KEY)):
|
||||
factor = 1 + np.arctan(10 * offset[1])
|
||||
frame.scale(1 / factor, about_point=point)
|
||||
if EVENT_DISPATCHER.is_key_pressed(ord(ZOOM_KEY)):
|
||||
factor = 1 / 1.25 if offset[1] > 0 else 1.25
|
||||
self.camera.scale(factor, about_point=point)
|
||||
else:
|
||||
transform = frame.get_inverse_camera_rotation_matrix()
|
||||
transform = self.camera.get_inverse_rotation_matrix()
|
||||
shift = np.dot(np.transpose(transform), offset)
|
||||
frame.shift(-20.0 * shift)
|
||||
self.camera.shift(-shift / 2)
|
||||
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
event_data = {"symbol": symbol, "modifiers": modifiers}
|
||||
|
|
@ -624,7 +634,12 @@ class Scene:
|
|||
return
|
||||
|
||||
if char == RESET_FRAME_KEY:
|
||||
self.play(self.camera.frame.animate.to_default_state())
|
||||
self.camera.reset()
|
||||
elif char == GET_ORIENTATION_KEY:
|
||||
orientation = self.camera.get_orientation()
|
||||
print(
|
||||
f"theta={orientation['theta']}, phi={orientation['phi']}, gamma={orientation['gamma']}, zoom={orientation['zoom']}, focal_distance={orientation['focal_distance']}, frame_center=np.array([{orientation['frame_center'][0]}, {orientation['frame_center'][1]}, {orientation['frame_center'][2]}])"
|
||||
)
|
||||
elif char == "z" and modifiers == key.MOD_COMMAND:
|
||||
self.undo()
|
||||
elif char == "z" and modifiers == key.MOD_COMMAND | key.MOD_SHIFT:
|
||||
|
|
@ -648,6 +663,28 @@ class Scene:
|
|||
def on_close(self) -> None:
|
||||
pass
|
||||
|
||||
def _pos_window_to_camera(self, point: Point3D) -> Point3D:
|
||||
"""The window gives position coordinates in pixels, we need them in camera coordinates for intuitive interactions."""
|
||||
return np.array(
|
||||
[
|
||||
point[0] / self.manager.window.size[0] * self.camera.get_width()
|
||||
- self.camera.get_width() / 2,
|
||||
point[1] / self.manager.window.size[1] * self.camera.get_height()
|
||||
- self.camera.get_height() / 2,
|
||||
0,
|
||||
]
|
||||
)
|
||||
|
||||
def _d_pos_window_to_camera(self, d_point: Point3D) -> Point3D:
|
||||
"""The window gives positions differentials in pixels, we need them in camera units for untitive interactions."""
|
||||
return np.array(
|
||||
[
|
||||
d_point[0] / self.manager.window.size[0] * self.camera.get_width(),
|
||||
d_point[1] / self.manager.window.size[1] * self.camera.get_height(),
|
||||
0,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SceneState:
|
||||
def __init__(self, scene: Scene, ignore: Iterable[Mobject] | None = None) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue