Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions manimlib/default_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ scene:
preview_while_skipping: True
# How long does a scene pause on Scene.wait calls
default_wait_time: 1.0
fixed_aspect_ratio: False
vmobject:
default_stroke_width: 4.0
default_stroke_color: "#DDDDDD" # Default is GREY_A
Expand Down
36 changes: 30 additions & 6 deletions manimlib/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import numpy as np
from tqdm.auto import tqdm as ProgressDisplay
from pyglet.window import key as PygletWindowKeys
import pyglet

from manimlib.animation.animation import prepare_animation
from manimlib.camera.camera import Camera
Expand Down Expand Up @@ -75,6 +76,7 @@ def __init__(
preview_while_skipping: bool = True,
presenter_mode: bool = False,
default_wait_time: float = 1.0,
fixed_aspect_ratio: bool = False,
):
self.skip_animations = skip_animations
self.always_update_mobjects = always_update_mobjects
Expand All @@ -85,6 +87,7 @@ def __init__(
self.preview_while_skipping = preview_while_skipping
self.presenter_mode = presenter_mode
self.default_wait_time = default_wait_time
self.fixed_aspect_ratio = fixed_aspect_ratio

self.camera_config = merge_dicts_recursively(
manim_config.camera, # Global default
Expand All @@ -109,6 +112,9 @@ def __init__(
samples=self.samples,
**self.camera_config
)
if self.window:
# Ensure viewport or frame matches current window size
self.on_resize(*self.window.size)
self.frame: CameraFrame = self.camera.frame
self.frame.reorient(*self.default_frame_orientation)
self.frame.make_orientation_default()
Expand Down Expand Up @@ -242,11 +248,14 @@ def update_frame(self, dt: float = 0, force_draw: bool = False) -> None:
if self.is_window_closing():
raise EndScene()

if self.window and dt == 0 and not self.window.has_undrawn_event() and not force_draw:
# In this case, there's no need for new rendering, but we
# shoudl still listen for new events
self.window._window.dispatch_events()
return
if self.window:
pyglet.app.platform_event_loop.dispatch_posted_events()
pyglet.app.platform_event_loop.step(0)
self.window.dispatch_events()
if dt == 0 and not self.window.has_undrawn_event() and not force_draw:
# In this case, there's no need for new rendering, but we
# should still listen for new events
return

self.camera.capture(*self.render_groups)

Expand Down Expand Up @@ -854,7 +863,22 @@ def on_key_press(
self.hold_on_wait = False

def on_resize(self, width: int, height: int) -> None:
pass
if not hasattr(self, 'camera'):
return

if self.fixed_aspect_ratio:
aspect = self.camera.frame.get_aspect_ratio()
window_aspect = width / height
if window_aspect > aspect:
vp_height = height
vp_width = int(vp_height * aspect)
else:
vp_width = width
vp_height = int(vp_width / aspect)
vp_x = (width - vp_width) // 2
vp_y = (height - vp_height) // 2
if self.window:
self.window.ctx.viewport = (vp_x, vp_y, vp_width, vp_height)

def on_show(self) -> None:
pass
Expand Down
112 changes: 85 additions & 27 deletions manimlib/window.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from __future__ import annotations

import numpy as np

import moderngl_window as mglw
from moderngl_window.context.pyglet.window import Window as PygletWindow
from moderngl_window.timers.clock import Timer
from functools import wraps

import moderngl
import numpy as np
import pyglet
import screeninfo

from pyglet.window import Window as PygletWindow

from manimlib.constants import ASPECT_RATIO
from manimlib.constants import FRAME_SHAPE

Expand All @@ -33,17 +34,42 @@ def __init__(
position_string: str = "UR",
monitor_index: int = 1,
full_screen: bool = False,
style: str = None,
size: Optional[tuple[int, int]] = None,
position: Optional[tuple[int, int]] = None,
samples: int = 0
samples: int = 0,
):
self.scene = scene
self.monitor = self.get_monitor(monitor_index)
self.default_size = size or self.get_default_size(full_screen)
self.default_position = position or self.position_from_string(position_string)
self.pressed_keys = set()
self._has_undrawn_event = True

super().__init__(samples=samples)
config = pyglet.gl.Config(
sample_buffers=1 if samples > 0 else 0,
samples=samples,
major_version=self.gl_version[0],
minor_version=self.gl_version[1],
double_buffer=True,
depth_size=24,
stencil_size=8,
)

pyglet.app.platform_event_loop.start()

super().__init__(
width=self.default_size[0],
height=self.default_size[1],
resizable=self.resizable,
vsync=self.vsync,
fullscreen=full_screen,
style=style,
config=config,
)

self.set_mouse_visible(self.cursor)
self.set_mouse_passthrough(False)
self.to_default_position()

if self.scene:
Expand All @@ -60,18 +86,34 @@ def init_for_scene(self, scene: Scene):
self._has_undrawn_event = True

self.scene = scene
self.title = str(scene)
self.set_caption(str(scene))

self.init_mgl_context()

self.timer = Timer()
self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer)
mglw.activate_context(window=self, ctx=self.ctx)
self.timer.start()

# This line seems to resync the viewport
self.on_resize(*self.size)

def init_mgl_context(self) -> None:
self.ctx = moderngl.create_context()
self.ctx.viewport = (0, 0, self.width, self.height)

# Helper properties for width/height and position
@property
def size(self) -> tuple[int, int]:
return (self.width, self.height)

@size.setter
def size(self, size: tuple[int, int]) -> None:
self.set_size(*size)

@property
def position(self) -> tuple[int, int]:
return self.get_location()

@position.setter
def position(self, pos: tuple[int, int]) -> None:
self.set_location(*pos)

def get_monitor(self, index):
try:
monitors = screeninfo.get_monitors()
Expand Down Expand Up @@ -105,8 +147,8 @@ def focus(self):
flicker on the window but at least reliably focuses it. It may also
offset the window position slightly.
"""
self._window.set_visible(False)
self._window.set_visible(True)
self.set_visible(False)
self.set_visible(True)

def to_default_position(self):
self.position = self.default_position
Expand Down Expand Up @@ -139,8 +181,14 @@ def pixel_coords_to_space_coords(
def has_undrawn_event(self) -> bool:
return self._has_undrawn_event

def swap_buffers(self):
super().swap_buffers()
def clear(self, r: float = 0.0, g: float = 0.0, b: float = 0.0, a: float = 1.0) -> None:
if hasattr(self, "ctx"):
self.ctx.clear(r, g, b, a, depth=1.0)
else:
super().clear()

def swap_buffers(self) -> None:
self.flip()
self._has_undrawn_event = False

@staticmethod
Expand All @@ -153,7 +201,6 @@ def wrapper(self, *args, **kwargs):

@note_undrawn_event
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
super().on_mouse_motion(x, y, dx, dy)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
Expand All @@ -162,7 +209,6 @@ def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:

@note_undrawn_event
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
Expand All @@ -171,23 +217,20 @@ def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifier

@note_undrawn_event
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
super().on_mouse_press(x, y, button, mods)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
self.scene.on_mouse_press(point, button, mods)

@note_undrawn_event
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
super().on_mouse_release(x, y, button, mods)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
self.scene.on_mouse_release(point, button, mods)

@note_undrawn_event
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
super().on_mouse_scroll(x, y, x_offset, y_offset)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
Expand All @@ -196,37 +239,41 @@ def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> N

@note_undrawn_event
def on_key_press(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.add(symbol) # Modifiers?
super().on_key_press(symbol, modifiers)
if symbol == pyglet.window.key.F11:
if self.width == self.screen.width * self.screen.get_scale():
self.to_default_position()
else:
self.maximize()
return
self.pressed_keys.add(symbol) # Modifiers?
if not self.scene:
return
self.scene.on_key_press(symbol, modifiers)

@note_undrawn_event
def on_key_release(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.difference_update({symbol}) # Modifiers?
super().on_key_release(symbol, modifiers)
if not self.scene:
return
self.scene.on_key_release(symbol, modifiers)

@note_undrawn_event
def on_resize(self, width: int, height: int) -> None:
super().on_resize(width, height)
if hasattr(self, 'ctx'):
self.ctx.viewport = (0, 0, width, height)
if not self.scene:
return
self.scene.on_resize(width, height)

@note_undrawn_event
def on_show(self) -> None:
super().on_show()
if not self.scene:
return
self.scene.on_show()

@note_undrawn_event
def on_hide(self) -> None:
super().on_hide()
if not self.scene:
return
self.scene.on_hide()
Expand All @@ -240,3 +287,14 @@ def on_close(self) -> None:

def is_key_pressed(self, symbol: int) -> bool:
return (symbol in self.pressed_keys)

# Methods for compatibility with previous window wrapper
@property
def is_closing(self) -> bool:
return self.has_exit

def destroy(self) -> None:
if hasattr(self, "ctx"):
self.ctx.release()
self.close()
pyglet.app.platform_event_loop.stop()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pyglet @ git+https://github.com/pyglet/pyglet.git@f08debc
addict
appdirs
audioop-lts; python_version>='3.13'
Expand All @@ -10,10 +11,10 @@ manimpango>=0.6.0
mapbox-earcut
matplotlib
moderngl
moderngl_window
numpy
Pillow
pydub
pyglet
pygments
PyOpenGL
pyperclip
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ install_requires =
mapbox-earcut
matplotlib
moderngl
moderngl_window
numpy
Pillow
pydub
pyglet
pygments
PyOpenGL
pyperclip
Expand Down