From 80187955ece5c7d04f9e5cdc982181912679960d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 21 May 2018 12:22:49 -0700 Subject: [PATCH 1/3] Revert "Revert "Merge branch 'master' of github.com:3b1b/manim into alt-calc"" This reverts commit 13d72289189f9566e5a71177aa7e11bd0fa68a0b. --- active_projects/eola2/cramer.py | 2 +- animation/transform.py | 7 - camera/camera.py | 262 ++++++++++++++---------------- camera/mapping_camera.py | 25 ++- camera/moving_camera.py | 71 ++------ camera/multi_camera.py | 59 ------- camera/three_d_camera.py | 8 +- constants.py | 11 +- extract_scene.py | 7 +- mobject/mobject.py | 9 - mobject/types/image_mobject.py | 83 +++------- old_projects/WindingNumber.py | 2 +- old_projects/WindingNumber_G.py | 2 +- old_projects/basel/basel.py | 2 +- old_projects/nn/part1.py | 6 +- old_projects/pi_day.py | 6 +- old_projects/wallis.py | 2 +- scene/moving_camera_scene.py | 34 ++-- scene/scene.py | 3 +- scene/zoomed_scene.py | 178 ++++++++++++-------- utils/color.py | 3 +- utils/output_directory_getters.py | 2 +- 22 files changed, 322 insertions(+), 462 deletions(-) delete mode 100644 camera/multi_camera.py diff --git a/active_projects/eola2/cramer.py b/active_projects/eola2/cramer.py index ff858f45..69eb2d0a 100644 --- a/active_projects/eola2/cramer.py +++ b/active_projects/eola2/cramer.py @@ -206,7 +206,7 @@ class LeaveItToComputers(TeacherStudentsScene): class PrerequisiteKnowledge(TeacherStudentsScene): CONFIG = { - "camera_config": {"background_opacity": 1} + "camera_config": {"background_alpha": 255} } def construct(self): diff --git a/animation/transform.py b/animation/transform.py index 3da8eeff..83e56462 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -140,13 +140,6 @@ class ApplyPointwiseFunction(ApplyMethod): ) -class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction): - def __init__(self, function, mobject, **kwargs): - ApplyMethod.__init__( - self, mobject.move_to, function(mobject.get_center()), **kwargs - ) - - class FadeToColor(ApplyMethod): def __init__(self, mobject, color, **kwargs): ApplyMethod.__init__(self, mobject.set_color, color, **kwargs) diff --git a/camera/camera.py b/camera/camera.py index f23a76eb..e8ccb6bb 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -1,6 +1,5 @@ import itertools as it import numpy as np -import operator as op import aggdraw import copy @@ -8,10 +7,9 @@ import time from PIL import Image from colour import Color -from scipy.spatial.distance import pdist from constants import * -from mobject.types.image_mobject import AbstractImageMobject +from mobject.types.image_mobject import ImageMobject from mobject.mobject import Mobject from mobject.types.point_cloud_mobject import PMobject from mobject.types.vectorized_mobject import VMobject @@ -23,26 +21,22 @@ from utils.iterables import batch_by_property from utils.iterables import list_difference_update from utils.iterables import remove_list_redundancies from utils.simple_functions import fdiv -from utils.space_ops import angle_of_vector class Camera(object): CONFIG = { "background_image": None, - "pixel_height": DEFAULT_PIXEL_HEIGHT, - "pixel_width": DEFAULT_PIXEL_WIDTH, - # Note: frame height and width will be resized to match - # the pixel aspect ratio - "frame_height": FRAME_HEIGHT, - "frame_width": FRAME_WIDTH, - "frame_center": ORIGIN, + "pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH), + # Note: frame_shape will be resized to match pixel_shape + "frame_shape": (FRAME_HEIGHT, FRAME_WIDTH), + "space_center": ORIGIN, "background_color": BLACK, - "background_opacity": 0, # Points in vectorized mobjects with norm greater # than this value will be rescaled. "max_allowable_norm": FRAME_WIDTH, "image_mode": "RGBA", "n_rgb_coords": 4, + "background_alpha": 0, # Out of rgb_max_val "pixel_array_dtype": 'uint8', "use_z_coordinate_for_display_order": False, # z_buff_func is only used if the flag above is set to True. @@ -64,72 +58,36 @@ class Camera(object): self.canvas = None return copy.copy(self) - def reset_pixel_shape(self, new_height, new_width): - self.pixel_width = new_width - self.pixel_height = new_height - self.init_background() - self.resize_frame_shape() - self.reset() - - def get_pixel_height(self): - return self.pixel_height - - def get_pixel_width(self): - return self.pixel_width - - def get_frame_height(self): - return self.frame_height - - def get_frame_width(self): - return self.frame_width - - def get_frame_center(self): - return self.frame_center - - def set_frame_height(self, frame_height): - self.frame_height = frame_height - - def set_frame_width(self, frame_width): - self.frame_width = frame_width - - def set_frame_center(self, frame_center): - self.frame_center = frame_center - def resize_frame_shape(self, fixed_dimension=0): """ Changes frame_shape to match the aspect ratio - of the pixels, where fixed_dimension determines - whether frame_height or frame_width + of pixel_shape, where fixed_dimension determines + whether frame_shape[0] (height) or frame_shape[1] (width) remains fixed while the other changes accordingly. """ - pixel_height = self.get_pixel_height() - pixel_width = self.get_pixel_width() - frame_height = self.get_frame_height() - frame_width = self.get_frame_width() - aspect_ratio = fdiv(pixel_width, pixel_height) + aspect_ratio = float(self.pixel_shape[1]) / self.pixel_shape[0] + frame_width, frame_height = self.frame_shape if fixed_dimension == 0: - frame_height = frame_width / aspect_ratio + frame_height = aspect_ratio * frame_width else: - frame_width = aspect_ratio * frame_height - self.set_frame_height(frame_height) - self.set_frame_width(frame_width) + frame_width = frame_height / aspect_ratio + self.frame_shape = (frame_width, frame_height) def init_background(self): - height = self.get_pixel_height() - width = self.get_pixel_width() if self.background_image is not None: path = get_full_raster_image_path(self.background_image) image = Image.open(path).convert(self.image_mode) + height, width = self.pixel_shape # TODO, how to gracefully handle backgrounds # with different sizes? self.background = np.array(image)[:height, :width] self.background = self.background.astype(self.pixel_array_dtype) else: background_rgba = color_to_int_rgba( - self.background_color, self.background_opacity + self.background_color, alpha=self.background_alpha ) self.background = np.zeros( - (height, width, self.n_rgb_coords), + list(self.pixel_shape) + [self.n_rgb_coords], dtype=self.pixel_array_dtype ) self.background[:, :] = background_rgba @@ -147,10 +105,10 @@ class Camera(object): retval = np.array(pixel_array) if convert_from_floats: retval = np.apply_along_axis( - lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype), + lambda f: ( + f * self.rgb_max_val).astype(self.pixel_array_dtype), 2, - retval - ) + retval) return retval def set_pixel_array(self, pixel_array, convert_from_floats=False): @@ -190,7 +148,6 @@ class Camera(object): def reset(self): self.set_pixel_array(self.background) - return self #### @@ -244,7 +201,7 @@ class Camera(object): type_func_pairs = [ (VMobject, self.display_multiple_vectorized_mobjects), (PMobject, self.display_multiple_point_cloud_mobjects), - (AbstractImageMobject, self.display_multiple_image_mobjects), + (ImageMobject, self.display_multiple_image_mobjects), (Mobject, lambda batch: batch), # Do nothing ] @@ -344,7 +301,8 @@ class Camera(object): # points = self.adjust_out_of_range_points(points) if len(points) == 0: continue - coords = self.points_to_pixel_coords(points) + aligned_points = self.align_points_to_camera(points) + coords = self.points_to_pixel_coords(aligned_points) coord_strings = coords.flatten().astype(str) # Start new path string with M coord_strings[0] = "M" + coord_strings[0] @@ -384,6 +342,7 @@ class Camera(object): def display_point_cloud(self, points, rgbas, thickness): if len(points) == 0: return + points = self.align_points_to_camera(points) pixel_coords = self.points_to_pixel_coords(points) pixel_coords = self.thickened_coordinates( pixel_coords, thickness @@ -399,8 +358,7 @@ class Camera(object): pixel_coords = pixel_coords[on_screen_indices] rgbas = rgbas[on_screen_indices] - ph = self.get_pixel_height() - pw = self.get_pixel_width() + ph, pw = self.pixel_shape flattener = np.array([1, pw], dtype='int') flattener = flattener.reshape((2, 1)) @@ -420,57 +378,95 @@ class Camera(object): ul_coords, ur_coords, dl_coords = corner_coords right_vect = ur_coords - ul_coords down_vect = dl_coords - ul_coords - center_coords = ul_coords + (right_vect + down_vect) / 2 - sub_image = Image.fromarray( - image_mobject.get_pixel_array(), - mode="RGBA" - ) + impa = image_mobject.pixel_array - # Reshape - pixel_width = int(pdist([ul_coords, ur_coords])) - pixel_height = int(pdist([ul_coords, dl_coords])) - sub_image = sub_image.resize( - (pixel_width, pixel_height), resample=Image.BICUBIC - ) + oh, ow = self.pixel_array.shape[:2] # Outer width and height + ih, iw = impa.shape[:2] # inner with and height + rgb_len = self.pixel_array.shape[2] - # Rotate - angle = angle_of_vector(right_vect) - adjusted_angle = -int(360 * angle / TAU) - if adjusted_angle != 0: - sub_image = sub_image.rotate( - adjusted_angle, resample=Image.BICUBIC, expand=1 - ) + image = np.zeros((oh, ow, rgb_len), dtype=self.pixel_array_dtype) - # TODO, there is no accounting for a shear... + if right_vect[1] == 0 and down_vect[0] == 0: + rv0 = right_vect[0] + dv1 = down_vect[1] + x_indices = np.arange(rv0, dtype='int') * iw / rv0 + y_indices = np.arange(dv1, dtype='int') * ih / dv1 + stretched_impa = impa[y_indices][:, x_indices] - # Paste into an image as large as the camear's pixel array - full_image = Image.fromarray( - np.zeros((self.get_pixel_height(), self.get_pixel_width())), - mode="RGBA" - ) - new_ul_coords = center_coords - np.array(sub_image.size) / 2 - full_image.paste( - sub_image, - box=( - new_ul_coords[0], - new_ul_coords[1], - new_ul_coords[0] + sub_image.size[0], - new_ul_coords[1] + sub_image.size[1], - ) - ) + x0, x1 = ul_coords[0], ur_coords[0] + y0, y1 = ul_coords[1], dl_coords[1] + if x0 >= ow or x1 < 0 or y0 >= oh or y1 < 0: + return + siy0 = max(-y0, 0) # stretched_impa y0 + siy1 = dv1 - max(y1 - oh, 0) + six0 = max(-x0, 0) + six1 = rv0 - max(x1 - ow, 0) + x0 = max(x0, 0) + y0 = max(y0, 0) + image[y0:y1, x0:x1] = stretched_impa[siy0:siy1, six0:six1] + else: + # Alternate (slower) tactic if image is tilted + # List of all coordinates of pixels, given as (x, y), + # which matches the return type of points_to_pixel_coords, + # even though np.array indexing naturally happens as (y, x) + all_pixel_coords = np.zeros((oh * ow, 2), dtype='int') + a = np.arange(oh * ow, dtype='int') + all_pixel_coords[:, 0] = a % ow + all_pixel_coords[:, 1] = a / ow - # Paint on top of existing pixel array - self.overlay_PIL_image(full_image) + recentered_coords = all_pixel_coords - ul_coords + + with np.errstate(divide='ignore'): + ix_coords, iy_coords = [ + np.divide( + dim * np.dot(recentered_coords, vect), + np.dot(vect, vect), + ) + for vect, dim in (right_vect, iw), (down_vect, ih) + ] + to_change = reduce(op.and_, [ + ix_coords >= 0, ix_coords < iw, + iy_coords >= 0, iy_coords < ih, + ]) + inner_flat_coords = iw * \ + iy_coords[to_change] + ix_coords[to_change] + flat_impa = impa.reshape((iw * ih, rgb_len)) + target_rgbas = flat_impa[inner_flat_coords, :] + + image = image.reshape((ow * oh, rgb_len)) + image[to_change] = target_rgbas + image = image.reshape((oh, ow, rgb_len)) + self.overlay_rgba_array(image) def overlay_rgba_array(self, arr): - self.overlay_PIL_image(Image.fromarray(arr, mode="RGBA")) + fg = arr + bg = self.pixel_array + # rgba_max_val = self.rgb_max_val + src_rgb, src_a, dst_rgb, dst_a = [ + a.astype(np.float32) / self.rgb_max_val + for a in fg[..., :3], fg[..., 3], bg[..., :3], bg[..., 3] + ] - def overlay_PIL_image(self, image): - self.pixel_array = np.array( - Image.alpha_composite(self.get_image(), image) + out_a = src_a + dst_a * (1.0 - src_a) + + # When the output alpha is 0 for full transparency, + # we have a choice over what RGB value to use in our + # output representation. We choose 0 here. + out_rgb = fdiv( + src_rgb * src_a[..., None] + + dst_rgb * dst_a[..., None] * (1.0 - src_a[..., None]), + out_a[..., None], + zero_over_zero_value=0 ) + self.pixel_array[..., :3] = out_rgb * self.rgb_max_val + self.pixel_array[..., 3] = out_a * self.rgb_max_val + + def align_points_to_camera(self, points): + # This is where projection should live + return points - self.space_center + def adjust_out_of_range_points(self, points): if not np.any(points > self.max_allowable_norm): return points @@ -487,43 +483,31 @@ class Camera(object): return points def points_to_pixel_coords(self, points): - shifted_points = points - self.get_frame_center() - result = np.zeros((len(points), 2)) - pixel_height = self.get_pixel_height() - pixel_width = self.get_pixel_width() - frame_height = self.get_frame_height() - frame_width = self.get_frame_width() - width_mult = pixel_width / frame_width - width_add = pixel_width / 2 - height_mult = pixel_height / frame_height - height_add = pixel_height / 2 + ph, pw = self.pixel_shape + sh, sw = self.frame_shape + width_mult = pw / sw + width_add = pw / 2 + height_mult = ph / sh + height_add = ph / 2 # Flip on y-axis as you go height_mult *= -1 - result[:, 0] = shifted_points[:, 0] * width_mult + width_add - result[:, 1] = shifted_points[:, 1] * height_mult + height_add + result[:, 0] = points[:, 0] * width_mult + width_add + result[:, 1] = points[:, 1] * height_mult + height_add return result.astype('int') def on_screen_pixels(self, pixel_coords): return reduce(op.and_, [ pixel_coords[:, 0] >= 0, - pixel_coords[:, 0] < self.get_pixel_width(), + pixel_coords[:, 0] < self.pixel_shape[1], pixel_coords[:, 1] >= 0, - pixel_coords[:, 1] < self.get_pixel_height(), + pixel_coords[:, 1] < self.pixel_shape[0], ]) def adjusted_thickness(self, thickness): - # TODO: This seems...unsystematic - big_sum = op.add( - PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_height"], - PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"], - ) - this_sum = op.add( - self.get_pixel_height(), - self.get_pixel_width(), - ) - factor = fdiv(big_sum, this_sum) + big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"] + factor = sum(big_shape) / sum(self.pixel_shape) return 1 + (thickness - 1) / factor def get_thickening_nudges(self, thickness): @@ -541,20 +525,13 @@ class Camera(object): def get_coords_of_all_pixels(self): # These are in x, y order, to help me keep things straight - full_space_dims = np.array([ - self.get_frame_width(), - self.get_frame_height() - ]) - full_pixel_dims = np.array([ - self.get_pixel_width(), - self.get_pixel_height() - ]) + full_space_dims = np.array(self.frame_shape)[::-1] + full_pixel_dims = np.array(self.pixel_shape)[::-1] # These are addressed in the same y, x order as in pixel_array, but the values in them # are listed in x, y order - uncentered_pixel_coords = np.indices( - [self.get_pixel_height(), self.get_pixel_width()] - )[::-1].transpose(1, 2, 0) + uncentered_pixel_coords = np.indices(self.pixel_shape)[ + ::-1].transpose(1, 2, 0) uncentered_space_coords = fdiv( uncentered_pixel_coords * full_space_dims, full_pixel_dims) @@ -564,8 +541,7 @@ class Camera(object): # overflow is unlikely to be a problem) centered_space_coords = ( - uncentered_space_coords - fdiv(full_space_dims, 2) - ) + uncentered_space_coords - fdiv(full_space_dims, 2)) # Have to also flip the y coordinates to account for pixel array being listed in # top-to-bottom order, opposite of screen coordinate convention diff --git a/camera/mapping_camera.py b/camera/mapping_camera.py index d85f51ca..b55d4bc0 100644 --- a/camera/mapping_camera.py +++ b/camera/mapping_camera.py @@ -43,9 +43,8 @@ class MappingCamera(Camera): # TODO: Add optional separator borders between cameras (or perhaps peel this off into a # CameraPlusOverlay class) -# TODO, the classes below should likely be deleted -class OldMultiCamera(Camera): +class MultiCamera(Camera): def __init__(self, *cameras_with_start_positions, **kwargs): self.shifted_cameras = [ DictAsObject( @@ -53,8 +52,8 @@ class OldMultiCamera(Camera): "camera": camera_with_start_positions[0], "start_x": camera_with_start_positions[1][1], "start_y": camera_with_start_positions[1][0], - "end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].get_pixel_width(), - "end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].get_pixel_height(), + "end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].pixel_shape[1], + "end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].pixel_shape[0], }) for camera_with_start_positions in cameras_with_start_positions ] @@ -93,23 +92,23 @@ class OldMultiCamera(Camera): for shifted_camera in self.shifted_cameras: shifted_camera.camera.init_background() -# A OldMultiCamera which, when called with two full-size cameras, initializes itself +# A MultiCamera 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(OldMultiCamera): +class SplitScreenCamera(MultiCamera): def __init__(self, left_camera, right_camera, **kwargs): digest_config(self, kwargs) self.left_camera = left_camera self.right_camera = right_camera - half_width = self.get_pixel_width() / 2 + half_width = self.pixel_shape[1] / 2 for camera in [self.left_camera, self.right_camera]: # TODO: Round up on one if width is odd - camera.reset_pixel_shape(camera.get_pixel_height(), half_width) + camera.pixel_shape = (self.pixel_shape[0], half_width) + camera.init_background() + camera.resize_frame_shape() + camera.reset() - OldMultiCamera.__init__( - self, - (left_camera, (0, 0)), - (right_camera, (0, half_width)), - ) + MultiCamera.__init__(self, (left_camera, (0, 0)), + (right_camera, (0, half_width))) diff --git a/camera/moving_camera.py b/camera/moving_camera.py index 16585f54..dab64fe7 100644 --- a/camera/moving_camera.py +++ b/camera/moving_camera.py @@ -1,77 +1,40 @@ 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): """ - Stays in line with the height, width and position of it's 'frame', which is a Rectangle + Stays in line with the height, width and position + of a given mobject """ - CONFIG = { - "fixed_dimension": 0, # width - "default_frame_stroke_color": WHITE, - "default_frame_stroke_width": 0, + "aligned_dimension": "width" # or height } def __init__(self, frame=None, **kwargs): """ - frame is a Mobject, (should almost certainly be a rectangle) - determining which region of space the camera displys + 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.set_stroke( - self.default_frame_stroke_color, - self.default_frame_stroke_width, - ) + frame.fade(1) self.frame = frame Camera.__init__(self, **kwargs) - # TODO, make these work for a rotated frame - def get_frame_height(self): - return self.frame.get_height() + def capture_mobjects(self, *args, **kwargs): + self.space_center = self.frame.get_center() + self.realign_frame_shape() + Camera.capture_mobjects(self, *args, **kwargs) - def get_frame_width(self): - return self.frame.get_width() - - def get_frame_center(self): - return self.frame.get_center() - - def set_frame_height(self, frame_height): - self.frame.stretch_to_fit_height(frame_height) - - def set_frame_width(self, frame_width): - self.frame.stretch_to_fit_width(frame_width) - - def set_frame_center(self, frame_center): - self.frame.move_to(frame_center) - - def capture_mobjects(self, mobjects, **kwargs): - # self.reset_frame_center() - # self.realign_frame_shape() - Camera.capture_mobjects(self, mobjects, **kwargs) - - # def reset_frame_center(self): - # self.frame_center = self.frame.get_center() - - # def realign_frame_shape(self): - # height, width = self.frame_shape - # if self.fixed_dimension == 0: - # self.frame_shape = (height, self.frame.get_width()) - # else: - # self.frame_shape = (self.frame.get_height(), width) - # self.resize_frame_shape(fixed_dimension=self.fixed_dimension) - - def get_mobjects_indicating_movement(self): - """ - Returns all mobjets whose movement implies that the camera - should think of all other mobjects on the screen as moving - """ - return [self.frame] + def realign_frame_shape(self): + height, width = self.frame_shape + if self.aligned_dimension == "height": + self.frame_shape = (self.frame.get_height(), width) + else: + self.frame_shape = (height, self.frame.get_width()) + self.resize_frame_shape(0 if self.aligned_dimension == "height" else 1) diff --git a/camera/multi_camera.py b/camera/multi_camera.py deleted file mode 100644 index 42b428cd..00000000 --- a/camera/multi_camera.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import absolute_import - -from camera.moving_camera import MovingCamera -from utils.iterables import list_difference_update - - -class MultiCamera(MovingCamera): - CONFIG = { - "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 - assert(isinstance(imfc.camera, MovingCamera)) - self.image_mobjects_from_cameras.append(imfc) - - def update_sub_cameras(self): - """ Reshape sub_camera pixel_arrays """ - for imfc in self.image_mobjects_from_cameras: - 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() / self.get_frame_height()), - int(pixel_width * imfc.get_width() / self.get_frame_width()), - ) - - def reset(self): - for imfc in self.image_mobjects_from_cameras: - imfc.camera.reset() - MovingCamera.reset(self) - return self - - def capture_mobjects(self, mobjects, **kwargs): - 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) - - def get_mobjects_indicating_movement(self): - return [self.frame] + [ - imfc.camera.frame - for imfc in self.image_mobjects_from_cameras - ] diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index 2bcb750a..84ddece8 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -12,8 +12,6 @@ from utils.bezier import interpolate from utils.space_ops import rotation_about_z from utils.space_ops import rotation_matrix -# TODO: Make sure this plays well with latest camera updates - class CameraWithPerspective(Camera): CONFIG = { @@ -51,7 +49,7 @@ class ThreeDCamera(CameraWithPerspective): self.rotation_mobject = VectorizedPoint() # moving_center lives in the x-y-z space # It representes the center of rotation - self.moving_center = VectorizedPoint(self.frame_center) + self.moving_center = VectorizedPoint(self.space_center) self.set_position(self.phi, self.theta, self.distance) def modified_rgb(self, vmobject, rgb): @@ -165,7 +163,7 @@ class ThreeDCamera(CameraWithPerspective): center_of_rotation = self.get_center_of_rotation( center_x, center_y, center_z) self.moving_center.move_to(center_of_rotation) - self.frame_center = self.moving_center.points[0] + self.space_center = self.moving_center.points[0] def get_view_transformation_matrix(self): return (self.default_distance / self.get_distance()) * np.dot( @@ -176,6 +174,6 @@ class ThreeDCamera(CameraWithPerspective): def points_to_pixel_coords(self, points): matrix = self.get_view_transformation_matrix() new_points = np.dot(points, matrix.T) - self.frame_center = self.moving_center.points[0] + self.space_center = self.moving_center.points[0] return Camera.points_to_pixel_coords(self, new_points) diff --git a/constants.py b/constants.py index 888b0c31..a87edffa 100644 --- a/constants.py +++ b/constants.py @@ -17,20 +17,17 @@ LOW_QUALITY_FRAME_DURATION = 1. / 15 MEDIUM_QUALITY_FRAME_DURATION = 1. / 30 PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60 -# There might be other configuration than pixel shape later... +# There might be other configuration than pixel_shape later... PRODUCTION_QUALITY_CAMERA_CONFIG = { - "pixel_height": DEFAULT_PIXEL_HEIGHT, - "pixel_width": DEFAULT_PIXEL_WIDTH, + "pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH), } MEDIUM_QUALITY_CAMERA_CONFIG = { - "pixel_height": 720, - "pixel_width": 1280, + "pixel_shape": (720, 1280), } LOW_QUALITY_CAMERA_CONFIG = { - "pixel_height": 480, - "pixel_width": 854, + "pixel_shape": (480, 854), } DEFAULT_POINT_DENSITY_2D = 25 diff --git a/extract_scene.py b/extract_scene.py index 1cddadc1..f7c91f94 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -224,8 +224,11 @@ def get_module_posix(file_name): module_name = file_name.replace(".py", "") last_module = imp.load_module(".", *imp.find_module(".")) for part in module_name.split(os.sep): - load_args = imp.find_module(part, last_module.__path__) - last_module = imp.load_module(part, *load_args) + try: + load_args = imp.find_module(part, last_module.__path__) + last_module = imp.load_module(part, *load_args) + except ImportError: + continue return last_module diff --git a/mobject/mobject.py b/mobject/mobject.py index c6364fa3..8283c345 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -199,15 +199,6 @@ class Mobject(Container): ) return self - def apply_function_to_position(self, function): - self.move_to(function(self.get_center())) - return self - - def apply_function_to_submobject_positions(self, function): - for submob in self.submobjects: - submob.apply_function_to_position(function) - return self - def apply_matrix(self, matrix, **kwargs): # Default to applying matrix about the origin, not mobjects center if len(kwargs) == 0: diff --git a/mobject/types/image_mobject.py b/mobject/types/image_mobject.py index 9d6878b4..ca8c03bd 100644 --- a/mobject/types/image_mobject.py +++ b/mobject/types/image_mobject.py @@ -7,49 +7,23 @@ 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 AbstractImageMobject(Mobject): +class ImageMobject(Mobject): """ Automatically filters out black pixels """ CONFIG = { - "height": 2.0, - "pixel_array_dtype": "uint8", - } - - def get_pixel_array(self): - raise Exception("Not implemented") - - def set_color(self): - # Likely to be implemented in subclasses, but no obgligation - pass - - def init_points(self): - # Corresponding corners of image are fixed to these 3 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): - CONFIG = { + "filter_color": "black", "invert": False, + # "use_cache" : True, + "height": 2.0, "image_mode": "RGBA", + "pixel_array_dtype": "uint8", } def __init__(self, filename_or_array, **kwargs): @@ -63,7 +37,7 @@ class ImageMobject(AbstractImageMobject): self.change_to_rgba_array() if self.invert: self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3] - AbstractImageMobject.__init__(self, **kwargs) + Mobject.__init__(self, **kwargs) def change_to_rgba_array(self): pa = self.pixel_array @@ -79,9 +53,6 @@ class ImageMobject(AbstractImageMobject): 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 @@ -92,6 +63,19 @@ class ImageMobject(AbstractImageMobject): 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 @@ -106,30 +90,5 @@ class ImageMobject(AbstractImageMobject): mobject1.pixel_array, mobject2.pixel_array, alpha ).astype(self.pixel_array_dtype) -# TODO, add the ability to have the dimensions/orientation of this -# mobject more strongly tied to the frame of the camera it contains, -# in the case where that's a MovingCamera - - -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 + def copy(self): + return self.deepcopy() diff --git a/old_projects/WindingNumber.py b/old_projects/WindingNumber.py index b94a5ef6..c12c5cbd 100644 --- a/old_projects/WindingNumber.py +++ b/old_projects/WindingNumber.py @@ -668,7 +668,7 @@ class ColorMappedByFuncScene(Scene): # We hash just based on output image # Thus, multiple scenes with same output image can re-use it # without recomputation - full_hash = hash((func_hash, self.camera.get_pixel_width())) + full_hash = hash((func_hash, self.camera.pixel_shape)) self.background_image_file = self.short_path_to_long_path( "color_mapped_bg_hash_" + str(full_hash) + ".png" ) diff --git a/old_projects/WindingNumber_G.py b/old_projects/WindingNumber_G.py index b5880ed3..4b65bb31 100644 --- a/old_projects/WindingNumber_G.py +++ b/old_projects/WindingNumber_G.py @@ -2884,7 +2884,7 @@ class ZeroFoundOnBoundary(Scene): class AllOfTheVideos(Scene): CONFIG = { "camera_config" : { - "background_opacity" : 1, + "background_alpha" : 255, } } def construct(self): diff --git a/old_projects/basel/basel.py b/old_projects/basel/basel.py index c3301a43..69809107 100644 --- a/old_projects/basel/basel.py +++ b/old_projects/basel/basel.py @@ -2056,7 +2056,7 @@ class IPTScene1(PiCreatureScene): # use the following for the zoomed inset if show_detail: self.camera.frame_shape = (0.02 * FRAME_HEIGHT, 0.02 * FRAME_WIDTH) - self.camera.frame_center = C + self.camera.space_center = C SCREEN_SCALE = 0.01 SCREEN_THICKNESS = 0.02 diff --git a/old_projects/nn/part1.py b/old_projects/nn/part1.py index 177b9622..cdd3618d 100644 --- a/old_projects/nn/part1.py +++ b/old_projects/nn/part1.py @@ -1479,7 +1479,7 @@ class AskAboutLayers(PreviewMNistNetwork): class BreakUpMacroPatterns(IntroduceEachLayer): CONFIG = { - "camera_config" : {"background_opacity" : 1}, + "camera_config" : {"background_alpha" : 255}, "prefixes" : [ "nine", "eight", "four", "upper_loop", "right_line", @@ -1868,7 +1868,7 @@ class BreakUpMicroPatterns(BreakUpMacroPatterns): class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer): CONFIG = { "camera_config" : { - "background_opacity" : 1, + "background_alpha" : 255, }, "network_mob_config" : { "layer_to_layer_buff" : 2, @@ -2081,7 +2081,7 @@ class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer): class EdgeDetection(Scene): CONFIG = { - "camera_config" : {"background_opacity" : 1} + "camera_config" : {"background_alpha" : 255} } def construct(self): lion = ImageMobject("Lion") diff --git a/old_projects/pi_day.py b/old_projects/pi_day.py index c5c3a3bd..bea9b697 100644 --- a/old_projects/pi_day.py +++ b/old_projects/pi_day.py @@ -468,7 +468,7 @@ class TauFalls(Scene): class EulerWrites628(Scene): CONFIG = { "camera_config" : { - "background_opacity" : 1, + "background_alpha" : 255, } } def construct(self): @@ -548,7 +548,7 @@ class EulerWrites628(Scene): class HeroAndVillain(Scene): CONFIG = { "camera_config" : { - "background_opacity" : 1, + "background_alpha" : 255, } } def construct(self): @@ -987,7 +987,7 @@ class SpecialThanks(Scene): class EndScene(PatreonEndScreen): CONFIG = { "camera_config" : { - "background_opacity" : 1, + "background_alpha" : 255, } } def construct(self): diff --git a/old_projects/wallis.py b/old_projects/wallis.py index b071e81f..707d7d94 100644 --- a/old_projects/wallis.py +++ b/old_projects/wallis.py @@ -4958,7 +4958,7 @@ class KeeperAndSailorForSineProduct(KeeperAndSailor): class Conclusion(TeacherStudentsScene): CONFIG = { - "camera_config": {"background_opacity": 1}, + "camera_config": {"background_alpha": 255}, } def construct(self): diff --git a/scene/moving_camera_scene.py b/scene/moving_camera_scene.py index 29e1430f..f3f5ed74 100644 --- a/scene/moving_camera_scene.py +++ b/scene/moving_camera_scene.py @@ -1,32 +1,26 @@ from __future__ import absolute_import +from constants import * + from scene.scene import Scene from camera.moving_camera import MovingCamera -from utils.iterables import list_update +from mobject.frame import ScreenRectangle class MovingCameraScene(Scene): - CONFIG = { - "camera_class": MovingCamera - } - def setup(self): - Scene.setup(self) - assert(isinstance(self.camera, MovingCamera)) - self.camera_frame = self.camera.frame - # Hmm, this currently relies on the fact that MovingCamera - # willd default to a full-sized frame. Is that okay? + self.camera_frame = ScreenRectangle(height=FRAME_HEIGHT) + self.camera_frame.set_stroke(width=0) + self.camera = MovingCamera( + self.camera_frame, **self.camera_config + ) return self def get_moving_mobjects(self, *animations): + # TODO: Code repetition from ZoomedScene moving_mobjects = Scene.get_moving_mobjects(self, *animations) - all_moving_mobjects = self.camera.extract_mobject_family_members( - moving_mobjects - ) - movement_indicators = self.camera.get_mobjects_indicating_movement() - for movement_indicator in movement_indicators: - if movement_indicator in all_moving_mobjects: - # When one of these is moving, the camera should - # consider all mobjects to be moving - return list_update(self.mobjects, moving_mobjects) - return moving_mobjects + if self.camera_frame in moving_mobjects: + # When the camera is moving, so is everything, + return self.mobjects + else: + return moving_mobjects diff --git a/scene/scene.py b/scene/scene.py index 364e6348..f136982f 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -572,8 +572,7 @@ class Scene(Container): self.args_to_rename_file = (temp_file_path, file_path) fps = int(1 / self.frame_duration) - height = self.camera.get_pixel_height() - width = self.camera.get_pixel_width() + height, width = self.camera.pixel_shape command = [ FFMPEG_BIN, diff --git a/scene/zoomed_scene.py b/scene/zoomed_scene.py index 2d3d4ac0..71c623b3 100644 --- a/scene/zoomed_scene.py +++ b/scene/zoomed_scene.py @@ -1,19 +1,20 @@ from __future__ import absolute_import -from scene.moving_camera_scene import MovingCameraScene +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.types.image_mobject import ImageMobjectFromCamera -from utils.simple_functions import fdiv +from mobject.geometry import Rectangle from constants import * -from animation.transform import ApplyMethod - -# Note, any scenes from old videos using ZoomedScene will almost certainly -# break, as it was restructured. -class ZoomedScene(MovingCameraScene): +class ZoomedScene(Scene): + """ + Move around self.little_rectangle to determine + which part of the screen is zoomed in on. + """ CONFIG = { "camera_class": MultiCamera, "zoomed_display_height": 3, @@ -32,62 +33,109 @@ class ZoomedScene(MovingCameraScene): "zoom_activated": False, } - def setup(self): - MovingCameraScene.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) + def activate_zooming(self): + self.generate_big_rectangle() + self.setup_zoomed_canvas() + self.setup_zoomed_camera() + self.zoom_activated = True - # 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 + def animate_activate_zooming(self): + self.activate_zooming() + self.play(*map(FadeIn, [ + self.little_rectangle, self.big_rectangle + ])) + + def disactivate_zooming(self): + self.remove(self.big_rectangle, self.little_rectangle) + self.zoom_activated = False + + def get_zoomed_camera_mobject(self): + return self.little_rectangle + + def get_zoomed_screen(self): + return self.big_rectangle + + def generate_big_rectangle(self): + height, width = self.zoomed_canvas_frame_shape + self.big_rectangle = Rectangle( + height=height, + width=width, + color=self.square_color + ) + if self.zoomed_canvas_center is not None: + self.big_rectangle.shift(self.zoomed_canvas_center) + elif self.zoomed_canvas_corner is not None: + self.big_rectangle.to_corner( + self.zoomed_canvas_corner, + buff=self.zoomed_canvas_corner_buff + ) + self.add(self.big_rectangle) + + def setup_zoomed_canvas(self): + upper_left = self.big_rectangle.get_corner(UP + LEFT) + lower_right = self.big_rectangle.get_corner(DOWN + RIGHT) + pixel_coords = self.camera.points_to_pixel_coords( + np.array([upper_left, lower_right]) + ) + self.zoomed_canvas_pixel_indices = pixel_coords + (up, left), (down, right) = pixel_coords + self.zoomed_canvas_pixel_shape = ( + right - left, + down - up, + ) + + def setup_zoomed_camera(self): + self.little_rectangle = self.big_rectangle.copy() + self.little_rectangle.scale(1. / self.zoom_factor) + self.little_rectangle.move_to( + self.little_rectangle_start_position + ) + self.zoomed_camera = MovingCamera( + self.little_rectangle, + pixel_shape=self.zoomed_canvas_pixel_shape, + background=self.zoomed_camera_background + ) + self.add(self.little_rectangle) + # TODO, is there a better way to hanld this? + self.zoomed_camera.adjusted_thickness = lambda x: x + + def get_frame(self): + frame = Scene.get_frame(self) + if self.zoom_activated: + (up, left), (down, right) = self.zoomed_canvas_pixel_indices + frame[left:right, up:down, :] = self.zoomed_camera.get_image() + return frame + + def set_camera_pixel_array(self, pixel_array): + self.camera.set_pixel_array(pixel_array) + if self.zoom_activated: + (up, left), (down, right) = self.zoomed_canvas_pixel_indices + self.zoomed_camera.set_pixel_array( + pixel_array[left:right, up:down]) + + def set_camera_background(self, background): + self.set_camera_pixel_array(self, background) + # TODO, check this... + + def reset_camera(self): + self.camera.reset() + if self.zoom_activated: + self.zoomed_camera.reset() + + def capture_mobjects_in_camera(self, mobjects, **kwargs): + self.camera.capture_mobjects(mobjects, **kwargs) + if self.zoom_activated: + if self.big_rectangle in mobjects: + mobjects = list(mobjects) + mobjects.remove(self.big_rectangle) + self.zoomed_camera.capture_mobjects( + mobjects, **kwargs ) - self.zoomed_camera = zoomed_camera - self.zoomed_display = zoomed_display - - def activate_zooming(self, animate=False): - self.zoom_activated = True - self.camera.add_image_mobject_from_camera(self.zoomed_display) - if animate: - self.play(self.get_zoom_in_animation()) - self.play(self.get_zoomed_display_pop_out_animation()) - self.add_foreground_mobjects( - self.zoomed_camera.frame, - self.zoomed_display, - ) - - def get_zoom_in_animation(self, run_time=2, **kwargs): - frame = self.zoomed_camera.frame - full_frame_height = self.camera.get_frame_height() - full_frame_width = self.camera.get_frame_width() - frame.save_state() - frame.stretch_to_fit_width(full_frame_width) - frame.stretch_to_fit_height(full_frame_height) - frame.center() - frame.set_stroke(width=0) - return ApplyMethod(frame.restore, run_time=run_time, **kwargs) - - def get_zoomed_display_pop_out_animation(self, **kwargs): - display = self.zoomed_display - display.save_state(use_deepcopy=True) - display.replace(self.zoomed_camera.frame, stretch=True) - return ApplyMethod(display.restore) - - def get_zoom_factor(self): - return fdiv( - self.zoomed_camera.frame.get_height(), - self.zoomed_display.get_height() - ) + def get_moving_mobjects(self, *animations): + moving_mobjects = Scene.get_moving_mobjects(self, *animations) + if self.zoom_activated and self.little_rectangle in moving_mobjects: + # When the camera is moving, so is everything, + return self.mobjects + else: + return moving_mobjects diff --git a/utils/color.py b/utils/color.py index f8999670..a3df972e 100644 --- a/utils/color.py +++ b/utils/color.py @@ -39,8 +39,7 @@ def color_to_int_rgb(color): return (255 * color_to_rgb(color)).astype('uint8') -def color_to_int_rgba(color, opacity=1.0): - alpha = int(255 * opacity) +def color_to_int_rgba(color, alpha=255): return np.append(color_to_int_rgb(color), alpha) diff --git a/utils/output_directory_getters.py b/utils/output_directory_getters.py index 14d317fc..8011392e 100644 --- a/utils/output_directory_getters.py +++ b/utils/output_directory_getters.py @@ -34,7 +34,7 @@ def get_scene_output_directory(scene_class): def get_movie_output_directory(scene_class, camera_config, frame_duration): directory = get_scene_output_directory(scene_class) sub_dir = "%dp%d" % ( - camera_config["pixel_height"], + camera_config["pixel_shape"][0], int(1.0 / frame_duration) ) return guarantee_existance(os.path.join(directory, sub_dir)) From ee164b205e1bb41f0373e6fb1bf8009e7c474557 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 21 May 2018 12:23:44 -0700 Subject: [PATCH 2/3] Revert "End of Alt calc project" This reverts commit 0f3b54694d5bfed034b24c931afce554e5634a96. --- active_projects/alt_calc.py | 488 +++++++++++++++++++++++++++++++- old_projects/WindingNumber_G.py | 1 - 2 files changed, 473 insertions(+), 16 deletions(-) diff --git a/active_projects/alt_calc.py b/active_projects/alt_calc.py index 454e426a..688284c2 100644 --- a/active_projects/alt_calc.py +++ b/active_projects/alt_calc.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from big_ol_pile_of_manim_imports import * @@ -316,7 +317,7 @@ class NumberlineTransformationScene(ZoomedScene): zcbr_group.add(mini_line_copy, mini_line) anims += [FadeIn(mini_line), FadeIn(mini_line_copy)] - # Add tiny coordiantes + # Add tiny coordinates if local_coordinate_values is None: local_coordinate_values = [x] local_coordinates = self.get_local_coordinates( @@ -660,7 +661,7 @@ class StartingCalc101(PiCreatureScene): circumference = TexMobject("2\\pi r") circumference.move_to(unfilled_circle) equation = TexMobject( - "{d (\\pi r^2) \\over dx} = 2\\pi r", + "{d (\\pi r^2) \\over dr} = 2\\pi r", tex_to_color_map={ "\\pi r^2": BLUE_D, "2\\pi r": YELLOW, @@ -875,8 +876,8 @@ class Wrapper(Scene): } def construct(self): - rect = ScreenRectangle(height=self.screen_height) - title = TextMobject(self.title, **self.title_kwargs) + rect = self.rect = ScreenRectangle(height=self.screen_height) + title = self.title = TextMobject(self.title, **self.title_kwargs) title.to_edge(UP) rect.next_to(title, DOWN) @@ -1562,11 +1563,11 @@ class TalkThroughXSquaredExample(IntroduceTransformationView): la, ra = ra, la if factor < 0: kwargs = { - "path_arc": -np.pi, + "path_arc": np.pi, "use_rectangular_stem": False, } - la = Arrow(DOWN, UP, **kwargs) - ra = Arrow(UP, DOWN, **kwargs) + la = Arrow(UP, DOWN, **kwargs) + ra = Arrow(DOWN, UP, **kwargs) for arrow in la, ra: arrow.pointwise_become_partial(arrow, 0, 0.9) arrow.tip.scale(2) @@ -1871,9 +1872,8 @@ class ZoomInMoreAndMoreToZero(ZoomInOnXSquaredNearZero): frame.scale_to_fit_height(factor * zoomed_display_height) self.local_coordinate_num_decimal_places = int(-np.log10(factor)) zoom_words = TextMobject( - "Zoomed ", "{:,}".format(int(1.0 / factor)), - "x \\\\", "near 0", - arg_separator="" + "Zoomed", "{:,}x \\\\".format(int(1.0 / factor)), + "near 0", ) zoom_words.next_to(self.zoomed_display, DOWN) @@ -1954,6 +1954,14 @@ class XSquaredForNegativeInput(TalkThroughXSquaredExample): sample_dots.set_fill(opacity=0.8) self.play(LaggedStart(DrawBorderThenFill, sample_dots)) + self.play(LaggedStart( + ApplyFunction, sample_dots[len(sample_dots) / 2:0:-1], + lambda mob: ( + lambda m: m.scale(2).shift(SMALL_BUFF * UP).set_color(PINK), + mob, + ), + rate_func=there_and_back, + )) self.add_sample_dot_ghosts(sample_dots) self.apply_function(self.func, sample_dots=sample_dots) self.wait() @@ -3207,6 +3215,7 @@ class StabilityAndInstability(AnalyzeFunctionWithTransformations): CONFIG = { "num_initial_applications": 0, } + def construct(self): self.force_skipping() self.add_function_title() @@ -3237,9 +3246,10 @@ class StabilityAndInstability(AnalyzeFunctionWithTransformations): self.all_arrows )).copy() arrows.set_fill(PINK, 1) + arrows.set_stroke(PINK, 3) arrows.second_anim = LaggedStart( ApplyMethod, arrows, - lambda m: (m.set_fill, YELLOW, 1), + lambda m: (m.set_color, YELLOW), rate_func=there_and_back_with_pause, lag_ratio=0.7, run_time=2, @@ -3269,11 +3279,87 @@ class StabilityAndInstability(AnalyzeFunctionWithTransformations): def write_derivative_fact(self): stable_label = self.stable_label unstable_label = self.unstable_label + labels = VGroup(stable_label, unstable_label) phi_arrows = self.phi_arrows phi_bro_arrows = self.phi_bro_arrows - arrow_groups = [phi_arrows, phi_bro_arrows] + arrow_groups = VGroup(phi_arrows, phi_bro_arrows) + + deriv_labels = VGroup() + for char, label in zip("<>", labels): + deriv_label = TexMobject( + "\\big|", "\\frac{df}{dx}(", "x", ")", "\\big|", + char, "1" + ) + deriv_label.get_parts_by_tex("\\big|").match_height( + deriv_label, stretch=True + ) + deriv_label.set_color_by_tex("x", YELLOW, substring=False) + deriv_label.next_to(label, UP) + deriv_labels.add(deriv_label) + + dot_groups = VGroup() + for arrow_group in arrow_groups: + dot_group = VGroup() + for arrow in arrow_group: + start_point, end_point = [ + line.number_to_point(line.point_to_number(p)) + for line, p in [ + (self.input_line, arrow.get_start()), + (self.output_line, arrow.get_end()), + ] + ] + dot = Dot(start_point, radius=0.05) + dot.set_color(YELLOW) + dot.generate_target() + dot.target.move_to(end_point) + dot_group.add(dot) + dot_groups.add(dot_group) + + for deriv_label, dot_group in zip(deriv_labels, dot_groups): + self.play(FadeInFromDown(deriv_label)) + self.play(LaggedStart(GrowFromCenter, dot_group)) + self.play(*map(MoveToTarget, dot_group), run_time=2) + self.wait() +class StaticAlgebraicObject(Scene): + def construct(self): + frac = get_phi_continued_fraction(40) + frac.scale_to_fit_width(FRAME_WIDTH - 1) + # frac.shift(2 * DOWN) + frac.to_edge(DOWN) + frac.set_stroke(WHITE, width=0.5) + + title = TexMobject( + "\\infty \\ne \\lim", + tex_to_color_map={"\\ne": RED} + ) + title.scale(1.5) + title.to_edge(UP) + + polynomial = TexMobject("x^2 - x - 1 = 0") + polynomial.move_to(title) + + self.add(title) + self.play(LaggedStart( + GrowFromCenter, frac, + lag_ratio=0.1, + run_time=3 + )) + self.wait() + factor = 1.1 + self.play(frac.scale, factor, run_time=0.5) + self.play( + frac.scale, 1 / factor, + frac.set_color, LIGHT_GREY, + run_time=0.5, rate_func=lambda t: t**5, + ) + self.wait() + self.play( + FadeOut(title), + FadeIn(polynomial) + ) + self.wait(2) class NotBetterThanGraphs(TeacherStudentsScene): @@ -3305,6 +3391,23 @@ class NotBetterThanGraphs(TeacherStudentsScene): self.wait(3) +class WhatComesAfterWrapper(Wrapper): + CONFIG = {"title": "Beyond the first year"} + + def construct(self): + Wrapper.construct(self) + new_title = TextMobject("Next video") + new_title.set_color(BLUE) + new_title.move_to(self.title) + + self.play( + FadeInFromDown(new_title), + self.title.shift, UP, + self.title.fade, 1, + ) + self.wait(3) + + class TopicsAfterSingleVariable(PiCreatureScene, MoreTopics): CONFIG = { "pi_creatures_start_on_screen": False, @@ -3356,6 +3459,83 @@ class TopicsAfterSingleVariable(PiCreatureScene, MoreTopics): self.wait(4) +class ShowJacobianZoomedIn(LinearTransformationScene, ZoomedScene): + CONFIG = { + "show_basis_vectors": False, + "show_coordinates": True, + "zoom_factor": 0.05, + } + + def setup(self): + LinearTransformationScene.setup(self) + ZoomedScene.setup(self) + + def construct(self): + def example_function(point): + x, y, z = point + return np.array([ + x + np.sin(y), + y + np.sin(x), + 0 + ]) + + zoomed_camera = self.zoomed_camera + zoomed_display = self.zoomed_display + frame = zoomed_camera.frame + frame.move_to(3 * LEFT + 1 * UP) + frame.set_color(YELLOW) + zoomed_display.display_frame.set_color(YELLOW) + zd_rect = BackgroundRectangle( + zoomed_display, + fill_opacity=1, + buff=MED_SMALL_BUFF, + ) + self.add_foreground_mobject(zd_rect) + zd_rect.anim = UpdateFromFunc( + zd_rect, + lambda rect: rect.replace(zoomed_display).scale(1.1) + ) + zd_rect.next_to(FRAME_HEIGHT * UP, UP) + + tiny_grid = NumberPlane( + x_radius=2, + y_radius=2, + color=BLUE_E, + secondary_color=DARK_GREY, + ) + tiny_grid.replace(frame) + + jacobian_words = TextMobject("Jacobian") + jacobian_words.add_background_rectangle() + jacobian_words.scale(1.5) + jacobian_words.move_to(zoomed_display, UP) + zoomed_display.next_to(jacobian_words, DOWN) + + self.play(self.get_zoom_in_animation()) + self.activate_zooming() + self.play( + self.get_zoomed_display_pop_out_animation(), + zd_rect.anim + ) + self.play( + ShowCreation(tiny_grid), + Write(jacobian_words), + run_time=2 + ) + self.add_transformable_mobject(tiny_grid) + self.add_foreground_mobject(jacobian_words) + self.wait() + self.apply_nonlinear_transformation( + example_function, + added_anims=[MaintainPositionRelativeTo( + zoomed_camera.frame, tiny_grid, + )], + run_time=5 + ) + self.wait() + +# Video 2 + class ComplexAnalysisOverlay(Scene): def construct(self): words = TextMobject("Complex analysis") @@ -3365,6 +3545,7 @@ class ComplexAnalysisOverlay(Scene): self.add(words) self.wait() + class CompelxAnalyticFluidFlow(ComplexTransformationScene, MovingCameraScene): CONFIG = { "num_anchors_to_add_per_line": 200, @@ -3442,6 +3623,7 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene): CONFIG = { "num_anchors_to_add_per_line": 20, "complex_homotopy": lambda z, t: z**(1.0 + t), + "zoom_factor": 0.05, } def setup(self): @@ -3451,8 +3633,8 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene): def construct(self): self.edit_background_plane() self.add_title() - self.add_transforming_planes() - self.preview_some_numbers() + # self.add_transforming_planes() + # self.preview_some_numbers() self.zoom_in_to_one_plus_half_i() self.write_derivative() @@ -3548,9 +3730,44 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene): self.play(FadeOut(dot_groups)) self.wait() self.play(FadeOut(self.plane)) + self.transformable_mobjects.remove(self.plane) def zoom_in_to_one_plus_half_i(self): - pass + z = complex(1, 0.5) + point = self.background.number_to_point(z) + point_mob = VectorizedPoint(point) + frame = self.zoomed_camera.frame + frame.move_to(point) + tiny_plane = NumberPlane( + x_radius=2, y_radius=2, + color=GREEN, + secondary_color=GREEN_E + ) + tiny_plane.replace(frame) + + plane = self.get_plane() + + words = TextMobject("What does this look like") + words.add_background_rectangle() + words.next_to(self.zoomed_display, LEFT, aligned_edge=UP) + arrow = Arrow(words.get_bottom(), self.zoomed_display.get_left()) + VGroup(words, arrow).set_color(YELLOW) + + self.play(FadeIn(plane)) + self.activate_zooming(animate=True) + self.play(ShowCreation(tiny_plane)) + self.wait() + self.add_transformable_mobjects(plane, tiny_plane, point_mob) + self.add_foreground_mobjects(words, arrow) + self.apply_complex_homotopy( + self.complex_homotopy, + added_anims=[ + Write(words), + GrowArrow(arrow), + MaintainPositionRelativeTo(frame, point_mob) + ] + ) + self.wait(2) def write_derivative(self): pass @@ -3567,3 +3784,244 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene): top_plane.next_to(ORIGIN, UP, buff=tiny_tiny_buff) bottom_plane.next_to(ORIGIN, DOWN, buff=tiny_tiny_buff) return VGroup(top_plane, bottom_plane) + + +class PrinciplesOverlay(PiCreatureScene): + CONFIG = { + "default_pi_creature_kwargs": { + "color": GREY_BROWN, + "flip_at_start": True, + }, + "default_pi_creature_start_corner": DR, + } + + def construct(self): + morty = self.pi_creature + q_marks = VGroup(*[TexMobject("?") for x in range(40)]) + q_marks.arrange_submobjects_in_grid(4, 10) + q_marks.space_out_submobjects(1.4) + for mark in q_marks: + mark.shift( + random.random() * RIGHT, + random.random() * UP, + ) + mark.scale(1.5) + mark.set_stroke(BLACK, 1) + q_marks.next_to(morty, UP) + q_marks.shift_onto_screen() + q_marks.sort_submobjects( + lambda p: np.linalg.norm(p - morty.get_top()) + ) + + self.play(morty.change, "pondering") + self.wait(2) + self.play(morty.change, "raise_right_hand") + self.wait() + self.play(morty.change, "thinking") + self.wait(4) + self.play(FadeInFromDown(q_marks[0])) + self.wait(2) + self.play(LaggedStart( + FadeInFromDown, q_marks[1:], + run_time=3 + )) + self.wait(3) + + +class ManyInfiniteExpressions(Scene): + def construct(self): + frac = get_phi_continued_fraction(10) + frac.scale_to_fit_height(2) + frac.to_corner(UL) + + n = 9 + radical_str_parts = [ + "%d + \\sqrt{" % d + for d in range(1, n + 1) + ] + radical_str_parts += ["\\cdots"] + radical_str_parts += ["}"] * n + radical = TexMobject("".join(radical_str_parts)) + radical.to_corner(UR) + radical.to_edge(DOWN) + radical.set_color_by_gradient(YELLOW, RED) + + n = 12 + power_tower = TexMobject( + *["\\sqrt{2}^{"] * n + ["\\dots"] + ["}"] * n + ) + power_tower.to_corner(UR) + power_tower.set_color_by_gradient(BLUE, GREEN) + + self.play(*[ + LaggedStart( + GrowFromCenter, group, + lag_ratio=0.1, + run_time=8, + ) + for group in frac, radical, power_tower + ]) + self.wait(2) + + +class HoldUpPromo(PrinciplesOverlay): + def construct(self): + morty = self.pi_creature + + url = TextMobject("https://brilliant.org/3b1b/") + url.to_corner(UL) + + rect = ScreenRectangle(height=5.5) + rect.next_to(url, DOWN) + rect.to_edge(LEFT) + + self.play( + Write(url), + self.pi_creature.change, "raise_right_hand" + ) + self.play(ShowCreation(rect)) + self.wait(2) + self.change_mode("thinking") + self.wait() + self.look_at(url) + self.wait(10) + self.change_mode("happy") + self.wait(10) + self.change_mode("raise_right_hand") + self.wait(10) + + self.play(FadeOut(rect), FadeOut(url)) + self.play(morty.change, "raise_right_hand") + self.wait() + self.play(morty.change, "hooray") + self.wait(3) + + +class EndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "Juan Benet", + "Keith Smith", + "Chloe Zhou", + "Desmos", + "Burt Humburg", + "CrypticSwarm", + "Andrew Sachs", + "Ho\\`ang T\\`ung L\\^am", + # "Hoàng Tùng Lâm", + "Devin Scott", + "Akash Kumar", + "Felix Tripier", + "Arthur Zey", + "David Kedmey", + "Ali Yahya", + "Mayank M. Mehrotra", + "Lukas Biewald", + "Yana Chernobilsky", + "Kaustuv DeBiswas", + "Yu Jun", + "dave nicponski", + "Damion Kistler", + "Jordan Scales", + "Markus Persson", + "Fela", + "Fred Ehrsam", + "Britt Selvitelle", + "Jonathan Wilson", + "Ryan Atallah", + "Joseph John Cox", + "Luc Ritchie", + "Matt Roveto", + "Jamie Warner", + "Marek Cirkos", + "Magister Mugit", + "Stevie Metke", + "Cooper Jones", + "James Hughes", + "John V Wertheim", + "Chris Giddings", + "Song Gao", + "Alexander Feldman", + "Matt Langford", + "Max Mitchell", + "Richard Burgmann", + "John Griffith", + "Chris Connett", + "Steven Tomlinson", + "Jameel Syed", + "Bong Choung", + "Ignacio Freiberg", + "Zhilong Yang", + "Giovanni Filippi", + "Eric Younge", + "Prasant Jagannath", + "Cody Brocious", + "James H. Park", + "Norton Wang", + "Kevin Le", + "Tianyu Ge", + "David MacCumber", + "Oliver Steele", + "Yaw Etse", + "Dave B", + "Waleed Hamied", + "George Chiesa", + "supershabam", + "Delton Ding", + "Thomas Tarler", + "Isak Hietala", + "1st ViewMaths", + "Jacob Magnuson", + "Mark Govea", + "Clark Gaebel", + "Mathias Jansson", + "David Clark", + "Michael Gardner", + "Mads Elvheim", + "Awoo", + "Dr . David G. Stork", + "Ted Suzman", + "Linh Tran", + "Andrew Busey", + "John Haley", + "Ankalagon", + "Eric Lavault", + "Boris Veselinovich", + "Julian Pulgarin", + "Jeff Linse", + "Robert Teed", + "Jason Hise", + "Meshal Alshammari", + "Bernd Sing", + "James Thornton", + "Mustafa Mahdi", + "Mathew Bramson", + "Jerry Ling", + "Sh\\`im\\'in Ku\\=ang", + "Rish Kundalia", + "Achille Brighton", + "Ripta Pasay", + ] + } + + +# class Thumbnail(GraphicalIntuitions): +class Thumbnail(AnalyzeFunctionWithTransformations): + CONFIG = { + "x_axis_width": 12, + "graph_origin": 1.5 * DOWN + 4 * LEFT, + "num_initial_applications": 1, + "input_line_zero_point": 2 * UP, + } + + def construct(self): + self.add_function_title() + self.title.fade(1) + self.titles.fade(1) + self.repeatedly_apply_function() + + full_rect = FullScreenFadeRectangle() + cross = Cross(full_rect) + cross.set_stroke(width=40) + + self.add(cross) diff --git a/old_projects/WindingNumber_G.py b/old_projects/WindingNumber_G.py index 4b65bb31..bf2a2c07 100644 --- a/old_projects/WindingNumber_G.py +++ b/old_projects/WindingNumber_G.py @@ -3274,6 +3274,5 @@ class Thumbnail(SearchSpacePerimeterVsArea): - From e4d44bf2bfdd1343da75041b215c9675b8018a79 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 21 May 2018 12:23:58 -0700 Subject: [PATCH 3/3] Revert "Merge branch 'master' of github.com:3b1b/manim into alt-calc" This reverts commit 17a1ea6db500eddfdec75cc727b70737d892e961, reversing changes made to c8c6e6d9ba77d42c8d50d8187ca2b0427da0079c. --- active_projects/eola2/cramer.py | 2 +- animation/transform.py | 7 + camera/camera.py | 274 ++++++++++++++++-------------- camera/mapping_camera.py | 25 +-- camera/moving_camera.py | 71 ++++++-- camera/multi_camera.py | 59 +++++++ camera/three_d_camera.py | 8 +- constants.py | 11 +- extract_scene.py | 7 +- mobject/mobject.py | 9 + mobject/types/image_mobject.py | 83 ++++++--- old_projects/WindingNumber.py | 2 +- old_projects/WindingNumber_G.py | 2 +- old_projects/basel/basel.py | 2 +- old_projects/nn/part1.py | 6 +- old_projects/pi_day.py | 6 +- old_projects/wallis.py | 2 +- scene/moving_camera_scene.py | 34 ++-- scene/scene.py | 3 +- scene/zoomed_scene.py | 178 +++++++------------ utils/color.py | 3 +- utils/output_directory_getters.py | 2 +- 22 files changed, 468 insertions(+), 328 deletions(-) create mode 100644 camera/multi_camera.py diff --git a/active_projects/eola2/cramer.py b/active_projects/eola2/cramer.py index 69eb2d0a..ff858f45 100644 --- a/active_projects/eola2/cramer.py +++ b/active_projects/eola2/cramer.py @@ -206,7 +206,7 @@ class LeaveItToComputers(TeacherStudentsScene): class PrerequisiteKnowledge(TeacherStudentsScene): CONFIG = { - "camera_config": {"background_alpha": 255} + "camera_config": {"background_opacity": 1} } def construct(self): diff --git a/animation/transform.py b/animation/transform.py index 83e56462..3da8eeff 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -140,6 +140,13 @@ class ApplyPointwiseFunction(ApplyMethod): ) +class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction): + def __init__(self, function, mobject, **kwargs): + ApplyMethod.__init__( + self, mobject.move_to, function(mobject.get_center()), **kwargs + ) + + class FadeToColor(ApplyMethod): def __init__(self, mobject, color, **kwargs): ApplyMethod.__init__(self, mobject.set_color, color, **kwargs) diff --git a/camera/camera.py b/camera/camera.py index e8ccb6bb..f23a76eb 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -1,5 +1,6 @@ import itertools as it import numpy as np +import operator as op import aggdraw import copy @@ -7,9 +8,10 @@ import time from PIL import Image 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 @@ -21,22 +23,26 @@ from utils.iterables import batch_by_property from utils.iterables import list_difference_update from utils.iterables import remove_list_redundancies from utils.simple_functions import fdiv +from utils.space_ops import angle_of_vector class Camera(object): CONFIG = { "background_image": None, - "pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH), - # Note: frame_shape will be resized to match pixel_shape - "frame_shape": (FRAME_HEIGHT, FRAME_WIDTH), - "space_center": ORIGIN, + "pixel_height": DEFAULT_PIXEL_HEIGHT, + "pixel_width": DEFAULT_PIXEL_WIDTH, + # Note: frame height and width will be resized to match + # the pixel aspect ratio + "frame_height": FRAME_HEIGHT, + "frame_width": FRAME_WIDTH, + "frame_center": ORIGIN, "background_color": BLACK, + "background_opacity": 0, # Points in vectorized mobjects with norm greater # than this value will be rescaled. "max_allowable_norm": FRAME_WIDTH, "image_mode": "RGBA", "n_rgb_coords": 4, - "background_alpha": 0, # Out of rgb_max_val "pixel_array_dtype": 'uint8', "use_z_coordinate_for_display_order": False, # z_buff_func is only used if the flag above is set to True. @@ -58,36 +64,72 @@ class Camera(object): self.canvas = None return copy.copy(self) + def reset_pixel_shape(self, new_height, new_width): + self.pixel_width = new_width + self.pixel_height = new_height + self.init_background() + self.resize_frame_shape() + self.reset() + + def get_pixel_height(self): + return self.pixel_height + + def get_pixel_width(self): + return self.pixel_width + + def get_frame_height(self): + return self.frame_height + + def get_frame_width(self): + return self.frame_width + + def get_frame_center(self): + return self.frame_center + + def set_frame_height(self, frame_height): + self.frame_height = frame_height + + def set_frame_width(self, frame_width): + self.frame_width = frame_width + + def set_frame_center(self, frame_center): + self.frame_center = frame_center + def resize_frame_shape(self, fixed_dimension=0): """ Changes frame_shape to match the aspect ratio - of pixel_shape, where fixed_dimension determines - whether frame_shape[0] (height) or frame_shape[1] (width) + of the pixels, where fixed_dimension determines + whether frame_height or frame_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 + pixel_height = self.get_pixel_height() + pixel_width = self.get_pixel_width() + frame_height = self.get_frame_height() + frame_width = self.get_frame_width() + 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.set_frame_height(frame_height) + self.set_frame_width(frame_width) def init_background(self): + height = self.get_pixel_height() + width = self.get_pixel_width() if self.background_image is not None: path = get_full_raster_image_path(self.background_image) image = Image.open(path).convert(self.image_mode) - height, width = self.pixel_shape # TODO, how to gracefully handle backgrounds # with different sizes? self.background = np.array(image)[:height, :width] self.background = self.background.astype(self.pixel_array_dtype) else: background_rgba = color_to_int_rgba( - self.background_color, alpha=self.background_alpha + self.background_color, self.background_opacity ) self.background = np.zeros( - list(self.pixel_shape) + [self.n_rgb_coords], + (height, width, self.n_rgb_coords), dtype=self.pixel_array_dtype ) self.background[:, :] = background_rgba @@ -105,10 +147,10 @@ class Camera(object): retval = np.array(pixel_array) if convert_from_floats: retval = np.apply_along_axis( - lambda f: ( - f * self.rgb_max_val).astype(self.pixel_array_dtype), + lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype), 2, - retval) + retval + ) return retval def set_pixel_array(self, pixel_array, convert_from_floats=False): @@ -148,6 +190,7 @@ class Camera(object): def reset(self): self.set_pixel_array(self.background) + return self #### @@ -201,7 +244,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 ] @@ -301,8 +344,7 @@ class Camera(object): # points = self.adjust_out_of_range_points(points) if len(points) == 0: continue - aligned_points = self.align_points_to_camera(points) - coords = self.points_to_pixel_coords(aligned_points) + coords = self.points_to_pixel_coords(points) coord_strings = coords.flatten().astype(str) # Start new path string with M coord_strings[0] = "M" + coord_strings[0] @@ -342,7 +384,6 @@ class Camera(object): def display_point_cloud(self, points, rgbas, thickness): if len(points) == 0: return - points = self.align_points_to_camera(points) pixel_coords = self.points_to_pixel_coords(points) pixel_coords = self.thickened_coordinates( pixel_coords, thickness @@ -358,7 +399,8 @@ class Camera(object): pixel_coords = pixel_coords[on_screen_indices] rgbas = rgbas[on_screen_indices] - ph, pw = self.pixel_shape + ph = self.get_pixel_height() + pw = self.get_pixel_width() flattener = np.array([1, pw], dtype='int') flattener = flattener.reshape((2, 1)) @@ -378,94 +420,56 @@ class Camera(object): ul_coords, ur_coords, dl_coords = corner_coords right_vect = ur_coords - ul_coords down_vect = dl_coords - ul_coords + center_coords = ul_coords + (right_vect + down_vect) / 2 - impa = image_mobject.pixel_array - - oh, ow = self.pixel_array.shape[:2] # Outer width and height - ih, iw = impa.shape[:2] # inner with and height - rgb_len = self.pixel_array.shape[2] - - image = np.zeros((oh, ow, rgb_len), dtype=self.pixel_array_dtype) - - if right_vect[1] == 0 and down_vect[0] == 0: - rv0 = right_vect[0] - dv1 = down_vect[1] - x_indices = np.arange(rv0, dtype='int') * iw / rv0 - y_indices = np.arange(dv1, dtype='int') * ih / dv1 - stretched_impa = impa[y_indices][:, x_indices] - - x0, x1 = ul_coords[0], ur_coords[0] - y0, y1 = ul_coords[1], dl_coords[1] - if x0 >= ow or x1 < 0 or y0 >= oh or y1 < 0: - return - siy0 = max(-y0, 0) # stretched_impa y0 - siy1 = dv1 - max(y1 - oh, 0) - six0 = max(-x0, 0) - six1 = rv0 - max(x1 - ow, 0) - x0 = max(x0, 0) - y0 = max(y0, 0) - image[y0:y1, x0:x1] = stretched_impa[siy0:siy1, six0:six1] - else: - # Alternate (slower) tactic if image is tilted - # List of all coordinates of pixels, given as (x, y), - # which matches the return type of points_to_pixel_coords, - # even though np.array indexing naturally happens as (y, x) - all_pixel_coords = np.zeros((oh * ow, 2), dtype='int') - a = np.arange(oh * ow, dtype='int') - all_pixel_coords[:, 0] = a % ow - all_pixel_coords[:, 1] = a / ow - - recentered_coords = all_pixel_coords - ul_coords - - with np.errstate(divide='ignore'): - ix_coords, iy_coords = [ - np.divide( - dim * np.dot(recentered_coords, vect), - np.dot(vect, vect), - ) - for vect, dim in (right_vect, iw), (down_vect, ih) - ] - to_change = reduce(op.and_, [ - ix_coords >= 0, ix_coords < iw, - iy_coords >= 0, iy_coords < ih, - ]) - inner_flat_coords = iw * \ - iy_coords[to_change] + ix_coords[to_change] - flat_impa = impa.reshape((iw * ih, rgb_len)) - target_rgbas = flat_impa[inner_flat_coords, :] - - image = image.reshape((ow * oh, rgb_len)) - image[to_change] = target_rgbas - image = image.reshape((oh, ow, rgb_len)) - self.overlay_rgba_array(image) - - def overlay_rgba_array(self, arr): - fg = arr - bg = self.pixel_array - # rgba_max_val = self.rgb_max_val - src_rgb, src_a, dst_rgb, dst_a = [ - a.astype(np.float32) / self.rgb_max_val - for a in fg[..., :3], fg[..., 3], bg[..., :3], bg[..., 3] - ] - - out_a = src_a + dst_a * (1.0 - src_a) - - # When the output alpha is 0 for full transparency, - # we have a choice over what RGB value to use in our - # output representation. We choose 0 here. - out_rgb = fdiv( - src_rgb * src_a[..., None] + - dst_rgb * dst_a[..., None] * (1.0 - src_a[..., None]), - out_a[..., None], - zero_over_zero_value=0 + sub_image = Image.fromarray( + image_mobject.get_pixel_array(), + mode="RGBA" ) - self.pixel_array[..., :3] = out_rgb * self.rgb_max_val - self.pixel_array[..., 3] = out_a * self.rgb_max_val + # Reshape + pixel_width = int(pdist([ul_coords, ur_coords])) + pixel_height = int(pdist([ul_coords, dl_coords])) + sub_image = sub_image.resize( + (pixel_width, pixel_height), resample=Image.BICUBIC + ) - def align_points_to_camera(self, points): - # This is where projection should live - return points - self.space_center + # Rotate + angle = angle_of_vector(right_vect) + adjusted_angle = -int(360 * angle / TAU) + if adjusted_angle != 0: + sub_image = sub_image.rotate( + adjusted_angle, resample=Image.BICUBIC, expand=1 + ) + + # TODO, there is no accounting for a shear... + + # Paste into an image as large as the camear's pixel array + full_image = Image.fromarray( + np.zeros((self.get_pixel_height(), self.get_pixel_width())), + mode="RGBA" + ) + new_ul_coords = center_coords - np.array(sub_image.size) / 2 + full_image.paste( + sub_image, + box=( + new_ul_coords[0], + new_ul_coords[1], + new_ul_coords[0] + sub_image.size[0], + new_ul_coords[1] + sub_image.size[1], + ) + ) + + # Paint on top of existing pixel array + self.overlay_PIL_image(full_image) + + def overlay_rgba_array(self, arr): + self.overlay_PIL_image(Image.fromarray(arr, mode="RGBA")) + + def overlay_PIL_image(self, image): + self.pixel_array = np.array( + Image.alpha_composite(self.get_image(), image) + ) def adjust_out_of_range_points(self, points): if not np.any(points > self.max_allowable_norm): @@ -483,31 +487,43 @@ class Camera(object): return points def points_to_pixel_coords(self, points): + shifted_points = points - self.get_frame_center() + result = np.zeros((len(points), 2)) - ph, pw = self.pixel_shape - sh, sw = self.frame_shape - width_mult = pw / sw - width_add = pw / 2 - height_mult = ph / sh - height_add = ph / 2 + pixel_height = self.get_pixel_height() + pixel_width = self.get_pixel_width() + frame_height = self.get_frame_height() + frame_width = self.get_frame_width() + width_mult = pixel_width / frame_width + width_add = pixel_width / 2 + height_mult = pixel_height / frame_height + height_add = pixel_height / 2 # Flip on y-axis as you go height_mult *= -1 - result[:, 0] = points[:, 0] * width_mult + width_add - result[:, 1] = points[:, 1] * height_mult + height_add + result[:, 0] = shifted_points[:, 0] * width_mult + width_add + result[:, 1] = shifted_points[:, 1] * height_mult + height_add return result.astype('int') def on_screen_pixels(self, pixel_coords): return reduce(op.and_, [ pixel_coords[:, 0] >= 0, - pixel_coords[:, 0] < self.pixel_shape[1], + pixel_coords[:, 0] < self.get_pixel_width(), pixel_coords[:, 1] >= 0, - pixel_coords[:, 1] < self.pixel_shape[0], + pixel_coords[:, 1] < self.get_pixel_height(), ]) def adjusted_thickness(self, thickness): - big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"] - factor = sum(big_shape) / sum(self.pixel_shape) + # TODO: This seems...unsystematic + big_sum = op.add( + PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_height"], + PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"], + ) + this_sum = op.add( + self.get_pixel_height(), + self.get_pixel_width(), + ) + factor = fdiv(big_sum, this_sum) return 1 + (thickness - 1) / factor def get_thickening_nudges(self, thickness): @@ -525,13 +541,20 @@ class Camera(object): def get_coords_of_all_pixels(self): # These are in x, y order, to help me keep things straight - full_space_dims = np.array(self.frame_shape)[::-1] - full_pixel_dims = np.array(self.pixel_shape)[::-1] + full_space_dims = np.array([ + self.get_frame_width(), + self.get_frame_height() + ]) + full_pixel_dims = np.array([ + self.get_pixel_width(), + self.get_pixel_height() + ]) # These are addressed in the same y, x order as in pixel_array, but the values in them # are listed in x, y order - uncentered_pixel_coords = np.indices(self.pixel_shape)[ - ::-1].transpose(1, 2, 0) + uncentered_pixel_coords = np.indices( + [self.get_pixel_height(), self.get_pixel_width()] + )[::-1].transpose(1, 2, 0) uncentered_space_coords = fdiv( uncentered_pixel_coords * full_space_dims, full_pixel_dims) @@ -541,7 +564,8 @@ class Camera(object): # overflow is unlikely to be a problem) centered_space_coords = ( - uncentered_space_coords - fdiv(full_space_dims, 2)) + uncentered_space_coords - fdiv(full_space_dims, 2) + ) # Have to also flip the y coordinates to account for pixel array being listed in # top-to-bottom order, opposite of screen coordinate convention diff --git a/camera/mapping_camera.py b/camera/mapping_camera.py index b55d4bc0..d85f51ca 100644 --- a/camera/mapping_camera.py +++ b/camera/mapping_camera.py @@ -43,8 +43,9 @@ class MappingCamera(Camera): # TODO: Add optional separator borders between cameras (or perhaps peel this off into a # CameraPlusOverlay class) +# TODO, the classes below should likely be deleted -class MultiCamera(Camera): +class OldMultiCamera(Camera): def __init__(self, *cameras_with_start_positions, **kwargs): self.shifted_cameras = [ DictAsObject( @@ -52,8 +53,8 @@ class MultiCamera(Camera): "camera": camera_with_start_positions[0], "start_x": camera_with_start_positions[1][1], "start_y": camera_with_start_positions[1][0], - "end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].pixel_shape[1], - "end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].pixel_shape[0], + "end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].get_pixel_width(), + "end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].get_pixel_height(), }) for camera_with_start_positions in cameras_with_start_positions ] @@ -92,23 +93,23 @@ 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 self.right_camera = right_camera - half_width = self.pixel_shape[1] / 2 + half_width = self.get_pixel_width() / 2 for camera in [self.left_camera, self.right_camera]: # TODO: Round up on one if width is odd - camera.pixel_shape = (self.pixel_shape[0], half_width) - camera.init_background() - camera.resize_frame_shape() - camera.reset() + camera.reset_pixel_shape(camera.get_pixel_height(), half_width) - 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..16585f54 100644 --- a/camera/moving_camera.py +++ b/camera/moving_camera.py @@ -1,40 +1,77 @@ 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): """ - Stays in line with the height, width and position - of a given mobject + Stays in line with the height, width and position of it's 'frame', which is a Rectangle """ + 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): """ - frame is a Mobject, (should be a rectangle) determining - which region of space the camera displys + frame is a Mobject, (should almost certainly 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() - self.realign_frame_shape() - Camera.capture_mobjects(self, *args, **kwargs) + # TODO, make these work for a rotated frame + def get_frame_height(self): + return self.frame.get_height() - def realign_frame_shape(self): - height, width = self.frame_shape - if self.aligned_dimension == "height": - self.frame_shape = (self.frame.get_height(), width) - else: - self.frame_shape = (height, self.frame.get_width()) - self.resize_frame_shape(0 if self.aligned_dimension == "height" else 1) + def get_frame_width(self): + return self.frame.get_width() + + def get_frame_center(self): + return self.frame.get_center() + + def set_frame_height(self, frame_height): + self.frame.stretch_to_fit_height(frame_height) + + def set_frame_width(self, frame_width): + self.frame.stretch_to_fit_width(frame_width) + + def set_frame_center(self, frame_center): + self.frame.move_to(frame_center) + + def capture_mobjects(self, mobjects, **kwargs): + # self.reset_frame_center() + # self.realign_frame_shape() + Camera.capture_mobjects(self, mobjects, **kwargs) + + # def reset_frame_center(self): + # self.frame_center = self.frame.get_center() + + # def realign_frame_shape(self): + # height, width = self.frame_shape + # if self.fixed_dimension == 0: + # self.frame_shape = (height, self.frame.get_width()) + # else: + # self.frame_shape = (self.frame.get_height(), width) + # self.resize_frame_shape(fixed_dimension=self.fixed_dimension) + + def get_mobjects_indicating_movement(self): + """ + Returns all mobjets whose movement implies that the camera + should think of all other mobjects on the screen as moving + """ + return [self.frame] diff --git a/camera/multi_camera.py b/camera/multi_camera.py new file mode 100644 index 00000000..42b428cd --- /dev/null +++ b/camera/multi_camera.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import + +from camera.moving_camera import MovingCamera +from utils.iterables import list_difference_update + + +class MultiCamera(MovingCamera): + CONFIG = { + "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 + assert(isinstance(imfc.camera, MovingCamera)) + self.image_mobjects_from_cameras.append(imfc) + + def update_sub_cameras(self): + """ Reshape sub_camera pixel_arrays """ + for imfc in self.image_mobjects_from_cameras: + 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() / self.get_frame_height()), + int(pixel_width * imfc.get_width() / self.get_frame_width()), + ) + + def reset(self): + for imfc in self.image_mobjects_from_cameras: + imfc.camera.reset() + MovingCamera.reset(self) + return self + + def capture_mobjects(self, mobjects, **kwargs): + 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) + + def get_mobjects_indicating_movement(self): + return [self.frame] + [ + imfc.camera.frame + for imfc in self.image_mobjects_from_cameras + ] diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index 84ddece8..2bcb750a 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -12,6 +12,8 @@ from utils.bezier import interpolate from utils.space_ops import rotation_about_z from utils.space_ops import rotation_matrix +# TODO: Make sure this plays well with latest camera updates + class CameraWithPerspective(Camera): CONFIG = { @@ -49,7 +51,7 @@ class ThreeDCamera(CameraWithPerspective): self.rotation_mobject = VectorizedPoint() # moving_center lives in the x-y-z space # It representes the center of rotation - self.moving_center = VectorizedPoint(self.space_center) + self.moving_center = VectorizedPoint(self.frame_center) self.set_position(self.phi, self.theta, self.distance) def modified_rgb(self, vmobject, rgb): @@ -163,7 +165,7 @@ class ThreeDCamera(CameraWithPerspective): center_of_rotation = self.get_center_of_rotation( center_x, center_y, center_z) self.moving_center.move_to(center_of_rotation) - self.space_center = self.moving_center.points[0] + self.frame_center = self.moving_center.points[0] def get_view_transformation_matrix(self): return (self.default_distance / self.get_distance()) * np.dot( @@ -174,6 +176,6 @@ class ThreeDCamera(CameraWithPerspective): def points_to_pixel_coords(self, points): matrix = self.get_view_transformation_matrix() new_points = np.dot(points, matrix.T) - self.space_center = self.moving_center.points[0] + self.frame_center = self.moving_center.points[0] return Camera.points_to_pixel_coords(self, new_points) diff --git a/constants.py b/constants.py index a87edffa..888b0c31 100644 --- a/constants.py +++ b/constants.py @@ -17,17 +17,20 @@ LOW_QUALITY_FRAME_DURATION = 1. / 15 MEDIUM_QUALITY_FRAME_DURATION = 1. / 30 PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60 -# There might be other configuration than pixel_shape later... +# There might be other configuration than pixel shape later... PRODUCTION_QUALITY_CAMERA_CONFIG = { - "pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH), + "pixel_height": DEFAULT_PIXEL_HEIGHT, + "pixel_width": DEFAULT_PIXEL_WIDTH, } MEDIUM_QUALITY_CAMERA_CONFIG = { - "pixel_shape": (720, 1280), + "pixel_height": 720, + "pixel_width": 1280, } LOW_QUALITY_CAMERA_CONFIG = { - "pixel_shape": (480, 854), + "pixel_height": 480, + "pixel_width": 854, } DEFAULT_POINT_DENSITY_2D = 25 diff --git a/extract_scene.py b/extract_scene.py index f7c91f94..1cddadc1 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -224,11 +224,8 @@ def get_module_posix(file_name): module_name = file_name.replace(".py", "") last_module = imp.load_module(".", *imp.find_module(".")) for part in module_name.split(os.sep): - try: - load_args = imp.find_module(part, last_module.__path__) - last_module = imp.load_module(part, *load_args) - except ImportError: - continue + load_args = imp.find_module(part, last_module.__path__) + last_module = imp.load_module(part, *load_args) return last_module diff --git a/mobject/mobject.py b/mobject/mobject.py index 8283c345..c6364fa3 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -199,6 +199,15 @@ class Mobject(Container): ) return self + def apply_function_to_position(self, function): + self.move_to(function(self.get_center())) + return self + + def apply_function_to_submobject_positions(self, function): + for submob in self.submobjects: + submob.apply_function_to_position(function) + return self + def apply_matrix(self, matrix, **kwargs): # Default to applying matrix about the origin, not mobjects center if len(kwargs) == 0: diff --git a/mobject/types/image_mobject.py b/mobject/types/image_mobject.py index ca8c03bd..9d6878b4 100644 --- a/mobject/types/image_mobject.py +++ b/mobject/types/image_mobject.py @@ -7,25 +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, but no obgligation + pass + + def init_points(self): + # Corresponding corners of image are fixed to these 3 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): + CONFIG = { + "invert": False, + "image_mode": "RGBA", + } + def __init__(self, filename_or_array, **kwargs): digest_config(self, kwargs) if isinstance(filename_or_array, str): @@ -37,7 +63,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 +79,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 +92,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 +106,30 @@ class ImageMobject(Mobject): mobject1.pixel_array, mobject2.pixel_array, alpha ).astype(self.pixel_array_dtype) - def copy(self): - return self.deepcopy() +# TODO, add the ability to have the dimensions/orientation of this +# mobject more strongly tied to the frame of the camera it contains, +# in the case where that's a MovingCamera + + +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/old_projects/WindingNumber.py b/old_projects/WindingNumber.py index c12c5cbd..b94a5ef6 100644 --- a/old_projects/WindingNumber.py +++ b/old_projects/WindingNumber.py @@ -668,7 +668,7 @@ class ColorMappedByFuncScene(Scene): # We hash just based on output image # Thus, multiple scenes with same output image can re-use it # without recomputation - full_hash = hash((func_hash, self.camera.pixel_shape)) + full_hash = hash((func_hash, self.camera.get_pixel_width())) self.background_image_file = self.short_path_to_long_path( "color_mapped_bg_hash_" + str(full_hash) + ".png" ) diff --git a/old_projects/WindingNumber_G.py b/old_projects/WindingNumber_G.py index bf2a2c07..7a5ac489 100644 --- a/old_projects/WindingNumber_G.py +++ b/old_projects/WindingNumber_G.py @@ -2884,7 +2884,7 @@ class ZeroFoundOnBoundary(Scene): class AllOfTheVideos(Scene): CONFIG = { "camera_config" : { - "background_alpha" : 255, + "background_opacity" : 1, } } def construct(self): diff --git a/old_projects/basel/basel.py b/old_projects/basel/basel.py index 69809107..c3301a43 100644 --- a/old_projects/basel/basel.py +++ b/old_projects/basel/basel.py @@ -2056,7 +2056,7 @@ class IPTScene1(PiCreatureScene): # use the following for the zoomed inset if show_detail: self.camera.frame_shape = (0.02 * FRAME_HEIGHT, 0.02 * FRAME_WIDTH) - self.camera.space_center = C + self.camera.frame_center = C SCREEN_SCALE = 0.01 SCREEN_THICKNESS = 0.02 diff --git a/old_projects/nn/part1.py b/old_projects/nn/part1.py index cdd3618d..177b9622 100644 --- a/old_projects/nn/part1.py +++ b/old_projects/nn/part1.py @@ -1479,7 +1479,7 @@ class AskAboutLayers(PreviewMNistNetwork): class BreakUpMacroPatterns(IntroduceEachLayer): CONFIG = { - "camera_config" : {"background_alpha" : 255}, + "camera_config" : {"background_opacity" : 1}, "prefixes" : [ "nine", "eight", "four", "upper_loop", "right_line", @@ -1868,7 +1868,7 @@ class BreakUpMicroPatterns(BreakUpMacroPatterns): class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer): CONFIG = { "camera_config" : { - "background_alpha" : 255, + "background_opacity" : 1, }, "network_mob_config" : { "layer_to_layer_buff" : 2, @@ -2081,7 +2081,7 @@ class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer): class EdgeDetection(Scene): CONFIG = { - "camera_config" : {"background_alpha" : 255} + "camera_config" : {"background_opacity" : 1} } def construct(self): lion = ImageMobject("Lion") diff --git a/old_projects/pi_day.py b/old_projects/pi_day.py index bea9b697..c5c3a3bd 100644 --- a/old_projects/pi_day.py +++ b/old_projects/pi_day.py @@ -468,7 +468,7 @@ class TauFalls(Scene): class EulerWrites628(Scene): CONFIG = { "camera_config" : { - "background_alpha" : 255, + "background_opacity" : 1, } } def construct(self): @@ -548,7 +548,7 @@ class EulerWrites628(Scene): class HeroAndVillain(Scene): CONFIG = { "camera_config" : { - "background_alpha" : 255, + "background_opacity" : 1, } } def construct(self): @@ -987,7 +987,7 @@ class SpecialThanks(Scene): class EndScene(PatreonEndScreen): CONFIG = { "camera_config" : { - "background_alpha" : 255, + "background_opacity" : 1, } } def construct(self): diff --git a/old_projects/wallis.py b/old_projects/wallis.py index 707d7d94..b071e81f 100644 --- a/old_projects/wallis.py +++ b/old_projects/wallis.py @@ -4958,7 +4958,7 @@ class KeeperAndSailorForSineProduct(KeeperAndSailor): class Conclusion(TeacherStudentsScene): CONFIG = { - "camera_config": {"background_alpha": 255}, + "camera_config": {"background_opacity": 1}, } def construct(self): diff --git a/scene/moving_camera_scene.py b/scene/moving_camera_scene.py index f3f5ed74..29e1430f 100644 --- a/scene/moving_camera_scene.py +++ b/scene/moving_camera_scene.py @@ -1,26 +1,32 @@ from __future__ import absolute_import -from constants import * - from scene.scene import Scene from camera.moving_camera import MovingCamera -from mobject.frame import ScreenRectangle +from utils.iterables import list_update class MovingCameraScene(Scene): + CONFIG = { + "camera_class": MovingCamera + } + def setup(self): - self.camera_frame = ScreenRectangle(height=FRAME_HEIGHT) - self.camera_frame.set_stroke(width=0) - self.camera = MovingCamera( - self.camera_frame, **self.camera_config - ) + Scene.setup(self) + assert(isinstance(self.camera, MovingCamera)) + self.camera_frame = self.camera.frame + # Hmm, this currently relies on the fact that MovingCamera + # willd default to a full-sized frame. Is that okay? return self def get_moving_mobjects(self, *animations): - # TODO: Code repetition from ZoomedScene moving_mobjects = Scene.get_moving_mobjects(self, *animations) - if self.camera_frame in moving_mobjects: - # When the camera is moving, so is everything, - return self.mobjects - else: - return moving_mobjects + all_moving_mobjects = self.camera.extract_mobject_family_members( + moving_mobjects + ) + movement_indicators = self.camera.get_mobjects_indicating_movement() + for movement_indicator in movement_indicators: + if movement_indicator in all_moving_mobjects: + # When one of these is moving, the camera should + # consider all mobjects to be moving + return list_update(self.mobjects, moving_mobjects) + return moving_mobjects diff --git a/scene/scene.py b/scene/scene.py index f136982f..364e6348 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -572,7 +572,8 @@ class Scene(Container): self.args_to_rename_file = (temp_file_path, file_path) fps = int(1 / self.frame_duration) - height, width = self.camera.pixel_shape + height = self.camera.get_pixel_height() + width = self.camera.get_pixel_width() command = [ FFMPEG_BIN, diff --git a/scene/zoomed_scene.py b/scene/zoomed_scene.py index 71c623b3..2d3d4ac0 100644 --- a/scene/zoomed_scene.py +++ b/scene/zoomed_scene.py @@ -1,20 +1,19 @@ from __future__ import absolute_import -import numpy as np - -from scene.scene import Scene -from animation.creation import FadeIn +from scene.moving_camera_scene import MovingCameraScene from camera.moving_camera import MovingCamera -from mobject.geometry import Rectangle +from camera.multi_camera import MultiCamera +from mobject.types.image_mobject import ImageMobjectFromCamera +from utils.simple_functions import fdiv from constants import * +from animation.transform import ApplyMethod + +# Note, any scenes from old videos using ZoomedScene will almost certainly +# break, as it was restructured. -class ZoomedScene(Scene): - """ - Move around self.little_rectangle to determine - which part of the screen is zoomed in on. - """ +class ZoomedScene(MovingCameraScene): CONFIG = { "camera_class": MultiCamera, "zoomed_display_height": 3, @@ -33,109 +32,62 @@ class ZoomedScene(Scene): "zoom_activated": False, } - def activate_zooming(self): - self.generate_big_rectangle() - self.setup_zoomed_canvas() - self.setup_zoomed_camera() - self.zoom_activated = True - - def animate_activate_zooming(self): - self.activate_zooming() - self.play(*map(FadeIn, [ - self.little_rectangle, self.big_rectangle - ])) - - def disactivate_zooming(self): - self.remove(self.big_rectangle, self.little_rectangle) - self.zoom_activated = False - - def get_zoomed_camera_mobject(self): - return self.little_rectangle - - def get_zoomed_screen(self): - return self.big_rectangle - - def generate_big_rectangle(self): - height, width = self.zoomed_canvas_frame_shape - self.big_rectangle = Rectangle( - height=height, - width=width, - color=self.square_color + def setup(self): + MovingCameraScene.setup(self) + # Initialize camera and display + zoomed_camera = MovingCamera(**self.zoomed_camera_config) + zoomed_display = ImageMobjectFromCamera( + zoomed_camera, **self.zoomed_camera_image_mobject_config ) - if self.zoomed_canvas_center is not None: - self.big_rectangle.shift(self.zoomed_canvas_center) - elif self.zoomed_canvas_corner is not None: - self.big_rectangle.to_corner( - self.zoomed_canvas_corner, - buff=self.zoomed_canvas_corner_buff - ) - self.add(self.big_rectangle) + 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) - def setup_zoomed_canvas(self): - upper_left = self.big_rectangle.get_corner(UP + LEFT) - lower_right = self.big_rectangle.get_corner(DOWN + RIGHT) - pixel_coords = self.camera.points_to_pixel_coords( - np.array([upper_left, lower_right]) - ) - self.zoomed_canvas_pixel_indices = pixel_coords - (up, left), (down, right) = pixel_coords - self.zoomed_canvas_pixel_shape = ( - right - left, - down - up, - ) - - def setup_zoomed_camera(self): - self.little_rectangle = self.big_rectangle.copy() - self.little_rectangle.scale(1. / self.zoom_factor) - self.little_rectangle.move_to( - self.little_rectangle_start_position - ) - self.zoomed_camera = MovingCamera( - self.little_rectangle, - pixel_shape=self.zoomed_canvas_pixel_shape, - background=self.zoomed_camera_background - ) - self.add(self.little_rectangle) - # TODO, is there a better way to hanld this? - self.zoomed_camera.adjusted_thickness = lambda x: x - - def get_frame(self): - frame = Scene.get_frame(self) - if self.zoom_activated: - (up, left), (down, right) = self.zoomed_canvas_pixel_indices - frame[left:right, up:down, :] = self.zoomed_camera.get_image() - return frame - - def set_camera_pixel_array(self, pixel_array): - self.camera.set_pixel_array(pixel_array) - if self.zoom_activated: - (up, left), (down, right) = self.zoomed_canvas_pixel_indices - self.zoomed_camera.set_pixel_array( - pixel_array[left:right, up:down]) - - def set_camera_background(self, background): - self.set_camera_pixel_array(self, background) - # TODO, check this... - - def reset_camera(self): - self.camera.reset() - if self.zoom_activated: - self.zoomed_camera.reset() - - def capture_mobjects_in_camera(self, mobjects, **kwargs): - self.camera.capture_mobjects(mobjects, **kwargs) - if self.zoom_activated: - if self.big_rectangle in mobjects: - mobjects = list(mobjects) - mobjects.remove(self.big_rectangle) - self.zoomed_camera.capture_mobjects( - mobjects, **kwargs - ) - - def get_moving_mobjects(self, *animations): - moving_mobjects = Scene.get_moving_mobjects(self, *animations) - if self.zoom_activated and self.little_rectangle in moving_mobjects: - # When the camera is moving, so is everything, - return self.mobjects + # 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: - return moving_mobjects + 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): + self.zoom_activated = True + self.camera.add_image_mobject_from_camera(self.zoomed_display) + if animate: + self.play(self.get_zoom_in_animation()) + self.play(self.get_zoomed_display_pop_out_animation()) + self.add_foreground_mobjects( + self.zoomed_camera.frame, + self.zoomed_display, + ) + + def get_zoom_in_animation(self, run_time=2, **kwargs): + frame = self.zoomed_camera.frame + full_frame_height = self.camera.get_frame_height() + full_frame_width = self.camera.get_frame_width() + frame.save_state() + frame.stretch_to_fit_width(full_frame_width) + frame.stretch_to_fit_height(full_frame_height) + frame.center() + frame.set_stroke(width=0) + return ApplyMethod(frame.restore, run_time=run_time, **kwargs) + + def get_zoomed_display_pop_out_animation(self, **kwargs): + display = self.zoomed_display + display.save_state(use_deepcopy=True) + display.replace(self.zoomed_camera.frame, stretch=True) + return ApplyMethod(display.restore) + + def get_zoom_factor(self): + return fdiv( + self.zoomed_camera.frame.get_height(), + self.zoomed_display.get_height() + ) diff --git a/utils/color.py b/utils/color.py index a3df972e..f8999670 100644 --- a/utils/color.py +++ b/utils/color.py @@ -39,7 +39,8 @@ def color_to_int_rgb(color): return (255 * color_to_rgb(color)).astype('uint8') -def color_to_int_rgba(color, alpha=255): +def color_to_int_rgba(color, opacity=1.0): + alpha = int(255 * opacity) return np.append(color_to_int_rgb(color), alpha) diff --git a/utils/output_directory_getters.py b/utils/output_directory_getters.py index 8011392e..14d317fc 100644 --- a/utils/output_directory_getters.py +++ b/utils/output_directory_getters.py @@ -34,7 +34,7 @@ def get_scene_output_directory(scene_class): def get_movie_output_directory(scene_class, camera_config, frame_duration): directory = get_scene_output_directory(scene_class) sub_dir = "%dp%d" % ( - camera_config["pixel_shape"][0], + camera_config["pixel_height"], int(1.0 / frame_duration) ) return guarantee_existance(os.path.join(directory, sub_dir))