diff --git a/manim/camera/camera.py b/manim/camera/camera.py index a05bec3e2a..68af478eed 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -56,6 +56,11 @@ def init_points(self) -> None: 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 @@ def increment_phi(self, dphi: float) -> Self: :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 diff --git a/manim/manager.py b/manim/manager.py index 68fb867cf6..2c82a8a031 100644 --- a/manim/manager.py +++ b/manim/manager.py @@ -131,7 +131,7 @@ def create_window(self) -> WindowProtocol | None: ------- 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. diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 25fd6d314c..9e0a1ab046 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -39,6 +39,7 @@ 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 diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index cd8f5c619d..672158e635 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -3,12 +3,14 @@ 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,9 @@ 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 +69,7 @@ def __init__(self, window_size: str | tuple[int, ...] = config.window_size): 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 +114,128 @@ def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]: -monitor.y + char_to_n[custom_position[0]] * height_diff // 2, ) + def on_key_press(self, symbol: int, modifiers: int) -> bool: + """tie key pressing events 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 + + """ + # TODO: do we override pyglet's functions or call them like in this function? + 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 + """ + self.scene.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 + + Parameters + ---------- + 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 + + Parameters + ---------- + 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 + + Parameters + ---------- + x + x pixel coordinate + y + y pixel coordinate + button + the mouse button that is 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 + + Parameters + ---------- + x + x pixel coordinate + y + y pixel coordinate + button + the mouse button that is 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 scrolling events to the scene response + + Parameters + ---------- + 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 diff --git a/manim/scene/scene.py b/manim/scene/scene.py index db6e9b58c4..02903468c8 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -525,6 +525,9 @@ def redo(self): # 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 +537,25 @@ def on_mouse_motion(self, point: Point3D, d_point: Vector3D) -> None: 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 +571,8 @@ def on_mouse_drag( 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 +582,8 @@ def on_mouse_press(self, point: Point3D, button: int, mods: int) -> None: 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 +592,8 @@ def on_mouse_release(self, point: Point3D, button: int, mods: int) -> None: 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 +601,13 @@ def on_mouse_scroll(self, point: Point3D, offset: Vector3D) -> None: 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 +632,7 @@ def on_key_press(self, symbol: int, modifiers: int) -> None: return if char == RESET_FRAME_KEY: - self.play(self.camera.frame.animate.to_default_state()) + self.camera.reset() elif char == "z" and modifiers == key.MOD_COMMAND: self.undo() elif char == "z" and modifiers == key.MOD_COMMAND | key.MOD_SHIFT: @@ -648,6 +656,28 @@ def on_hide(self) -> None: 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 intuitive 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: