mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
commit
485a4ca33a
19 changed files with 688 additions and 159 deletions
|
@ -579,7 +579,7 @@ class SurfaceExample(Scene):
|
|||
self.play(light.animate.move_to(3 * IN), run_time=5)
|
||||
self.play(light.animate.shift(10 * OUT), run_time=5)
|
||||
|
||||
drag_text = Text("Try moving the mouse while pressing d or s")
|
||||
drag_text = Text("Try moving the mouse while pressing d or f")
|
||||
drag_text.move_to(light_text)
|
||||
drag_text.fix_in_frame()
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ from manimlib.mobject.value_tracker import *
|
|||
from manimlib.mobject.vector_field import *
|
||||
|
||||
from manimlib.scene.scene import *
|
||||
from manimlib.scene.interactive_scene import *
|
||||
from manimlib.scene.three_d_scene import *
|
||||
|
||||
from manimlib.utils.bezier import *
|
||||
|
|
|
@ -52,6 +52,7 @@ class Animation(object):
|
|||
# played. As much initialization as possible,
|
||||
# especially any mobject copying, should live in
|
||||
# this method
|
||||
self.mobject.set_animating_status(True)
|
||||
self.starting_mobject = self.create_starting_mobject()
|
||||
if self.suspend_mobject_updating:
|
||||
# All calls to self.mobject's internal updaters
|
||||
|
@ -66,6 +67,7 @@ class Animation(object):
|
|||
|
||||
def finish(self) -> None:
|
||||
self.interpolate(self.final_alpha_value)
|
||||
self.mobject.set_animating_status(False)
|
||||
if self.suspend_mobject_updating:
|
||||
self.mobject.resume_updating()
|
||||
|
||||
|
|
|
@ -50,11 +50,13 @@ class AnimationGroup(Animation):
|
|||
return self.group
|
||||
|
||||
def begin(self) -> None:
|
||||
self.group.set_animating_status(True)
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
# self.init_run_time()
|
||||
|
||||
def finish(self) -> None:
|
||||
self.group.set_animating_status(False)
|
||||
for anim in self.animations:
|
||||
anim.finish()
|
||||
|
||||
|
|
|
@ -60,6 +60,15 @@ class CameraFrame(Mobject):
|
|||
def get_euler_angles(self):
|
||||
return self.get_orientation().as_euler("zxz")[::-1]
|
||||
|
||||
def get_theta(self):
|
||||
return self.get_euler_angles()[0]
|
||||
|
||||
def get_phi(self):
|
||||
return self.get_euler_angles()[1]
|
||||
|
||||
def get_gamma(self):
|
||||
return self.get_euler_angles()[2]
|
||||
|
||||
def get_inverse_camera_rotation_matrix(self):
|
||||
return self.get_orientation().as_matrix().T
|
||||
|
||||
|
@ -190,7 +199,10 @@ class Camera(object):
|
|||
self.init_textures()
|
||||
self.init_light_source()
|
||||
self.refresh_perspective_uniforms()
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
# A cached map from mobjects to their associated list of render groups
|
||||
# so that these render groups are not regenerated unnecessarily for static
|
||||
# mobjects
|
||||
self.mob_to_render_groups = {}
|
||||
|
||||
def init_frame(self) -> None:
|
||||
self.frame = CameraFrame(**self.frame_config)
|
||||
|
@ -356,11 +368,21 @@ class Camera(object):
|
|||
if render_group["single_use"]:
|
||||
self.release_render_group(render_group)
|
||||
|
||||
def get_render_group_list(self, mobject: Mobject) -> list[dict[str]] | map[dict[str]]:
|
||||
try:
|
||||
return self.static_mobject_to_render_group_list[id(mobject)]
|
||||
except KeyError:
|
||||
return map(self.get_render_group, mobject.get_shader_wrapper_list())
|
||||
def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str]]:
|
||||
if mobject.is_changing():
|
||||
return self.generate_render_group_list(mobject)
|
||||
|
||||
# Otherwise, cache result for later use
|
||||
key = id(mobject)
|
||||
if key not in self.mob_to_render_groups:
|
||||
self.mob_to_render_groups[key] = list(self.generate_render_group_list(mobject))
|
||||
return self.mob_to_render_groups[key]
|
||||
|
||||
def generate_render_group_list(self, mobject: Mobject) -> Iterable[dict[str]]:
|
||||
return (
|
||||
self.get_render_group(sw, single_use=mobject.is_changing())
|
||||
for sw in mobject.get_shader_wrapper_list()
|
||||
)
|
||||
|
||||
def get_render_group(
|
||||
self,
|
||||
|
@ -399,19 +421,10 @@ class Camera(object):
|
|||
if render_group[key] is not None:
|
||||
render_group[key].release()
|
||||
|
||||
def set_mobjects_as_static(self, *mobjects: Mobject) -> None:
|
||||
# Creates buffer and array objects holding each mobjects shader data
|
||||
for mob in mobjects:
|
||||
self.static_mobject_to_render_group_list[id(mob)] = [
|
||||
self.get_render_group(sw, single_use=False)
|
||||
for sw in mob.get_shader_wrapper_list()
|
||||
]
|
||||
|
||||
def release_static_mobjects(self) -> None:
|
||||
for rg_list in self.static_mobject_to_render_group_list.values():
|
||||
for render_group in rg_list:
|
||||
self.release_render_group(render_group)
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
def refresh_static_mobjects(self) -> None:
|
||||
for render_group in it.chain(*self.mob_to_render_groups.values()):
|
||||
self.release_render_group(render_group)
|
||||
self.mob_to_render_groups = {}
|
||||
|
||||
# Shaders
|
||||
def init_shaders(self) -> None:
|
||||
|
|
|
@ -71,6 +71,16 @@ BOLD = "BOLD"
|
|||
|
||||
DEFAULT_STROKE_WIDTH = 4
|
||||
|
||||
# For keyboard interactions
|
||||
CTRL_SYMBOL = 65508
|
||||
SHIFT_SYMBOL = 65505
|
||||
DELETE_SYMBOL = 65288
|
||||
ARROW_SYMBOLS = list(range(65361, 65365))
|
||||
|
||||
SHIFT_MODIFIER = 1
|
||||
CTRL_MODIFIER = 2
|
||||
COMMAND_MODIFIER = 64
|
||||
|
||||
# Colors
|
||||
BLUE_E = "#1C758A"
|
||||
BLUE_D = "#29ABCA"
|
||||
|
@ -127,6 +137,20 @@ LIGHT_PINK = "#DC75CD"
|
|||
GREEN_SCREEN = "#00FF00"
|
||||
ORANGE = "#FF862F"
|
||||
|
||||
MANIM_COLORS = [
|
||||
BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE,
|
||||
BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A,
|
||||
TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A,
|
||||
GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A,
|
||||
YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A,
|
||||
GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A,
|
||||
RED_E, RED_D, RED_C, RED_B, RED_A,
|
||||
MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A,
|
||||
PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A,
|
||||
GREY_BROWN, DARK_BROWN, LIGHT_BROWN,
|
||||
PINK, LIGHT_PINK,
|
||||
]
|
||||
|
||||
# Abbreviated names for the "median" colors
|
||||
BLUE = BLUE_C
|
||||
TEAL = TEAL_C
|
||||
|
|
|
@ -320,8 +320,8 @@ class Circle(Arc):
|
|||
"anchors_span_full_range": False
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
Arc.__init__(self, 0, TAU, **kwargs)
|
||||
def __init__(self, start_angle: float = 0, **kwargs):
|
||||
Arc.__init__(self, start_angle, TAU, **kwargs)
|
||||
|
||||
def surround(
|
||||
self,
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import copy
|
||||
import random
|
||||
import itertools as it
|
||||
from functools import wraps
|
||||
from typing import Iterable, Callable, Union, Sequence
|
||||
import pickle
|
||||
import os
|
||||
|
||||
import colour
|
||||
import moderngl
|
||||
|
@ -23,7 +24,6 @@ from manimlib.utils.iterables import list_update
|
|||
from manimlib.utils.iterables import resize_array
|
||||
from manimlib.utils.iterables import resize_preserving_order
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.iterables import make_even
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
|
@ -37,6 +37,7 @@ from manimlib.shader_wrapper import get_colormap_code
|
|||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.event_handler.event_listner import EventListner
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
TimeBasedUpdater = Callable[["Mobject", float], None]
|
||||
|
@ -71,7 +72,7 @@ class Mobject(object):
|
|||
# Must match in attributes of vert shader
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
@ -81,6 +82,8 @@ class Mobject(object):
|
|||
self.family: list[Mobject] = [self]
|
||||
self.locked_data_keys: set[str] = set()
|
||||
self.needs_new_bounding_box: bool = True
|
||||
self._is_animating: bool = False
|
||||
self._is_movable: bool = False
|
||||
|
||||
self.init_data()
|
||||
self.init_uniforms()
|
||||
|
@ -265,15 +268,30 @@ class Mobject(object):
|
|||
parent.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def is_point_touching(
|
||||
def are_points_touching(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
points: np.ndarray,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
) -> bool:
|
||||
bb = self.get_bounding_box()
|
||||
mins = (bb[0] - buff)
|
||||
maxs = (bb[2] + buff)
|
||||
return (point >= mins).all() and (point <= maxs).all()
|
||||
return ((points >= mins) * (points <= maxs)).all(1)
|
||||
|
||||
def is_point_touching(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
) -> bool:
|
||||
return self.are_points_touching(np.array(point, ndmin=2), buff)[0]
|
||||
|
||||
def is_touching(self, mobject: Mobject, buff: float = 1e-2) -> bool:
|
||||
bb1 = self.get_bounding_box()
|
||||
bb2 = mobject.get_bounding_box()
|
||||
return not any((
|
||||
(bb2[2] < bb1[0] - buff).any(), # E.g. Right of mobject is left of self's left
|
||||
(bb2[0] > bb1[2] + buff).any(), # E.g. Left of mobject is right of self's right
|
||||
))
|
||||
|
||||
# Family matters
|
||||
|
||||
|
@ -421,22 +439,6 @@ class Mobject(object):
|
|||
self.center()
|
||||
return self
|
||||
|
||||
def replicate(self, n: int) -> Group:
|
||||
return self.get_group_class()(
|
||||
*(self.copy() for x in range(n))
|
||||
)
|
||||
|
||||
def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs):
|
||||
"""
|
||||
Returns a new mobject containing multiple copies of this one
|
||||
arranged in a grid
|
||||
"""
|
||||
grid = self.replicate(n_rows * n_cols)
|
||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||
if height is not None:
|
||||
grid.set_height(height)
|
||||
return grid
|
||||
|
||||
def sort(
|
||||
self,
|
||||
point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0],
|
||||
|
@ -457,67 +459,46 @@ class Mobject(object):
|
|||
self.assemble_family()
|
||||
return self
|
||||
|
||||
# Creating new Mobjects from this one
|
||||
|
||||
def replicate(self, n: int) -> Group:
|
||||
return self.get_group_class()(
|
||||
*(self.copy() for x in range(n))
|
||||
)
|
||||
|
||||
def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs):
|
||||
"""
|
||||
Returns a new mobject containing multiple copies of this one
|
||||
arranged in a grid
|
||||
"""
|
||||
grid = self.replicate(n_rows * n_cols)
|
||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||
if height is not None:
|
||||
grid.set_height(height)
|
||||
return grid
|
||||
|
||||
# Copying
|
||||
|
||||
def copy(self):
|
||||
# TODO, either justify reason for shallow copy, or
|
||||
# remove this redundancy everywhere
|
||||
# return self.deepcopy()
|
||||
|
||||
parents = self.parents
|
||||
self.parents = []
|
||||
copy_mobject = copy.copy(self)
|
||||
self.parents = parents
|
||||
|
||||
copy_mobject.data = dict(self.data)
|
||||
for key in self.data:
|
||||
copy_mobject.data[key] = self.data[key].copy()
|
||||
|
||||
copy_mobject.uniforms = dict(self.uniforms)
|
||||
for key in self.uniforms:
|
||||
if isinstance(self.uniforms[key], np.ndarray):
|
||||
copy_mobject.uniforms[key] = self.uniforms[key].copy()
|
||||
|
||||
copy_mobject.submobjects = []
|
||||
copy_mobject.add(*[sm.copy() for sm in self.submobjects])
|
||||
copy_mobject.match_updaters(self)
|
||||
|
||||
copy_mobject.needs_new_bounding_box = self.needs_new_bounding_box
|
||||
|
||||
# Make sure any mobject or numpy array attributes are copied
|
||||
family = self.get_family()
|
||||
for attr, value in list(self.__dict__.items()):
|
||||
if isinstance(value, Mobject) and value in family and value is not self:
|
||||
setattr(copy_mobject, attr, value.copy())
|
||||
if isinstance(value, np.ndarray):
|
||||
setattr(copy_mobject, attr, value.copy())
|
||||
if isinstance(value, ShaderWrapper):
|
||||
setattr(copy_mobject, attr, value.copy())
|
||||
return copy_mobject
|
||||
return pickle.loads(pickle.dumps(self))
|
||||
|
||||
def deepcopy(self):
|
||||
parents = self.parents
|
||||
self.parents = []
|
||||
result = copy.deepcopy(self)
|
||||
self.parents = parents
|
||||
return result
|
||||
# This used to be different from copy, so is now just here for backward compatibility
|
||||
return self.copy()
|
||||
|
||||
def generate_target(self, use_deepcopy: bool = False):
|
||||
# TODO, remove now pointless use_deepcopy arg
|
||||
self.target = None # Prevent exponential explosion
|
||||
if use_deepcopy:
|
||||
self.target = self.deepcopy()
|
||||
else:
|
||||
self.target = self.copy()
|
||||
self.target = self.copy()
|
||||
return self.target
|
||||
|
||||
def save_state(self, use_deepcopy: bool = False):
|
||||
# TODO, remove now pointless use_deepcopy arg
|
||||
if hasattr(self, "saved_state"):
|
||||
# Prevent exponential growth of data
|
||||
self.saved_state = None
|
||||
if use_deepcopy:
|
||||
self.saved_state = self.deepcopy()
|
||||
else:
|
||||
self.saved_state = self.copy()
|
||||
self.saved_state = self.copy()
|
||||
return self
|
||||
|
||||
def restore(self):
|
||||
|
@ -526,6 +507,27 @@ class Mobject(object):
|
|||
self.become(self.saved_state)
|
||||
return self
|
||||
|
||||
def save_to_file(self, file_path):
|
||||
if not file_path.endswith(".mob"):
|
||||
file_path += ".mob"
|
||||
if os.path.exists(file_path):
|
||||
cont = input(f"{file_path} already exists. Overwrite (y/n)? ")
|
||||
if cont != "y":
|
||||
return
|
||||
with open(file_path, "wb") as fp:
|
||||
pickle.dump(self, fp)
|
||||
log.info(f"Saved mobject to {file_path}")
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def load(file_path):
|
||||
if not os.path.exists(file_path):
|
||||
log.error(f"No file found at {file_path}")
|
||||
sys.exit(2)
|
||||
with open(file_path, "rb") as fp:
|
||||
mobject = pickle.load(fp)
|
||||
return mobject
|
||||
|
||||
# Updating
|
||||
|
||||
def init_updaters(self):
|
||||
|
@ -575,6 +577,8 @@ class Mobject(object):
|
|||
updater_list.insert(index, update_function)
|
||||
|
||||
self.refresh_has_updater_status()
|
||||
for parent in self.parents:
|
||||
parent.has_updaters = True
|
||||
if call_updater:
|
||||
self.update(dt=0)
|
||||
return self
|
||||
|
@ -589,10 +593,10 @@ class Mobject(object):
|
|||
def clear_updaters(self, recurse: bool = True):
|
||||
self.time_based_updaters = []
|
||||
self.non_time_updaters = []
|
||||
self.refresh_has_updater_status()
|
||||
if recurse:
|
||||
for submob in self.submobjects:
|
||||
submob.clear_updaters()
|
||||
self.refresh_has_updater_status()
|
||||
return self
|
||||
|
||||
def match_updaters(self, mobject: Mobject):
|
||||
|
@ -623,6 +627,24 @@ class Mobject(object):
|
|||
self.has_updaters = any(mob.get_updaters() for mob in self.get_family())
|
||||
return self
|
||||
|
||||
# Check if mark as static or not for camera
|
||||
|
||||
def is_changing(self) -> bool:
|
||||
return self._is_animating or self.has_updaters or self._is_movable
|
||||
|
||||
def set_animating_status(self, is_animating: bool, recurse: bool = True) -> None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob._is_animating = is_animating
|
||||
return self
|
||||
|
||||
def make_movable(self, value: bool = True, recurse: bool = True) -> None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob._is_movable = value
|
||||
return self
|
||||
|
||||
def is_movable(self) -> bool:
|
||||
return self._is_movable
|
||||
|
||||
# Transforming operations
|
||||
|
||||
def shift(self, vector: np.ndarray):
|
||||
|
@ -1173,6 +1195,13 @@ class Mobject(object):
|
|||
def get_corner(self, direction: np.ndarray) -> np.ndarray:
|
||||
return self.get_bounding_box_point(direction)
|
||||
|
||||
def get_all_corners(self):
|
||||
bb = self.get_bounding_box()
|
||||
return np.array([
|
||||
[bb[indices[-i + 1]][i] for i in range(3)]
|
||||
for indices in it.product(*3 * [[0, 2]])
|
||||
])
|
||||
|
||||
def get_center(self) -> np.ndarray:
|
||||
return self.get_bounding_box()[1]
|
||||
|
||||
|
@ -1403,7 +1432,7 @@ class Mobject(object):
|
|||
return self
|
||||
|
||||
def push_self_into_submobjects(self):
|
||||
copy = self.deepcopy()
|
||||
copy = self.copy()
|
||||
copy.set_submobjects([])
|
||||
self.resize_points(0)
|
||||
self.add(copy)
|
||||
|
|
|
@ -274,6 +274,3 @@ class BarChart(VGroup):
|
|||
(value / self.max_value) * self.height
|
||||
)
|
||||
bar.move_to(bar_bottom, DOWN)
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
|
|
@ -123,9 +123,6 @@ class LabelledString(_StringSVG, ABC):
|
|||
self.group_substrs = self.get_group_substrs()
|
||||
self.submob_groups = self.get_submob_groups()
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
||||
# Toolkits
|
||||
|
||||
def get_substr(self, span: Span) -> str:
|
||||
|
|
|
@ -681,7 +681,7 @@ class VMobject(Mobject):
|
|||
self.get_end_anchors(),
|
||||
))))
|
||||
|
||||
def get_points_without_null_curves(self, atol: float=1e-9) -> np.ndarray:
|
||||
def get_points_without_null_curves(self, atol: float = 1e-9) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
points = self.get_points()
|
||||
distinct_curves = reduce(op.or_, [
|
||||
|
|
|
@ -326,7 +326,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
|||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
AnimationGroup.__init__(self, *[
|
||||
VShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
vmobject.copy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
)
|
||||
|
|
417
manimlib/scene/interactive_scene.py
Normal file
417
manimlib/scene/interactive_scene.py
Normal file
|
@ -0,0 +1,417 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
import pyperclip
|
||||
import os
|
||||
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.constants import MANIM_COLORS, WHITE, YELLOW
|
||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
|
||||
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
|
||||
from manimlib.constants import CTRL_SYMBOL, SHIFT_SYMBOL, DELETE_SYMBOL, ARROW_SYMBOLS
|
||||
from manimlib.constants import SHIFT_MODIFIER, COMMAND_MODIFIER
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.dot_cloud import DotCloud
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.utils.tex_file_writing import LatexError
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
SELECT_KEY = 's'
|
||||
GRAB_KEY = 'g'
|
||||
HORIZONTAL_GRAB_KEY = 'h'
|
||||
VERTICAL_GRAB_KEY = 'v'
|
||||
RESIZE_KEY = 't'
|
||||
COLOR_KEY = 'c'
|
||||
|
||||
|
||||
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||
|
||||
class InteractiveScene(Scene):
|
||||
"""
|
||||
TODO, Document
|
||||
|
||||
To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
|
||||
or just tap ctrl to select the mobject under the cursor.
|
||||
|
||||
Pressing command + t will toggle between modes where you either select top level
|
||||
mobjects part of the scene, or low level pieces.
|
||||
|
||||
Hold 'g' to grab the selection and move it around
|
||||
Hold 'h' to drag it constrained in the horizontal direction
|
||||
Hold 'v' to drag it constrained in the vertical direction
|
||||
Hold 't' to resize selection, adding 'shift' to resize with respect to a corner
|
||||
|
||||
Command + 'c' copies the ids of selections to clipboard
|
||||
Command + 'v' will paste either:
|
||||
- The copied mobject
|
||||
- A Tex mobject based on copied LaTeX
|
||||
- A Text mobject based on copied Text
|
||||
Command + 'z' restores selection back to its original state
|
||||
Command + 's' saves the selected mobjects to file
|
||||
"""
|
||||
corner_dot_config = dict(
|
||||
color=WHITE,
|
||||
radius=0.1,
|
||||
glow_factor=1.0,
|
||||
)
|
||||
selection_rectangle_stroke_color = WHITE
|
||||
selection_rectangle_stroke_width = 1.0
|
||||
colors = MANIM_COLORS
|
||||
selection_nudge_size = 0.05
|
||||
|
||||
def setup(self):
|
||||
self.selection = Group()
|
||||
self.selection_highlight = Group()
|
||||
self.selection_rectangle = self.get_selection_rectangle()
|
||||
self.color_palette = self.get_color_palette()
|
||||
self.unselectables = [
|
||||
self.selection,
|
||||
self.selection_highlight,
|
||||
self.selection_rectangle,
|
||||
self.camera.frame
|
||||
]
|
||||
self.saved_selection_state = []
|
||||
self.select_top_level_mobs = True
|
||||
|
||||
self.is_selecting = False
|
||||
self.add(self.selection_highlight)
|
||||
|
||||
def toggle_selection_mode(self):
|
||||
self.select_top_level_mobs = not self.select_top_level_mobs
|
||||
self.refresh_selection_scope()
|
||||
|
||||
def get_selection_search_set(self):
|
||||
mobs = [m for m in self.mobjects if m not in self.unselectables]
|
||||
if self.select_top_level_mobs:
|
||||
return mobs
|
||||
else:
|
||||
return [
|
||||
submob
|
||||
for mob in mobs
|
||||
for submob in mob.family_members_with_points()
|
||||
]
|
||||
|
||||
def refresh_selection_scope(self):
|
||||
curr = list(self.selection)
|
||||
if self.select_top_level_mobs:
|
||||
self.selection.set_submobjects([
|
||||
mob
|
||||
for mob in self.mobjects
|
||||
if any(sm in mob.get_family() for sm in curr)
|
||||
])
|
||||
self.selection.refresh_bounding_box(recurse_down=True)
|
||||
else:
|
||||
self.selection.set_submobjects(
|
||||
extract_mobject_family_members(
|
||||
curr, exclude_pointless=True,
|
||||
)
|
||||
)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def get_selection_rectangle(self):
|
||||
rect = Rectangle(
|
||||
stroke_color=self.selection_rectangle_stroke_color,
|
||||
stroke_width=self.selection_rectangle_stroke_width,
|
||||
)
|
||||
rect.fix_in_frame()
|
||||
rect.fixed_corner = ORIGIN
|
||||
rect.add_updater(self.update_selection_rectangle)
|
||||
return rect
|
||||
|
||||
def get_color_palette(self):
|
||||
palette = VGroup(*(
|
||||
Square(fill_color=color, fill_opacity=1, side_length=1)
|
||||
for color in self.colors
|
||||
))
|
||||
palette.set_stroke(width=0)
|
||||
palette.arrange(RIGHT, buff=0.5)
|
||||
palette.set_width(FRAME_WIDTH - 0.5)
|
||||
palette.to_edge(DOWN, buff=SMALL_BUFF)
|
||||
palette.fix_in_frame()
|
||||
return palette
|
||||
|
||||
def get_stroke_highlight(self, vmobject):
|
||||
outline = vmobject.copy()
|
||||
for sm, osm in zip(vmobject.get_family(), outline.get_family()):
|
||||
osm.set_fill(opacity=0)
|
||||
osm.set_stroke(YELLOW, width=sm.get_stroke_width() + 1.5)
|
||||
outline.add_updater(lambda o: o.replace(vmobject))
|
||||
return outline
|
||||
|
||||
def get_corner_dots(self, mobject):
|
||||
dots = DotCloud(**self.corner_dot_config)
|
||||
dots.add_updater(lambda d: d.set_points(mobject.get_all_corners()))
|
||||
dots.scale((dots.get_width() + dots.get_radius()) / dots.get_width())
|
||||
# Since for flat object, all 8 corners really appear as four, dim the dots
|
||||
if mobject.get_depth() < 1e-2:
|
||||
dots.set_opacity(0.5)
|
||||
return dots
|
||||
|
||||
def get_highlight(self, mobject):
|
||||
if isinstance(mobject, VMobject) and mobject.has_points():
|
||||
return self.get_stroke_highlight(mobject)
|
||||
else:
|
||||
return self.get_corner_dots(mobject)
|
||||
|
||||
def refresh_selection_highlight(self):
|
||||
self.selection_highlight.set_submobjects([
|
||||
self.get_highlight(mob)
|
||||
for mob in self.selection
|
||||
])
|
||||
|
||||
def update_selection_rectangle(self, rect):
|
||||
p1 = rect.fixed_corner
|
||||
p2 = self.mouse_point.get_center()
|
||||
rect.set_points_as_corners([
|
||||
p1, [p2[0], p1[1], 0],
|
||||
p2, [p1[0], p2[1], 0],
|
||||
p1,
|
||||
])
|
||||
return rect
|
||||
|
||||
def add_to_selection(self, *mobjects):
|
||||
for mob in mobjects:
|
||||
if mob in self.unselectables:
|
||||
continue
|
||||
if mob not in self.selection:
|
||||
self.selection.add(mob)
|
||||
self.selection_highlight.add(self.get_highlight(mob))
|
||||
self.saved_selection_state = [
|
||||
(mob, mob.copy())
|
||||
for mob in self.selection
|
||||
]
|
||||
|
||||
def toggle_from_selection(self, *mobjects):
|
||||
for mob in mobjects:
|
||||
if mob in self.selection:
|
||||
self.selection.remove(mob)
|
||||
else:
|
||||
self.add_to_selection(mob)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def clear_selection(self):
|
||||
self.selection.set_submobjects([])
|
||||
self.selection_highlight.set_submobjects([])
|
||||
|
||||
def add(self, *new_mobjects: Mobject):
|
||||
for mob in new_mobjects:
|
||||
mob.make_movable()
|
||||
super().add(*new_mobjects)
|
||||
|
||||
# Selection operations
|
||||
|
||||
def copy_selection(self):
|
||||
ids = map(id, self.selection)
|
||||
pyperclip.copy(",".join(map(str, ids)))
|
||||
|
||||
def paste_selection(self):
|
||||
clipboard_str = pyperclip.paste()
|
||||
# Try pasting a mobject
|
||||
try:
|
||||
ids = map(int, clipboard_str.split(","))
|
||||
mobs = map(self.id_to_mobject, ids)
|
||||
mob_copies = [m.copy() for m in mobs if m is not None]
|
||||
self.clear_selection()
|
||||
self.add_to_selection(*mob_copies)
|
||||
self.play(*(
|
||||
FadeIn(mc, run_time=0.5, scale=1.5)
|
||||
for mc in mob_copies
|
||||
))
|
||||
return
|
||||
except ValueError:
|
||||
pass
|
||||
# Otherwise, treat as tex or text
|
||||
if "\\" in clipboard_str: # Proxy to text for LaTeX
|
||||
try:
|
||||
new_mob = Tex(clipboard_str)
|
||||
except LatexError:
|
||||
return
|
||||
else:
|
||||
new_mob = Text(clipboard_str)
|
||||
self.clear_selection()
|
||||
self.add(new_mob)
|
||||
self.add_to_selection(new_mob)
|
||||
new_mob.move_to(self.mouse_point)
|
||||
|
||||
def delete_selection(self):
|
||||
self.remove(*self.selection)
|
||||
self.clear_selection()
|
||||
|
||||
def saved_selection_to_file(self):
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
files = os.listdir(directory)
|
||||
for mob in self.selection:
|
||||
file_name = str(mob) + "_0.mob"
|
||||
index = 0
|
||||
while file_name in files:
|
||||
file_name = file_name.replace(str(index), str(index + 1))
|
||||
index += 1
|
||||
user_name = input(
|
||||
f"Enter mobject file name (default is {file_name}): "
|
||||
)
|
||||
if user_name:
|
||||
file_name = user_name
|
||||
files.append(file_name)
|
||||
self.save_mobect(mob, file_name)
|
||||
|
||||
def undo(self):
|
||||
mobs = []
|
||||
for mob, state in self.saved_selection_state:
|
||||
mob.become(state)
|
||||
mobs.append(mob)
|
||||
if mob not in self.mobjects:
|
||||
self.add(mob)
|
||||
self.selection.set_submobjects(mobs)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def prepare_resizing(self, about_corner=False):
|
||||
center = self.selection.get_center()
|
||||
mp = self.mouse_point.get_center()
|
||||
if about_corner:
|
||||
self.scale_about_point = self.selection.get_corner(center - mp)
|
||||
else:
|
||||
self.scale_about_point = center
|
||||
self.scale_ref_vect = mp - self.scale_about_point
|
||||
self.scale_ref_width = self.selection.get_width()
|
||||
|
||||
# Event handlers
|
||||
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_press(symbol, modifiers)
|
||||
char = chr(symbol)
|
||||
# Enable selection
|
||||
if char == SELECT_KEY and modifiers == 0:
|
||||
self.is_selecting = True
|
||||
self.add(self.selection_rectangle)
|
||||
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
|
||||
# Prepare for move
|
||||
elif char in [GRAB_KEY, HORIZONTAL_GRAB_KEY, VERTICAL_GRAB_KEY] and modifiers == 0:
|
||||
mp = self.mouse_point.get_center()
|
||||
self.mouse_to_selection = mp - self.selection.get_center()
|
||||
# Prepare for resizing
|
||||
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
|
||||
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
|
||||
elif symbol == SHIFT_SYMBOL:
|
||||
if self.window.is_key_pressed(ord("t")):
|
||||
self.prepare_resizing(about_corner=True)
|
||||
# Show color palette
|
||||
elif char == COLOR_KEY and modifiers == 0:
|
||||
if len(self.selection) == 0:
|
||||
return
|
||||
if self.color_palette not in self.mobjects:
|
||||
self.add(self.color_palette)
|
||||
else:
|
||||
self.remove(self.color_palette)
|
||||
# Command + c -> Copy mobject ids to clipboard
|
||||
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||
self.copy_selection()
|
||||
# Command + v -> Paste
|
||||
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||
self.paste_selection()
|
||||
# Command + x -> Cut
|
||||
elif char == "x" and modifiers == COMMAND_MODIFIER:
|
||||
# TODO, this copy won't work, because once the objects are removed,
|
||||
# they're not searched for in the pasting.
|
||||
self.copy_selection()
|
||||
self.delete_selection()
|
||||
# Delete
|
||||
elif symbol == DELETE_SYMBOL:
|
||||
self.delete_selection()
|
||||
# Command + a -> Select all
|
||||
elif char == "a" and modifiers == COMMAND_MODIFIER:
|
||||
self.clear_selection()
|
||||
self.add_to_selection(*self.mobjects)
|
||||
# Command + g -> Group selection
|
||||
elif char == "g" and modifiers == COMMAND_MODIFIER:
|
||||
group = self.get_group(*self.selection)
|
||||
self.add(group)
|
||||
self.clear_selection()
|
||||
self.add_to_selection(group)
|
||||
# Command + shift + g -> Ungroup the selection
|
||||
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||
pieces = []
|
||||
for mob in list(self.selection):
|
||||
self.remove(mob)
|
||||
pieces.extend(list(mob))
|
||||
self.clear_selection()
|
||||
self.add(*pieces)
|
||||
self.add_to_selection(*pieces)
|
||||
# Command + t -> Toggle selection mode
|
||||
elif char == "t" and modifiers == COMMAND_MODIFIER:
|
||||
self.toggle_selection_mode()
|
||||
# Command + z -> Restore selection to original state
|
||||
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
||||
self.undo()
|
||||
# Command + s -> Save selections to file
|
||||
elif char == "s" and modifiers == COMMAND_MODIFIER:
|
||||
self.saved_selection_to_file()
|
||||
# Keyboard movements
|
||||
elif symbol in ARROW_SYMBOLS:
|
||||
nudge = self.selection_nudge_size
|
||||
if (modifiers & SHIFT_MODIFIER):
|
||||
nudge *= 10
|
||||
vect = [LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)]
|
||||
self.selection.shift(nudge * vect)
|
||||
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_release(symbol, modifiers)
|
||||
if chr(symbol) == SELECT_KEY:
|
||||
self.is_selecting = False
|
||||
self.remove(self.selection_rectangle)
|
||||
for mob in reversed(self.get_selection_search_set()):
|
||||
if mob.is_movable() and self.selection_rectangle.is_touching(mob):
|
||||
self.add_to_selection(mob)
|
||||
|
||||
elif symbol == SHIFT_SYMBOL:
|
||||
if self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.prepare_resizing(about_corner=False)
|
||||
|
||||
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
||||
super().on_mouse_motion(point, d_point)
|
||||
# Move selection
|
||||
if self.window.is_key_pressed(ord("g")):
|
||||
self.selection.move_to(point - self.mouse_to_selection)
|
||||
# Move selection restricted to horizontal
|
||||
elif self.window.is_key_pressed(ord("h")):
|
||||
self.selection.set_x((point - self.mouse_to_selection)[0])
|
||||
# Move selection restricted to vertical
|
||||
elif self.window.is_key_pressed(ord("v")):
|
||||
self.selection.set_y((point - self.mouse_to_selection)[1])
|
||||
# Scale selection
|
||||
elif self.window.is_key_pressed(ord("t")):
|
||||
# TODO, allow for scaling about the opposite corner
|
||||
vect = point - self.scale_about_point
|
||||
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
|
||||
self.selection.set_width(
|
||||
scalar * self.scale_ref_width,
|
||||
about_point=self.scale_about_point
|
||||
)
|
||||
|
||||
def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(point, button, mods)
|
||||
if self.color_palette in self.mobjects:
|
||||
# Search through all mobject on the screne, not just the palette
|
||||
to_search = list(it.chain(*(
|
||||
mobject.family_members_with_points()
|
||||
for mobject in self.mobjects
|
||||
if mobject not in self.unselectables
|
||||
)))
|
||||
mob = self.point_to_mobject(point, to_search)
|
||||
if mob is not None:
|
||||
self.selection.set_color(mob.get_fill_color())
|
||||
self.remove(self.color_palette)
|
||||
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||
mob = self.point_to_mobject(point)
|
||||
if mob is not None:
|
||||
self.toggle_from_selection(mob)
|
||||
else:
|
||||
self.clear_selection()
|
|
@ -4,20 +4,25 @@ import time
|
|||
import random
|
||||
import inspect
|
||||
import platform
|
||||
import itertools as it
|
||||
from functools import wraps
|
||||
from typing import Iterable, Callable
|
||||
import os
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
from manimlib.constants import ARROW_SYMBOLS
|
||||
from manimlib.constants import SHIFT_MODIFIER, CTRL_MODIFIER, COMMAND_MODIFIER
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
|
@ -33,6 +38,14 @@ if TYPE_CHECKING:
|
|||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
PAN_3D_KEY = 'd'
|
||||
FRAME_SHIFT_KEY = 'f'
|
||||
ZOOM_KEY = 'z'
|
||||
RESET_FRAME_KEY = 'r'
|
||||
QUIT_KEY = 'q'
|
||||
EMBED_KEY = 'e'
|
||||
|
||||
|
||||
class Scene(object):
|
||||
CONFIG = {
|
||||
"window_config": {},
|
||||
|
@ -116,16 +129,17 @@ class Scene(object):
|
|||
# If there is a window, enter a loop
|
||||
# which updates the frame while under
|
||||
# the hood calling the pyglet event loop
|
||||
log.info("Tips: You are now in the interactive mode. Now you can use the keyboard"
|
||||
" and the mouse to interact with the scene. Just press `q` if you want to quit.")
|
||||
log.info(
|
||||
"Tips: You are now in the interactive mode. Now you can use the keyboard"
|
||||
" and the mouse to interact with the scene. Just press `command + q` or `esc`"
|
||||
" if you want to quit."
|
||||
)
|
||||
self.quit_interaction = False
|
||||
self.lock_static_mobject_data()
|
||||
self.refresh_static_mobjects()
|
||||
while not (self.window.is_closing or self.quit_interaction):
|
||||
self.update_frame(1 / self.camera.frame_rate)
|
||||
if self.window.is_closing:
|
||||
self.window.destroy()
|
||||
if self.quit_interaction:
|
||||
self.unlock_mobject_data()
|
||||
|
||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||
if not self.preview:
|
||||
|
@ -142,15 +156,18 @@ class Scene(object):
|
|||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
shell = InteractiveShellEmbed()
|
||||
# Have the frame update after each command
|
||||
shell.events.register('post_run_cell', lambda *a, **kw: self.refresh_static_mobjects())
|
||||
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
|
||||
# Use the locals of the caller as the local namespace
|
||||
# once embedded, and add a few custom shortcuts
|
||||
local_ns = inspect.currentframe().f_back.f_locals
|
||||
local_ns["touch"] = self.interact
|
||||
local_ns["i2g"] = self.ids_to_group
|
||||
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
||||
local_ns[term] = getattr(self, term)
|
||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||
exec(get_custom_config()["universal_import_line"])
|
||||
shell(local_ns=local_ns, stack_depth=2)
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
|
@ -295,6 +312,25 @@ class Scene(object):
|
|||
return mobject
|
||||
return None
|
||||
|
||||
def get_group(self, *mobjects):
|
||||
if all(isinstance(m, VMobject) for m in mobjects):
|
||||
return VGroup(*mobjects)
|
||||
else:
|
||||
return Group(*mobjects)
|
||||
|
||||
def id_to_mobject(self, id_value):
|
||||
for mob in self.mobjects:
|
||||
for sm in mob.get_family():
|
||||
if id(sm) == id_value:
|
||||
return sm
|
||||
return None
|
||||
|
||||
def ids_to_group(self, *id_values):
|
||||
return self.get_group(*filter(
|
||||
lambda x: x is not None,
|
||||
map(self.id_to_mobject, id_values)
|
||||
))
|
||||
|
||||
# Related to skipping
|
||||
def update_skipping_status(self) -> None:
|
||||
if self.start_at_animation_number is not None:
|
||||
|
@ -443,6 +479,7 @@ class Scene(object):
|
|||
self.real_animation_start_time = time.time()
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
||||
self.refresh_static_mobjects()
|
||||
func(self, *args, **kwargs)
|
||||
|
||||
if should_write:
|
||||
|
@ -451,23 +488,8 @@ class Scene(object):
|
|||
self.num_plays += 1
|
||||
return wrapper
|
||||
|
||||
def lock_static_mobject_data(self, *animations: Animation) -> None:
|
||||
movers = list(it.chain(*[
|
||||
anim.mobject.get_family()
|
||||
for anim in animations
|
||||
]))
|
||||
for mobject in self.mobjects:
|
||||
if mobject in movers or mobject.get_family_updaters():
|
||||
continue
|
||||
self.camera.set_mobjects_as_static(mobject)
|
||||
|
||||
def unlock_mobject_data(self) -> None:
|
||||
self.camera.release_static_mobjects()
|
||||
|
||||
def refresh_locked_data(self):
|
||||
self.unlock_mobject_data()
|
||||
self.lock_static_mobject_data()
|
||||
return self
|
||||
def refresh_static_mobjects(self) -> None:
|
||||
self.camera.refresh_static_mobjects()
|
||||
|
||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||
for animation in animations:
|
||||
|
@ -507,11 +529,9 @@ class Scene(object):
|
|||
log.warning("Called Scene.play with no animations")
|
||||
return
|
||||
animations = self.anims_from_play_args(*args, **kwargs)
|
||||
self.lock_static_mobject_data(*animations)
|
||||
self.begin_animations(animations)
|
||||
self.progress_through_animations(animations)
|
||||
self.finish_animations(animations)
|
||||
self.unlock_mobject_data()
|
||||
|
||||
@handle_play_like_call
|
||||
def wait(
|
||||
|
@ -524,7 +544,6 @@ class Scene(object):
|
|||
if note:
|
||||
log.info(note)
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
self.lock_static_mobject_data()
|
||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||
while self.hold_on_wait:
|
||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||
|
@ -538,9 +557,8 @@ class Scene(object):
|
|||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
self.unlock_mobject_data()
|
||||
self.refresh_static_mobjects()
|
||||
return self
|
||||
|
||||
def wait_until(
|
||||
|
@ -574,22 +592,31 @@ class Scene(object):
|
|||
|
||||
# Helpers for interactive development
|
||||
def save_state(self) -> None:
|
||||
self.saved_state = {
|
||||
"mobjects": self.mobjects,
|
||||
"mobject_states": [
|
||||
mob.copy()
|
||||
for mob in self.mobjects
|
||||
],
|
||||
}
|
||||
self.saved_state = [
|
||||
(mob, mob.copy())
|
||||
for mob in self.mobjects
|
||||
]
|
||||
|
||||
def restore(self) -> None:
|
||||
if not hasattr(self, "saved_state"):
|
||||
raise Exception("Trying to restore scene without having saved")
|
||||
mobjects = self.saved_state["mobjects"]
|
||||
states = self.saved_state["mobject_states"]
|
||||
for mob, state in zip(mobjects, states):
|
||||
mob.become(state)
|
||||
self.mobjects = mobjects
|
||||
self.mobjects = []
|
||||
for mob, mob_state in self.saved_state:
|
||||
mob.become(mob_state)
|
||||
self.mobjects.append(mob)
|
||||
|
||||
def save_mobect(self, mobject: Mobject, file_name: str):
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
path = os.path.join(directory, file_name)
|
||||
mobject.save_to_file(path)
|
||||
|
||||
def load_mobject(self, file_name):
|
||||
if os.path.exists(file_name):
|
||||
path = file_name
|
||||
else:
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
path = os.path.join(directory, file_name)
|
||||
return Mobject.load(path)
|
||||
|
||||
# Event handling
|
||||
|
||||
|
@ -606,10 +633,12 @@ class Scene(object):
|
|||
return
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("d")):
|
||||
# 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])
|
||||
elif self.window.is_key_pressed(ord("s")):
|
||||
# Handle frame movements
|
||||
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
||||
shift = -d_point
|
||||
shift[0] *= frame.get_width() / 2
|
||||
shift[1] *= frame.get_height() / 2
|
||||
|
@ -637,6 +666,7 @@ class Scene(object):
|
|||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
self.mouse_drag_point.move_to(point)
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
|
@ -664,9 +694,9 @@ class Scene(object):
|
|||
return
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("z")):
|
||||
if self.window.is_key_pressed(ord(ZOOM_KEY)):
|
||||
factor = 1 + np.arctan(10 * offset[1])
|
||||
frame.scale(1/factor, about_point=point)
|
||||
frame.scale(1 / factor, about_point=point)
|
||||
else:
|
||||
transform = frame.get_inverse_camera_rotation_matrix()
|
||||
shift = np.dot(np.transpose(transform), offset)
|
||||
|
@ -698,13 +728,16 @@ class Scene(object):
|
|||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
if char == "r":
|
||||
if char == RESET_FRAME_KEY:
|
||||
self.camera.frame.to_default_state()
|
||||
elif char == "q":
|
||||
# command + q
|
||||
elif char == QUIT_KEY and modifiers == COMMAND_MODIFIER:
|
||||
self.quit_interaction = True
|
||||
elif char == " " or symbol == 65363: # Space or right arrow
|
||||
# Space or right arrow
|
||||
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
||||
self.hold_on_wait = False
|
||||
elif char == "e" and modifiers == 3: # ctrl + shift + e
|
||||
# ctrl + shift + e
|
||||
elif char == EMBED_KEY and modifiers == CTRL_MODIFIER | SHIFT_MODIFIER:
|
||||
self.embed(close_scene_on_exit=False)
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
|
|
|
@ -78,6 +78,8 @@ class SceneFileWriter(object):
|
|||
self.partial_movie_directory = guarantee_existence(os.path.join(
|
||||
movie_dir, "partial_movie_files", scene_name,
|
||||
))
|
||||
# A place to save mobjects
|
||||
self.saved_mobject_directory = os.path.join(out_dir, "mobjects")
|
||||
|
||||
def get_default_module_directory(self) -> str:
|
||||
path, _ = os.path.splitext(self.input_file_path)
|
||||
|
@ -119,6 +121,12 @@ class SceneFileWriter(object):
|
|||
def get_movie_file_path(self) -> str:
|
||||
return self.movie_file_path
|
||||
|
||||
def get_saved_mobject_directory(self) -> str:
|
||||
return guarantee_existence(os.path.join(
|
||||
self.saved_mobject_directory,
|
||||
str(self.scene),
|
||||
))
|
||||
|
||||
# Sound
|
||||
def init_audio(self) -> None:
|
||||
self.includes_sound: bool = False
|
||||
|
@ -220,7 +228,7 @@ class SceneFileWriter(object):
|
|||
# This is if the background of the exported
|
||||
# video should be transparent.
|
||||
command += [
|
||||
'-vcodec', 'qtrle',
|
||||
'-vcodec', 'prores_ks',
|
||||
]
|
||||
elif self.movie_file_extension == ".gif":
|
||||
command += []
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from typing import Iterable
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
@ -11,15 +10,14 @@ if TYPE_CHECKING:
|
|||
|
||||
def extract_mobject_family_members(
|
||||
mobject_list: Iterable[Mobject],
|
||||
only_those_with_points: bool = False
|
||||
exclude_pointless: bool = False
|
||||
) -> list[Mobject]:
|
||||
result = list(it.chain(*[
|
||||
mob.get_family()
|
||||
return [
|
||||
sm
|
||||
for mob in mobject_list
|
||||
]))
|
||||
if only_those_with_points:
|
||||
result = [mob for mob in result if mob.has_points()]
|
||||
return result
|
||||
for sm in mob.get_family()
|
||||
if (not exclude_pointless) or sm.has_points()
|
||||
]
|
||||
|
||||
|
||||
def restructure_list_to_exclude_certain_family_members(
|
||||
|
|
|
@ -152,6 +152,8 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
|
|||
"""
|
||||
n1 = get_norm(v1)
|
||||
n2 = get_norm(v2)
|
||||
if n1 == 0 or n2 == 0:
|
||||
return 0
|
||||
cos_angle = np.dot(v1, v2) / np.float64(n1 * n2)
|
||||
return math.acos(clip(cos_angle, -1, 1))
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ def tex_to_dvi(tex_file):
|
|||
for line in file.readlines():
|
||||
if line.startswith("!"):
|
||||
log.debug(f"The error could be: `{line[2:-1]}`")
|
||||
sys.exit(2)
|
||||
raise LatexError()
|
||||
return result
|
||||
|
||||
|
||||
|
@ -134,3 +134,8 @@ def display_during_execution(message):
|
|||
yield
|
||||
finally:
|
||||
print(" " * len(to_print), end="\r")
|
||||
|
||||
|
||||
|
||||
class LatexError(Exception):
|
||||
pass
|
||||
|
|
|
@ -11,6 +11,7 @@ moderngl_window
|
|||
skia-pathops
|
||||
pydub
|
||||
pygments
|
||||
pyperclip
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
|
|
Loading…
Add table
Reference in a new issue