mirror of
https://github.com/3b1b/manim.git
synced 2025-07-30 13:34:19 +08:00
Reimplemented ZoomedScene, using a new MultiCamera
This commit is contained in:
@ -11,7 +11,7 @@ from colour import Color
|
||||
from scipy.spatial.distance import pdist
|
||||
|
||||
from constants import *
|
||||
from mobject.types.image_mobject import ImageMobject
|
||||
from mobject.types.image_mobject import AbstractImageMobject
|
||||
from mobject.mobject import Mobject
|
||||
from mobject.types.point_cloud_mobject import PMobject
|
||||
from mobject.types.vectorized_mobject import VMobject
|
||||
@ -61,6 +61,12 @@ class Camera(object):
|
||||
self.canvas = None
|
||||
return copy.copy(self)
|
||||
|
||||
def reset_pixel_shape(self, new_shape):
|
||||
self.pixel_shape = tuple(new_shape)
|
||||
self.init_background()
|
||||
self.resize_frame_shape()
|
||||
self.reset()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension=0):
|
||||
"""
|
||||
Changes frame_shape to match the aspect ratio
|
||||
@ -68,13 +74,14 @@ class Camera(object):
|
||||
whether frame_shape[0] (height) or frame_shape[1] (width)
|
||||
remains fixed while the other changes accordingly.
|
||||
"""
|
||||
aspect_ratio = float(self.pixel_shape[1]) / self.pixel_shape[0]
|
||||
frame_width, frame_height = self.frame_shape
|
||||
frame_height, frame_width = self.frame_shape
|
||||
pixel_height, pixel_width = self.pixel_shape
|
||||
aspect_ratio = fdiv(pixel_width, pixel_height)
|
||||
if fixed_dimension == 0:
|
||||
frame_height = aspect_ratio * frame_width
|
||||
frame_height = frame_width / aspect_ratio
|
||||
else:
|
||||
frame_width = frame_height / aspect_ratio
|
||||
self.frame_shape = (frame_width, frame_height)
|
||||
frame_width = aspect_ratio * frame_height
|
||||
self.frame_shape = (frame_height, frame_width)
|
||||
|
||||
def init_background(self):
|
||||
if self.background_image is not None:
|
||||
@ -151,6 +158,7 @@ class Camera(object):
|
||||
|
||||
def reset(self):
|
||||
self.set_pixel_array(self.background)
|
||||
return self
|
||||
|
||||
####
|
||||
|
||||
@ -204,7 +212,7 @@ class Camera(object):
|
||||
type_func_pairs = [
|
||||
(VMobject, self.display_multiple_vectorized_mobjects),
|
||||
(PMobject, self.display_multiple_point_cloud_mobjects),
|
||||
(ImageMobject, self.display_multiple_image_mobjects),
|
||||
(AbstractImageMobject, self.display_multiple_image_mobjects),
|
||||
(Mobject, lambda batch: batch), # Do nothing
|
||||
]
|
||||
|
||||
|
@ -44,7 +44,7 @@ class MappingCamera(Camera):
|
||||
# CameraPlusOverlay class)
|
||||
|
||||
|
||||
class MultiCamera(Camera):
|
||||
class OldMultiCamera(Camera):
|
||||
def __init__(self, *cameras_with_start_positions, **kwargs):
|
||||
self.shifted_cameras = [
|
||||
DictAsObject(
|
||||
@ -92,11 +92,11 @@ class MultiCamera(Camera):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.init_background()
|
||||
|
||||
# A MultiCamera which, when called with two full-size cameras, initializes itself
|
||||
# A OldMultiCamera which, when called with two full-size cameras, initializes itself
|
||||
# as a splitscreen, also taking care to resize each individual camera within it
|
||||
|
||||
|
||||
class SplitScreenCamera(MultiCamera):
|
||||
class SplitScreenCamera(OldMultiCamera):
|
||||
def __init__(self, left_camera, right_camera, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.left_camera = left_camera
|
||||
@ -110,5 +110,8 @@ class SplitScreenCamera(MultiCamera):
|
||||
camera.resize_frame_shape()
|
||||
camera.reset()
|
||||
|
||||
MultiCamera.__init__(self, (left_camera, (0, 0)),
|
||||
(right_camera, (0, half_width)))
|
||||
OldMultiCamera.__init__(
|
||||
self,
|
||||
(left_camera, (0, 0)),
|
||||
(right_camera, (0, half_width)),
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from constants import FRAME_HEIGHT
|
||||
from constants import WHITE
|
||||
|
||||
from camera.camera import Camera
|
||||
from mobject.frame import ScreenRectangle
|
||||
from utils.config_ops import digest_config
|
||||
|
||||
|
||||
class MovingCamera(Camera):
|
||||
@ -12,7 +14,9 @@ class MovingCamera(Camera):
|
||||
of a given mobject
|
||||
"""
|
||||
CONFIG = {
|
||||
"aligned_dimension": "width" # or height
|
||||
"fixed_dimension": 0, # width
|
||||
"default_frame_stroke_color": WHITE,
|
||||
"default_frame_stroke_width": 0,
|
||||
}
|
||||
|
||||
def __init__(self, frame=None, **kwargs):
|
||||
@ -20,21 +24,28 @@ class MovingCamera(Camera):
|
||||
frame is a Mobject, (should be a rectangle) determining
|
||||
which region of space the camera displys
|
||||
"""
|
||||
digest_config(self, kwargs)
|
||||
if frame is None:
|
||||
frame = ScreenRectangle(height=FRAME_HEIGHT)
|
||||
frame.fade(1)
|
||||
frame.set_stroke(
|
||||
self.default_frame_stroke_color,
|
||||
self.default_frame_stroke_width,
|
||||
)
|
||||
self.frame = frame
|
||||
Camera.__init__(self, **kwargs)
|
||||
|
||||
def capture_mobjects(self, *args, **kwargs):
|
||||
self.space_center = self.frame.get_center()
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
self.reset_space_center()
|
||||
self.realign_frame_shape()
|
||||
Camera.capture_mobjects(self, *args, **kwargs)
|
||||
Camera.capture_mobjects(self, mobjects, **kwargs)
|
||||
|
||||
def reset_space_center(self):
|
||||
self.space_center = self.frame.get_center()
|
||||
|
||||
def realign_frame_shape(self):
|
||||
height, width = self.frame_shape
|
||||
if self.aligned_dimension == "height":
|
||||
self.frame_shape = (self.frame.get_height(), width)
|
||||
else:
|
||||
if self.fixed_dimension == 0:
|
||||
self.frame_shape = (height, self.frame.get_width())
|
||||
self.resize_frame_shape(0 if self.aligned_dimension == "height" else 1)
|
||||
else:
|
||||
self.frame_shape = (self.frame.get_height(), width)
|
||||
self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
|
||||
|
60
camera/multi_camera.py
Normal file
60
camera/multi_camera.py
Normal file
@ -0,0 +1,60 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from camera.moving_camera import MovingCamera
|
||||
from utils.iterables import list_difference_update
|
||||
|
||||
# Distinct notions of view frame vs. display frame
|
||||
|
||||
# For now, let's say it is the responsibility of the scene holding
|
||||
# this camera to add all of the relevant image_mobjects_from_cameras,
|
||||
# as well as their display_frames
|
||||
|
||||
|
||||
class MultiCamera(MovingCamera):
|
||||
CONFIG = {
|
||||
# "lock_display_frame_dimensions_to_view_frame": True,
|
||||
# "display_frame_fixed_dimension": 1, # Height
|
||||
"allow_cameras_to_capture_their_own_display": False,
|
||||
}
|
||||
|
||||
def __init__(self, *image_mobjects_from_cameras, **kwargs):
|
||||
self.image_mobjects_from_cameras = []
|
||||
for imfc in image_mobjects_from_cameras:
|
||||
self.add_image_mobject_from_camera(imfc)
|
||||
MovingCamera.__init__(self, **kwargs)
|
||||
|
||||
def add_image_mobject_from_camera(self, image_mobject_from_camera):
|
||||
# A silly method to have right now, but maybe there are things
|
||||
# we want to guarantee about any imfc's added later.
|
||||
imfc = image_mobject_from_camera
|
||||
self.image_mobjects_from_cameras.append(imfc)
|
||||
|
||||
def update_sub_cameras(self):
|
||||
# if self.lock_display_frame_dimensions_to_view_frame:
|
||||
# for imfc in self.image_mobjects_from_cameras:
|
||||
# aspect_ratio = imfc.view_frame.get_width() / imfc.view_frame.get_height()
|
||||
|
||||
# Reshape sub_camera pixel_arrays
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
frame_height, frame_width = self.frame_shape
|
||||
pixel_height, pixel_width = self.get_pixel_array().shape[:2]
|
||||
imfc.camera.frame_shape = (
|
||||
imfc.camera.frame.get_height(),
|
||||
imfc.camera.frame.get_width(),
|
||||
)
|
||||
imfc.camera.reset_pixel_shape((
|
||||
int(pixel_height * imfc.get_height() / frame_height),
|
||||
int(pixel_width * imfc.get_width() / frame_width),
|
||||
))
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
# Make sure all frames are in mobjects? Or not?
|
||||
self.update_sub_cameras()
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
to_add = list(mobjects)
|
||||
if not self.allow_cameras_to_capture_their_own_display:
|
||||
to_add = list_difference_update(
|
||||
to_add, imfc.submobject_family()
|
||||
)
|
||||
imfc.camera.capture_mobjects(to_add, **kwargs)
|
||||
MovingCamera.capture_mobjects(self, mobjects, **kwargs)
|
@ -7,13 +7,51 @@ from PIL import Image
|
||||
from constants import *
|
||||
|
||||
from mobject.mobject import Mobject
|
||||
from mobject.shape_matchers import SurroundingRectangle
|
||||
from utils.bezier import interpolate
|
||||
from utils.color import color_to_int_rgb
|
||||
from utils.config_ops import digest_config
|
||||
from utils.images import get_full_raster_image_path
|
||||
|
||||
|
||||
class ImageMobject(Mobject):
|
||||
class AbstractImageMobject(Mobject):
|
||||
"""
|
||||
Automatically filters out black pixels
|
||||
"""
|
||||
CONFIG = {
|
||||
"filter_color": "black",
|
||||
"invert": False,
|
||||
# "use_cache" : True,
|
||||
"height": 2.0,
|
||||
"image_mode": "RGBA",
|
||||
"pixel_array_dtype": "uint8",
|
||||
}
|
||||
|
||||
def get_pixel_array(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def set_color(self):
|
||||
# Likely to be implemented in subclasses
|
||||
pass
|
||||
|
||||
def init_points(self):
|
||||
# Corresponding corners of image are fixed to these
|
||||
# Three points
|
||||
self.points = np.array([
|
||||
UP + LEFT,
|
||||
UP + RIGHT,
|
||||
DOWN + LEFT,
|
||||
])
|
||||
self.center()
|
||||
h, w = self.get_pixel_array().shape[:2]
|
||||
self.stretch_to_fit_height(self.height)
|
||||
self.stretch_to_fit_width(self.height * w / h)
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
||||
|
||||
class ImageMobject(AbstractImageMobject):
|
||||
"""
|
||||
Automatically filters out black pixels
|
||||
"""
|
||||
@ -37,7 +75,7 @@ class ImageMobject(Mobject):
|
||||
self.change_to_rgba_array()
|
||||
if self.invert:
|
||||
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
|
||||
Mobject.__init__(self, **kwargs)
|
||||
AbstractImageMobject.__init__(self, **kwargs)
|
||||
|
||||
def change_to_rgba_array(self):
|
||||
pa = self.pixel_array
|
||||
@ -53,6 +91,9 @@ class ImageMobject(Mobject):
|
||||
pa = np.append(pa, alphas, axis=2)
|
||||
self.pixel_array = pa
|
||||
|
||||
def get_pixel_array(self):
|
||||
return self.pixel_array
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
rgb = color_to_int_rgb(color)
|
||||
self.pixel_array[:, :, :3] = rgb
|
||||
@ -63,19 +104,6 @@ class ImageMobject(Mobject):
|
||||
self.color = color
|
||||
return self
|
||||
|
||||
def init_points(self):
|
||||
# Corresponding corners of image are fixed to these
|
||||
# Three points
|
||||
self.points = np.array([
|
||||
UP + LEFT,
|
||||
UP + RIGHT,
|
||||
DOWN + LEFT,
|
||||
])
|
||||
self.center()
|
||||
h, w = self.pixel_array.shape[:2]
|
||||
self.stretch_to_fit_height(self.height)
|
||||
self.stretch_to_fit_width(self.height * w / h)
|
||||
|
||||
def set_opacity(self, alpha):
|
||||
self.pixel_array[:, :, 3] = int(255 * alpha)
|
||||
return self
|
||||
@ -90,5 +118,26 @@ class ImageMobject(Mobject):
|
||||
mobject1.pixel_array, mobject2.pixel_array, alpha
|
||||
).astype(self.pixel_array_dtype)
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
||||
class ImageMobjectFromCamera(AbstractImageMobject):
|
||||
CONFIG = {
|
||||
"default_display_frame_config": {
|
||||
"stroke_width": 3,
|
||||
"stroke_color": WHITE,
|
||||
"buff": 0,
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, camera, **kwargs):
|
||||
self.camera = camera
|
||||
AbstractImageMobject.__init__(self, **kwargs)
|
||||
|
||||
def get_pixel_array(self):
|
||||
return self.camera.get_pixel_array()
|
||||
|
||||
def add_display_frame(self, **kwargs):
|
||||
config = dict(self.default_display_frame_config)
|
||||
config.update(kwargs)
|
||||
self.display_frame = SurroundingRectangle(self, **config)
|
||||
self.add(self.display_frame)
|
||||
return self
|
||||
|
@ -5,12 +5,92 @@ import numpy as np
|
||||
from scene.scene import Scene
|
||||
from animation.creation import FadeIn
|
||||
from camera.moving_camera import MovingCamera
|
||||
from camera.multi_camera import MultiCamera
|
||||
from mobject.geometry import Rectangle
|
||||
from mobject.types.image_mobject import ImageMobjectFromCamera
|
||||
|
||||
from constants import *
|
||||
|
||||
|
||||
class ZoomedScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_class": MultiCamera,
|
||||
"zoomed_display_height": 3,
|
||||
"zoomed_display_width": 3,
|
||||
"zoomed_display_center": None,
|
||||
"zoomed_display_corner": UP + RIGHT,
|
||||
"zoomed_display_corner_buff": DEFAULT_MOBJECT_TO_EDGE_BUFFER,
|
||||
"zoomed_camera_config": {
|
||||
"default_frame_stroke_width": 2,
|
||||
},
|
||||
"zoomed_camera_image_mobject_config": {},
|
||||
"zoomed_camera_frame_starting_position": ORIGIN,
|
||||
"zoom_factor": 0.15,
|
||||
"image_frame_stroke_width": 3,
|
||||
"zoom_activated": False,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
# Initialize camera and display
|
||||
zoomed_camera = MovingCamera(**self.zoomed_camera_config)
|
||||
zoomed_display = ImageMobjectFromCamera(
|
||||
zoomed_camera, **self.zoomed_camera_image_mobject_config
|
||||
)
|
||||
zoomed_display.add_display_frame()
|
||||
for mob in zoomed_camera.frame, zoomed_display:
|
||||
mob.stretch_to_fit_height(self.zoomed_display_height)
|
||||
mob.stretch_to_fit_width(self.zoomed_display_width)
|
||||
zoomed_camera.frame.scale(self.zoom_factor)
|
||||
|
||||
# Position camera and display
|
||||
zoomed_camera.frame.move_to(self.zoomed_camera_frame_starting_position)
|
||||
if self.zoomed_display_center is not None:
|
||||
zoomed_display.move_to(self.zoomed_display_center)
|
||||
else:
|
||||
zoomed_display.to_corner(
|
||||
self.zoomed_display_corner,
|
||||
buff=self.zoomed_display_corner_buff
|
||||
)
|
||||
|
||||
self.zoomed_camera = zoomed_camera
|
||||
self.zoomed_display = zoomed_display
|
||||
|
||||
def activate_zooming(self, animate=False, run_times=[3, 2]):
|
||||
self.zoom_activated = True
|
||||
zoomed_camera = self.zoomed_camera
|
||||
zoomed_display = self.zoomed_display
|
||||
self.camera.add_image_mobject_from_camera(zoomed_display)
|
||||
|
||||
to_add = [zoomed_camera.frame, zoomed_display]
|
||||
if animate:
|
||||
zoomed_display.save_state()
|
||||
zoomed_display.replace(zoomed_camera.frame)
|
||||
|
||||
full_frame_height, full_frame_width = self.camera.frame_shape
|
||||
zoomed_camera.frame.save_state()
|
||||
zoomed_camera.frame.stretch_to_fit_width(full_frame_width)
|
||||
zoomed_camera.frame.stretch_to_fit_height(full_frame_height)
|
||||
zoomed_camera.frame.center()
|
||||
zoomed_camera.frame.set_stroke(width=0)
|
||||
|
||||
for mover, run_time in zip(to_add, run_times):
|
||||
self.add_foreground_mobject(mover)
|
||||
self.play(mover.restore, run_time=run_time)
|
||||
else:
|
||||
self.add_foreground_mobjects(*to_add)
|
||||
|
||||
def get_moving_mobjects(self, *animations):
|
||||
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
|
||||
zoomed_mobjects = [self.zoomed_camera.frame, self.zoomed_display]
|
||||
moving_zoomed_mobjects = set(moving_mobjects).intersection(zoomed_mobjects)
|
||||
if self.zoom_activated and moving_zoomed_mobjects:
|
||||
return self.mobjects
|
||||
else:
|
||||
return moving_mobjects
|
||||
|
||||
|
||||
|
||||
class OldZoomedScene(Scene):
|
||||
"""
|
||||
Move around self.little_rectangle to determine
|
||||
which part of the screen is zoomed in on.
|
||||
|
Reference in New Issue
Block a user