From 41e2126eca7bf585b075548172a4d26185667b33 Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Mon, 4 May 2026 15:43:21 -0600 Subject: [PATCH 1/3] d and f keys are mapped to motion --- manim/camera/camera.py | 2 +- manim/manager.py | 3 +++ manim/mobject/opengl/opengl_mobject.py | 1 + manim/renderer/opengl_renderer_window.py | 8 ++++++++ manim/scene/scene.py | 19 ++++++++----------- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index a05bec3e2a..a39181ae88 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -248,7 +248,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..9961d08f11 100644 --- a/manim/manager.py +++ b/manim/manager.py @@ -252,6 +252,9 @@ def _interact(self) -> None: dt = current_time - last_time last_time = current_time self._update_frame(dt) + # update the window pressed keys attribute + # FIXME: need to get mouse info for this + self.scene.on_mouse_motion(np.array([0, 0, 0]), np.array([0.01, 0.01, 0])) @contextlib.contextmanager def no_render(self) -> Iterator[None]: 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..de2e730b7d 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -109,6 +109,14 @@ 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: + self.pressed_keys.add(symbol) + return super().on_key_press(symbol, modifiers) + + def on_key_release(self, symbol: int, modifiers: int) -> None: + self.pressed_keys.remove(symbol) + return super().on_key_release(symbol, modifiers) + 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..ad304fa38c 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -534,21 +534,18 @@ 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 ord(PAN_3D_KEY) in self.manager.window.pressed_keys: + 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 ord(FRAME_SHIFT_KEY) in self.manager.window.pressed_keys: 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 From 8880bfc556def36215684fd62f2b017cfbd69747 Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Tue, 5 May 2026 08:48:52 -0600 Subject: [PATCH 2/3] keyboard and mouse interaction --- manim/manager.py | 5 +-- manim/renderer/opengl_renderer_window.py | 26 +++++++++++-- manim/scene/scene.py | 48 ++++++++++++++++++++---- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/manim/manager.py b/manim/manager.py index 9961d08f11..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. @@ -252,9 +252,6 @@ def _interact(self) -> None: dt = current_time - last_time last_time = current_time self._update_frame(dt) - # update the window pressed keys attribute - # FIXME: need to get mouse info for this - self.scene.on_mouse_motion(np.array([0, 0, 0]), np.array([0.01, 0.01, 0])) @contextlib.contextmanager def no_render(self) -> Iterator[None]: diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index de2e730b7d..f0fe3fa795 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -1,6 +1,8 @@ from __future__ import annotations +import numpy as np from typing import TYPE_CHECKING, TypeVar +from manim.typing import Point3D import moderngl_window as mglw from moderngl_window.context.pyglet.window import Window as PygletWindow @@ -9,6 +11,7 @@ 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 +32,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 +68,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 @@ -110,12 +114,26 @@ def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]: ) def on_key_press(self, symbol: int, modifiers: int) -> bool: - self.pressed_keys.add(symbol) + self.scene.on_key_press(symbol, modifiers) return super().on_key_press(symbol, modifiers) def on_key_release(self, symbol: int, modifiers: int) -> None: - self.pressed_keys.remove(symbol) - return super().on_key_release(symbol, modifiers) + self.scene.on_key_release(symbol, modifiers) + + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: + 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: + 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: + 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: + 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: + 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]]: diff --git a/manim/scene/scene.py b/manim/scene/scene.py index ad304fa38c..4891796112 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} @@ -535,11 +538,11 @@ def on_mouse_motion(self, point: Point3D, d_point: Vector3D) -> None: return # Handle perspective changes - if ord(PAN_3D_KEY) in self.manager.window.pressed_keys: + 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 ord(FRAME_SHIFT_KEY) in self.manager.window.pressed_keys: + elif EVENT_DISPATCHER.is_key_pressed(ord(FRAME_SHIFT_KEY)): shift = -d_point shift[0] *= self.camera.get_width() / 2 shift[1] *= self.camera.get_height() / 2 @@ -550,6 +553,9 @@ def on_mouse_motion(self, point: Point3D, d_point: Vector3D) -> None: 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 = { @@ -565,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( @@ -574,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 @@ -582,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 @@ -589,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} @@ -645,6 +656,27 @@ 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 untuitive 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: From 7e44326daf30b8851c170caf6fa1e99f664ddad1 Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Tue, 5 May 2026 13:48:18 -0600 Subject: [PATCH 3/3] feat: allow basic keyboard and mouse interaction in opengl renderer window --- manim/camera/camera.py | 5 + manim/renderer/opengl_renderer_window.py | 113 +++++++++++++++++++++-- manim/scene/scene.py | 41 ++++---- 3 files changed, 133 insertions(+), 26 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index a39181ae88..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, diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index f0fe3fa795..672158e635 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -1,10 +1,9 @@ from __future__ import annotations -import numpy as np from typing import TYPE_CHECKING, TypeVar -from manim.typing import Point3D 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 @@ -32,7 +31,9 @@ class Window(PygletWindow, WindowProtocol): vsync: bool = True cursor: bool = True - def __init__(self, scene: Scene, 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 @@ -114,26 +115,126 @@ def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]: ) 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: - self.scene.on_mouse_drag(np.array([x, y, 0]), np.array([dx, dy, 0]), buttons, modifiers) + 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: - self.scene.on_mouse_scroll(np.array([x, y, 0]), np.array([x_offset, y_offset, 0])) + """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]]: diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 4891796112..02903468c8 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -602,12 +602,12 @@ def on_mouse_scroll(self, point: Point3D, offset: Vector3D) -> None: return if EVENT_DISPATCHER.is_key_pressed(ord(ZOOM_KEY)): - factor = 1/1.25 if offset[1] > 0 else 1.25 + factor = 1 / 1.25 if offset[1] > 0 else 1.25 self.camera.scale(factor, about_point=point) else: transform = self.camera.get_inverse_rotation_matrix() shift = np.dot(np.transpose(transform), offset) - self.camera.shift(-shift/2) + self.camera.shift(-shift / 2) def on_key_release(self, symbol: int, modifiers: int) -> None: event_data = {"symbol": symbol, "modifiers": modifiers} @@ -632,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: @@ -657,25 +657,26 @@ 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 - ]) + """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 untuitive 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 - ]) + """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: