mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Initial ZoomedScene
This commit is contained in:
parent
a06848a219
commit
b6985b4321
9 changed files with 109 additions and 222 deletions
|
@ -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)
|
||||
|
|
13
camera.py
13
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)
|
||||
|
|
|
@ -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
|
||||
|
|
185
displayer.py
185
displayer.py
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
17
helpers.py
17
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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
61
scene/zoomed_scene.py
Normal file
61
scene/zoomed_scene.py
Normal file
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Add table
Reference in a new issue