diff --git a/camera/camera.py b/camera/camera.py index bc9a6204..c2b6f8f9 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -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 ] diff --git a/camera/mapping_camera.py b/camera/mapping_camera.py index b55d4bc0..cc283ccc 100644 --- a/camera/mapping_camera.py +++ b/camera/mapping_camera.py @@ -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)), + ) diff --git a/camera/moving_camera.py b/camera/moving_camera.py index dab64fe7..4072227a 100644 --- a/camera/moving_camera.py +++ b/camera/moving_camera.py @@ -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) diff --git a/camera/multi_camera.py b/camera/multi_camera.py new file mode 100644 index 00000000..2ce6447c --- /dev/null +++ b/camera/multi_camera.py @@ -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) diff --git a/mobject/types/image_mobject.py b/mobject/types/image_mobject.py index ca8c03bd..ab73a048 100644 --- a/mobject/types/image_mobject.py +++ b/mobject/types/image_mobject.py @@ -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 diff --git a/scene/zoomed_scene.py b/scene/zoomed_scene.py index d642fcc6..9369f8c0 100644 --- a/scene/zoomed_scene.py +++ b/scene/zoomed_scene.py @@ -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.