From bef5eac40faea503dc9a84a74f9039b292d1f3da Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Sat, 20 Jun 2026 18:23:17 -0600 Subject: [PATCH 1/6] camera reset --- manim/camera/camera.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 From 99a10ffacd23767217a87d7f58e6a02a72374059 Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Sat, 20 Jun 2026 18:27:26 -0600 Subject: [PATCH 2/6] scene preview --- manim/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From f324ea73ef80ac1d478ef2df2eb997cdd9573e82 Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Sat, 20 Jun 2026 19:45:58 -0600 Subject: [PATCH 3/6] interaction methods --- manim/renderer/opengl_renderer_window.py | 127 ++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index cd8f5c619d..e7f3c31403 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,126 @@ 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 + + """ + 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 From 2cd6d8ef581a927f051d4e4aac502ca2611ed87d Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Sat, 20 Jun 2026 19:49:16 -0600 Subject: [PATCH 4/6] proper transformation --- .../renderer/shaders/include/position_point_into_frame.glsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manim/renderer/shaders/include/position_point_into_frame.glsl b/manim/renderer/shaders/include/position_point_into_frame.glsl index d90c099d10..b19752433c 100644 --- a/manim/renderer/shaders/include/position_point_into_frame.glsl +++ b/manim/renderer/shaders/include/position_point_into_frame.glsl @@ -21,9 +21,9 @@ vec3 position_point_into_frame(vec3 point) } if (bool(is_fixed_orientation)) { - vec3 new_center = rotate_point_into_frame(fixed_orientation_center); - return point + (new_center - fixed_orientation_center); + + return point - camera_rotation * camera_center + camera_rotation * fixed_orientation_center - fixed_orientation_center; } - return rotate_point_into_frame(point - camera_center); + return camera_rotation*(point - camera_center); } #endif // POSITION_POINT_INTO_FRAME_GLSL From c686b822808e9959dfadf14e69fcd4f3293ef0b8 Mon Sep 17 00:00:00 2001 From: Colin Rubow Date: Sat, 20 Jun 2026 19:58:11 -0600 Subject: [PATCH 5/6] tying interaction together --- manim/scene/scene.py | 75 +++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index db6e9b58c4..a02a3ba298 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -51,6 +51,7 @@ ZOOM_KEY = "z" RESET_FRAME_KEY = "r" QUIT_KEY = "q" +GET_ORIENTATION_KEY = "o" class Scene: @@ -177,6 +178,7 @@ def find_groups(self) -> list[SceneGroup]: def _update_mobjects(self, dt: float) -> None: for mobject in self.mobjects: mobject.update(dt) + self.camera.update(dt) def should_update_mobjects(self) -> bool: """ @@ -191,7 +193,7 @@ def should_update_mobjects(self) -> bool: return ( self.always_update_mobjects or (len(self.updaters) > 0) - or any(mob.has_updaters for mob in self.mobjects) + or any(mob.has_updaters for mob in [*self.mobjects, self.camera]) ) def is_current_animation_frozen_frame( @@ -525,6 +527,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 +539,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 +573,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 +584,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 +594,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 +603,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 +634,12 @@ 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 == 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 @@ 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: From ed8e9a9e2085515481bc8fdebfd08a6f4036943d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 02:07:31 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/renderer/opengl_renderer_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index e7f3c31403..8ed5d10e25 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -235,5 +235,6 @@ def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> N 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