diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 15604dbb..c0bc7393 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -162,7 +162,7 @@ class DelayByOrder(Animation): self.num_mobject_points = animation.mobject.get_num_points() kwargs.update(dict([ (attr, getattr(animation, attr)) - for attr in Animation.DEFAULT_CONFIG + for attr in Animation.CONFIG ])) Animation.__init__(self, animation.mobject, **kwargs) self.name = str(self) + str(self.animation) diff --git a/camera.py b/camera.py index a6e0a59e..18f3e775 100644 --- a/camera.py +++ b/camera.py @@ -65,12 +65,10 @@ class Camera(object): def capture_mobjects(self, mobjects, include_sub_mobjects = True): if include_sub_mobjects: - all_families = [ + mobjects = it.chain(*[ mob.submobject_family() for mob in mobjects - ] - mobjects = reduce(op.add, all_families, []) - + ]) for mobject in mobjects: if mobject.display_mode == "region": self.display_region(mobject) @@ -153,9 +151,10 @@ class Camera(object): ]) def adjusted_thickness(self, thickness): - big_shape = PRODUCTION_QUALITY_DISPLAY_CONFIG["pixel_shape"] - factor = sum(big_shape)/sum(self.pixel_shape) - return 1 + (thickness-1)/factor + # big_shape = PRODUCTION_QUALITY_DISPLAY_CONFIG["pixel_shape"] + # factor = sum(big_shape)/sum(self.pixel_shape) + # return 1 + (thickness-1)/factor + return thickness def get_thickening_nudges(self, thickness): _range = range(-thickness/2+1, thickness/2+1) diff --git a/constants.py b/constants.py index 8561a22a..4b1ba6ff 100644 --- a/constants.py +++ b/constants.py @@ -15,14 +15,14 @@ MEDIUM_QUALITY_DISPLAY_CONFIG = { } LOW_QUALITY_DISPLAY_CONFIG = { - "pixel_shape" : (480, 640), + "pixel_shape" : (576, 1024), } DEFAULT_POINT_DENSITY_2D = 25 DEFAULT_POINT_DENSITY_1D = 200 -DEFAULT_POINT_THICKNESS = 6 +DEFAULT_POINT_THICKNESS = 3 #TODO, Make sure these are not needed SPACE_HEIGHT = 4.0 diff --git a/displayer.py b/displayer.py deleted file mode 100644 index 5a45875b..00000000 --- a/displayer.py +++ /dev/null @@ -1,185 +0,0 @@ -import numpy as np -import itertools as it -import os -import sys -from PIL import Image -import cv2 -from colour import Color -import progressbar - -from helpers import * - -class Camera(object): - CONFIG = { - "pixel_width" : DEFAULT_WIDTH, - "pixel_height" : DEFAULT_HEIGHT, - "space_height" : SPACE_HEIGHT, - "space_center" : ORIGIN, - "background_color" : BLACK, - } - - def __init__(self, background = None, **kwargs): - digest_config(self, kwargs, locals()) - self.init_background() - self.reset() - - width_to_height = float(self.pixel_width) / self.pixel_height - self.space_width = self.space_height * width_to_height - - def init_background(self): - if self.background: - shape = self.background.shape[:2] - self.pixel_height, self.pixel_width = shape - else: - background_color = Color(self.background_color) - background_rgb = np.array(background_color.get_rgb()) - ones = np.ones( - (self.pixel_height, self.pixel_width, 1), - dtype = 'uint8' - ) - self.background = np.dot( - ones, background_rgb.reshape((1, 3)) - ) - - def get_image(self): - return self.pixel_array - - def reset(self): - self.pixel_array = np.array(self.background) - - # def get_pixels(image_array): #TODO, FIX WIDTH/HEIGHT PROBLEM HERE - # if image_array is None: - # return np.zeros( - # (DEFAULT_HEIGHT, DEFAULT_WIDTH, 3), - # dtype = 'uint8' - # ) - # else: - # pixels = np.array(image_array).astype('uint8') - # assert len(pixels.shape) == 3 and pixels.shape[2] == 3 - # return pixels - - # def paint_region(region, image_array = None, color = None): - # pixels = get_pixels(image_array) - # assert region.shape == pixels.shape[:2] - # if color is None: - # #Random dark color - # rgb = 0.5 * np.random.random(3) - # else: - # rgb = np.array(Color(color).get_rgb()) - # pixels[region.bool_grid] = (255*rgb).astype('uint8') - # return pixels - - def capture_mobject(self, mobject): - return self.capture_mobjects([mobject]) - - def capture_mobjects(self, mobjects, include_sub_mobjects = True): - # pixels = get_pixels(image_array) - # height = pixels.shape[0] - # width = pixels.shape[1] - # space_height = SPACE_HEIGHT - # space_width = SPACE_HEIGHT * width / height - # pixels = pixels.reshape((pixels.size/3, 3)).astype('uint8') - - if include_sub_mobjects: - all_families = [ - mob.submobject_family() - for mob in mobjects - ] - mobjects = reduce(op.add, all_families, []) - - for mobject in mobjects: - self.display_points( - mobject.points, mobject.rgbs, - mobject.point_thickness - ) - - def display_points(self, points, rgbs, thickness): - points = self.project_onto_screen(points) - pixel_coordinates = self.pixel_coordinates_of_points(points) - rgbs = (255*rgbs).astype('uint8') - on_screen_indices = self.on_screen_pixels(pixel_coordinates) - pixel_coordinates = pixel_coordinates[on_screen_indices] - rgbs = rgbs[on_screen_indices] - - flattener = np.array([1, self.width], dtype = 'int') - flattener = flattener.reshape((2, 1)) - indices = np.dot(pixel_coordinates, flattener)[:,0] - - pw, ph = self.pixel_width, self.pixel_height - self.pixel_array.reshape((pw*ph, 3)) - self.pixel_array[indices] = rgbs.astype('uint8') - self.pixel_array.reshape((ph, pw, 3)) - - def project_onto_screen(points): - ## TODO - points[:,2] = 0 - - def pixel_coordinates_of_points(self, points): - result = np.zeros((len(points), 2)) - width_mult = self.pixel_width/self.space_width/2 - width_add = self.pixel_width/2 - height_mult = self.pixel_height/self.space_height/2 - height_add = self.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 - return result - - def on_screen_pixels(self, pixel_coordinates): - return (pixel_coordinates[:,0] < 0) | \ - (pixel_coordinates[:,0] >= width) | \ - (pixel_coordinates[:,1] < 0) | \ - (pixel_coordinates[:,1] >= height) - - - # def add_thickness(pixel_indices_and_rgbs, thickness, width, height): - # """ - # Imagine dragging each pixel around like a paintbrush in - # a plus-sign-shaped pixel arrangement surrounding it. - - # Pass rgb = None to do nothing to them - # """ - # thickness = adjusted_thickness(thickness, width, height) - # original = np.array(pixel_indices_and_rgbs) - # n_extra_columns = pixel_indices_and_rgbs.shape[1] - 2 - # for nudge in range(-thickness/2+1, thickness/2+1): - # if nudge == 0: - # continue - # for x, y in [[nudge, 0], [0, nudge]]: - # pixel_indices_and_rgbs = np.append( - # pixel_indices_and_rgbs, - # original+([x, y] + [0]*n_extra_columns), - # axis = 0 - # ) - # admissibles = (pixel_indices_and_rgbs[:,0] >= 0) & \ - # (pixel_indices_and_rgbs[:,0] < width) & \ - # (pixel_indices_and_rgbs[:,1] >= 0) & \ - # (pixel_indices_and_rgbs[:,1] < height) - # return pixel_indices_and_rgbs[admissibles] - - def adjusted_thickness(thickness, width, height): - big_width = PRODUCTION_QUALITY_DISPLAY_CONFIG["width"] - big_height = PRODUCTION_QUALITY_DISPLAY_CONFIG["height"] - factor = (big_width + big_height) / (width + height) - return 1 + (thickness-1)/facto - - - - - - - - - - - - - - - - - - - diff --git a/extract_scene.py b/extract_scene.py index 8e5541f1..c1cd580e 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -10,6 +10,7 @@ import imp from helpers import * from scene import Scene +from camera import Camera HELP_MESSAGE = """ Usage: @@ -194,7 +195,9 @@ def main(): inspect.getmembers(module, is_scene) ) config["movie_prefix"] = config["file"].replace(".py", "") - scene_kwargs = config["display_config"] + scene_kwargs = { + "camera" : Camera(**config["display_config"]) + } for SceneClass in get_scene_classes(scene_names_to_classes, config): for args in get_scene_args(SceneClass, config): scene_kwargs["construct_args"] = tuplify(args) diff --git a/generate_logo.py b/generate_logo.py index f897f6a5..d9414da0 100644 --- a/generate_logo.py +++ b/generate_logo.py @@ -28,7 +28,7 @@ class LogoGeneration(Scene): def construct(self): digest_config(self, {}) ## Usually shouldn't need this... - self.frame_duration = self.DEFAULT_CONFIG["frame_duration"] + self.frame_duration = self.CONFIG["frame_duration"] ## digest_config(self, {}) circle = Circle( diff --git a/helpers.py b/helpers.py index c66d82ec..b1ea06ba 100644 --- a/helpers.py +++ b/helpers.py @@ -86,20 +86,25 @@ def filtered_locals(local_args): def digest_config(obj, kwargs, local_args = {}): """ - Sets init args and DEFAULT_CONFIG values as local variables + Sets init args and CONFIG values as local variables + + The purpose of this function is to ensure that all + configuration of any object is inheritable, able to + be easily passed into instantiation, and is attached + as an attribute of the object. """ - ### Assemble list of DEFAULT_CONFIGs from all super classes + ### Assemble list of CONFIGs from all super classes classes_in_heirarchy = [obj.__class__] - default_configs = [] + configs = [] while len(classes_in_heirarchy) > 0: Class = classes_in_heirarchy.pop() classes_in_heirarchy += Class.__bases__ - if hasattr(Class, "DEFAULT_CONFIG"): - default_configs.append(Class.DEFAULT_CONFIG) + if hasattr(Class, "CONFIG"): + configs.append(Class.CONFIG) #Order matters a lot here, first dicts have higher priority all_dicts = [kwargs, filtered_locals(local_args), obj.__dict__] - all_dicts += default_configs + all_dicts += configs item_lists = reversed([d.items() for d in all_dicts]) obj.__dict__ = dict(reduce(op.add, item_lists)) diff --git a/scene/scene.py b/scene/scene.py index 8ec16287..870c381d 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -109,12 +109,11 @@ class Scene(object): animation.set_run_time(run_time) return animations - def separate_static_and_moving_mobjects(self, *animations): - moving_mobjects = [ - mobject + def separate_moving_and_static_mobjects(self, *animations): + moving_mobjects = list(it.chain(*[ + anim.mobject.submobject_family() for anim in animations - for mobject in anim.mobject.submobject_family() - ] + ])) bundle = Mobject(*self.mobjects) static_mobjects = filter( lambda m : m not in moving_mobjects, @@ -126,21 +125,30 @@ class Scene(object): run_time = animations[0].run_time times = np.arange(0, run_time, self.frame_duration) time_progression = ProgressDisplay(times) - time_progression.set_description( - "Animation %d: "%self.num_animations + \ - str(animations[0]) + \ - (", etc." if len(animations) > 1 else "") - ) + time_progression.set_description("".join([ + "Animation %d: "%self.num_animations, + str(animations[0]), + (", etc." if len(animations) > 1 else ""), + ])) return time_progression + def update_frame(self, moving_mobjects, static_image = None): + if static_image is not None: + self.camera.set_image(static_image) + else: + self.camera.reset() + self.camera.capture_mobjects(moving_mobjects) + + def play(self, *animations, **kwargs): if len(animations) == 0: raise Warning("Called Scene.play with no animations") return self.num_animations += 1 + self.add(*[anim.mobject for anim in animations]) animations = self.align_run_times(*animations, **kwargs) moving_mobjects, static_mobjects = \ - self.separate_static_and_moving_mobjects(*animations) + self.separate_moving_and_static_mobjects(*animations) self.camera.reset() self.camera.capture_mobjects( static_mobjects, @@ -151,14 +159,10 @@ class Scene(object): for t in self.get_time_progression(animations): for animation in animations: animation.update(t / animation.run_time) - self.camera.capture_mobjects(moving_mobjects) - frame = self.camera.get_image() - - self.frames.append(frame) - self.camera.set_image(static_image) + self.update_frame(moving_mobjects, static_image) + self.frames.append(self.get_frame()) for animation in animations: animation.clean_up() - self.add(*[anim.mobject for anim in animations]) return self def play_over_time_range(self, t0, t1, *animations): @@ -243,14 +247,14 @@ class Scene(object): print "Writing to %s"%file_path fps = int(1/self.frame_duration) - dim = tuple(reversed(self.shape)) + height, width = self.camera.pixel_shape command = [ FFMPEG_BIN, '-y', # overwrite output file if it exists '-f', 'rawvideo', '-vcodec','rawvideo', - '-s', '%dx%d'%dim, # size of one frame + '-s', '%dx%d'%(width, height), # size of one frame '-pix_fmt', 'rgb24', '-r', str(fps), # frames per second '-i', '-', # The imput comes from a pipe diff --git a/scene/zoomed_scene.py b/scene/zoomed_scene.py new file mode 100644 index 00000000..3c13e036 --- /dev/null +++ b/scene/zoomed_scene.py @@ -0,0 +1,61 @@ +import numpy as np + +from scene import Scene +from mobject import Mobject +from camera import MovingCamera, Camera + +from helpers import * + +class ZoomedScene(Scene): + CONFIG = { + "zoomed_canvas_space_shape" : (3, 3), + "zoomed_canvas_center" : None, + "zoomed_canvas_corner" : UP+RIGHT, + "zoomed_camera_background" : None + } + def init_zooming(self, moving_camera_mobject): + self.setup_zoomed_canvas() + self.zoomed_camera = MovingCamera( + moving_camera_mobject, + pixel_shape = self.zoomed_canvas_pixel_shape, + background = self.zoomed_camera_background + ) + self.add(moving_camera_mobject) + + def setup_zoomed_canvas(self): + height, width = self.zoomed_canvas_space_shape + canvas_corners = Mobject().add_points([ + ORIGIN, DOWN+RIGHT + ]) + canvas_corners.stretch_to_fit_height(height) + canvas_corners.stretch_to_fit_width(width) + canvas_corners.center() + if self.zoomed_canvas_center is not None: + canvas_corners.shift(self.zoomed_canvas_center) + elif self.zoomed_canvas_corner is not None: + canvas_corners.to_corner(self.zoomed_canvas_corner) + + pixel_coords = self.camera.points_to_pixel_coords(canvas_corners.points) + upper_left, lower_right = pixel_coords + self.zoomed_canvas_pixel_indices = pixel_coords + self.zoomed_canvas_pixel_shape = ( + lower_right[0] - upper_left[0], + lower_right[1] - upper_left[1] + ) + + def get_frame(self): + frame = Scene.get_frame(self) + (up, left), (down, right) = self.zoomed_canvas_pixel_indices + frame[left:right, up:down, :] = self.zoomed_camera.get_image() + return frame + + def update_frame(self, *args, **kwargs): + Scene.update_frame(self, *args, **kwargs) + self.zoomed_camera.reset() + self.zoomed_camera.capture_mobjects(self.mobjects) + + + + + +