Merge pull request #239 from 3b1b/alt-calc

Alt calc
This commit is contained in:
Grant Sanderson 2018-05-21 12:14:09 -07:00 committed by GitHub
commit 10c607af73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 484 additions and 801 deletions

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from big_ol_pile_of_manim_imports import * from big_ol_pile_of_manim_imports import *
@ -317,7 +316,7 @@ class NumberlineTransformationScene(ZoomedScene):
zcbr_group.add(mini_line_copy, mini_line) zcbr_group.add(mini_line_copy, mini_line)
anims += [FadeIn(mini_line), FadeIn(mini_line_copy)] anims += [FadeIn(mini_line), FadeIn(mini_line_copy)]
# Add tiny coordinates # Add tiny coordiantes
if local_coordinate_values is None: if local_coordinate_values is None:
local_coordinate_values = [x] local_coordinate_values = [x]
local_coordinates = self.get_local_coordinates( local_coordinates = self.get_local_coordinates(
@ -661,7 +660,7 @@ class StartingCalc101(PiCreatureScene):
circumference = TexMobject("2\\pi r") circumference = TexMobject("2\\pi r")
circumference.move_to(unfilled_circle) circumference.move_to(unfilled_circle)
equation = TexMobject( equation = TexMobject(
"{d (\\pi r^2) \\over dr} = 2\\pi r", "{d (\\pi r^2) \\over dx} = 2\\pi r",
tex_to_color_map={ tex_to_color_map={
"\\pi r^2": BLUE_D, "\\pi r^2": BLUE_D,
"2\\pi r": YELLOW, "2\\pi r": YELLOW,
@ -876,8 +875,8 @@ class Wrapper(Scene):
} }
def construct(self): def construct(self):
rect = self.rect = ScreenRectangle(height=self.screen_height) rect = ScreenRectangle(height=self.screen_height)
title = self.title = TextMobject(self.title, **self.title_kwargs) title = TextMobject(self.title, **self.title_kwargs)
title.to_edge(UP) title.to_edge(UP)
rect.next_to(title, DOWN) rect.next_to(title, DOWN)
@ -1563,11 +1562,11 @@ class TalkThroughXSquaredExample(IntroduceTransformationView):
la, ra = ra, la la, ra = ra, la
if factor < 0: if factor < 0:
kwargs = { kwargs = {
"path_arc": np.pi, "path_arc": -np.pi,
"use_rectangular_stem": False, "use_rectangular_stem": False,
} }
la = Arrow(UP, DOWN, **kwargs) la = Arrow(DOWN, UP, **kwargs)
ra = Arrow(DOWN, UP, **kwargs) ra = Arrow(UP, DOWN, **kwargs)
for arrow in la, ra: for arrow in la, ra:
arrow.pointwise_become_partial(arrow, 0, 0.9) arrow.pointwise_become_partial(arrow, 0, 0.9)
arrow.tip.scale(2) arrow.tip.scale(2)
@ -1872,8 +1871,9 @@ class ZoomInMoreAndMoreToZero(ZoomInOnXSquaredNearZero):
frame.scale_to_fit_height(factor * zoomed_display_height) frame.scale_to_fit_height(factor * zoomed_display_height)
self.local_coordinate_num_decimal_places = int(-np.log10(factor)) self.local_coordinate_num_decimal_places = int(-np.log10(factor))
zoom_words = TextMobject( zoom_words = TextMobject(
"Zoomed", "{:,}x \\\\".format(int(1.0 / factor)), "Zoomed ", "{:,}".format(int(1.0 / factor)),
"near 0", "x \\\\", "near 0",
arg_separator=""
) )
zoom_words.next_to(self.zoomed_display, DOWN) zoom_words.next_to(self.zoomed_display, DOWN)
@ -1954,14 +1954,6 @@ class XSquaredForNegativeInput(TalkThroughXSquaredExample):
sample_dots.set_fill(opacity=0.8) sample_dots.set_fill(opacity=0.8)
self.play(LaggedStart(DrawBorderThenFill, sample_dots)) 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.add_sample_dot_ghosts(sample_dots)
self.apply_function(self.func, sample_dots=sample_dots) self.apply_function(self.func, sample_dots=sample_dots)
self.wait() self.wait()
@ -3215,7 +3207,6 @@ class StabilityAndInstability(AnalyzeFunctionWithTransformations):
CONFIG = { CONFIG = {
"num_initial_applications": 0, "num_initial_applications": 0,
} }
def construct(self): def construct(self):
self.force_skipping() self.force_skipping()
self.add_function_title() self.add_function_title()
@ -3246,10 +3237,9 @@ class StabilityAndInstability(AnalyzeFunctionWithTransformations):
self.all_arrows self.all_arrows
)).copy() )).copy()
arrows.set_fill(PINK, 1) arrows.set_fill(PINK, 1)
arrows.set_stroke(PINK, 3)
arrows.second_anim = LaggedStart( arrows.second_anim = LaggedStart(
ApplyMethod, arrows, ApplyMethod, arrows,
lambda m: (m.set_color, YELLOW), lambda m: (m.set_fill, YELLOW, 1),
rate_func=there_and_back_with_pause, rate_func=there_and_back_with_pause,
lag_ratio=0.7, lag_ratio=0.7,
run_time=2, run_time=2,
@ -3279,87 +3269,11 @@ class StabilityAndInstability(AnalyzeFunctionWithTransformations):
def write_derivative_fact(self): def write_derivative_fact(self):
stable_label = self.stable_label stable_label = self.stable_label
unstable_label = self.unstable_label unstable_label = self.unstable_label
labels = VGroup(stable_label, unstable_label)
phi_arrows = self.phi_arrows phi_arrows = self.phi_arrows
phi_bro_arrows = self.phi_bro_arrows phi_bro_arrows = self.phi_bro_arrows
arrow_groups = VGroup(phi_arrows, phi_bro_arrows) arrow_groups = [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): class NotBetterThanGraphs(TeacherStudentsScene):
@ -3391,23 +3305,6 @@ class NotBetterThanGraphs(TeacherStudentsScene):
self.wait(3) 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): class TopicsAfterSingleVariable(PiCreatureScene, MoreTopics):
CONFIG = { CONFIG = {
"pi_creatures_start_on_screen": False, "pi_creatures_start_on_screen": False,
@ -3459,83 +3356,6 @@ class TopicsAfterSingleVariable(PiCreatureScene, MoreTopics):
self.wait(4) 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): class ComplexAnalysisOverlay(Scene):
def construct(self): def construct(self):
words = TextMobject("Complex analysis") words = TextMobject("Complex analysis")
@ -3545,7 +3365,6 @@ class ComplexAnalysisOverlay(Scene):
self.add(words) self.add(words)
self.wait() self.wait()
class CompelxAnalyticFluidFlow(ComplexTransformationScene, MovingCameraScene): class CompelxAnalyticFluidFlow(ComplexTransformationScene, MovingCameraScene):
CONFIG = { CONFIG = {
"num_anchors_to_add_per_line": 200, "num_anchors_to_add_per_line": 200,
@ -3623,7 +3442,6 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene):
CONFIG = { CONFIG = {
"num_anchors_to_add_per_line": 20, "num_anchors_to_add_per_line": 20,
"complex_homotopy": lambda z, t: z**(1.0 + t), "complex_homotopy": lambda z, t: z**(1.0 + t),
"zoom_factor": 0.05,
} }
def setup(self): def setup(self):
@ -3633,8 +3451,8 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene):
def construct(self): def construct(self):
self.edit_background_plane() self.edit_background_plane()
self.add_title() self.add_title()
# self.add_transforming_planes() self.add_transforming_planes()
# self.preview_some_numbers() self.preview_some_numbers()
self.zoom_in_to_one_plus_half_i() self.zoom_in_to_one_plus_half_i()
self.write_derivative() self.write_derivative()
@ -3730,44 +3548,9 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene):
self.play(FadeOut(dot_groups)) self.play(FadeOut(dot_groups))
self.wait() self.wait()
self.play(FadeOut(self.plane)) self.play(FadeOut(self.plane))
self.transformable_mobjects.remove(self.plane)
def zoom_in_to_one_plus_half_i(self): def zoom_in_to_one_plus_half_i(self):
z = complex(1, 0.5) pass
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): def write_derivative(self):
pass pass
@ -3784,244 +3567,3 @@ class AnalyzeZSquared(ComplexTransformationScene, ZoomedScene):
top_plane.next_to(ORIGIN, UP, buff=tiny_tiny_buff) top_plane.next_to(ORIGIN, UP, buff=tiny_tiny_buff)
bottom_plane.next_to(ORIGIN, DOWN, buff=tiny_tiny_buff) bottom_plane.next_to(ORIGIN, DOWN, buff=tiny_tiny_buff)
return VGroup(top_plane, bottom_plane) 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)

View file

@ -206,7 +206,7 @@ class LeaveItToComputers(TeacherStudentsScene):
class PrerequisiteKnowledge(TeacherStudentsScene): class PrerequisiteKnowledge(TeacherStudentsScene):
CONFIG = { CONFIG = {
"camera_config": {"background_alpha": 255} "camera_config": {"background_opacity": 1}
} }
def construct(self): def construct(self):

View file

@ -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): class FadeToColor(ApplyMethod):
def __init__(self, mobject, color, **kwargs): def __init__(self, mobject, color, **kwargs):
ApplyMethod.__init__(self, mobject.set_color, color, **kwargs) ApplyMethod.__init__(self, mobject.set_color, color, **kwargs)

View file

@ -1,5 +1,6 @@
import itertools as it import itertools as it
import numpy as np import numpy as np
import operator as op
import aggdraw import aggdraw
import copy import copy
@ -7,9 +8,10 @@ import time
from PIL import Image from PIL import Image
from colour import Color from colour import Color
from scipy.spatial.distance import pdist
from constants import * from constants import *
from mobject.types.image_mobject import ImageMobject from mobject.types.image_mobject import AbstractImageMobject
from mobject.mobject import Mobject from mobject.mobject import Mobject
from mobject.types.point_cloud_mobject import PMobject from mobject.types.point_cloud_mobject import PMobject
from mobject.types.vectorized_mobject import VMobject 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 list_difference_update
from utils.iterables import remove_list_redundancies from utils.iterables import remove_list_redundancies
from utils.simple_functions import fdiv from utils.simple_functions import fdiv
from utils.space_ops import angle_of_vector
class Camera(object): class Camera(object):
CONFIG = { CONFIG = {
"background_image": None, "background_image": None,
"pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH), "pixel_height": DEFAULT_PIXEL_HEIGHT,
# Note: frame_shape will be resized to match pixel_shape "pixel_width": DEFAULT_PIXEL_WIDTH,
"frame_shape": (FRAME_HEIGHT, FRAME_WIDTH), # Note: frame height and width will be resized to match
"space_center": ORIGIN, # the pixel aspect ratio
"frame_height": FRAME_HEIGHT,
"frame_width": FRAME_WIDTH,
"frame_center": ORIGIN,
"background_color": BLACK, "background_color": BLACK,
"background_opacity": 0,
# Points in vectorized mobjects with norm greater # Points in vectorized mobjects with norm greater
# than this value will be rescaled. # than this value will be rescaled.
"max_allowable_norm": FRAME_WIDTH, "max_allowable_norm": FRAME_WIDTH,
"image_mode": "RGBA", "image_mode": "RGBA",
"n_rgb_coords": 4, "n_rgb_coords": 4,
"background_alpha": 0, # Out of rgb_max_val
"pixel_array_dtype": 'uint8', "pixel_array_dtype": 'uint8',
"use_z_coordinate_for_display_order": False, "use_z_coordinate_for_display_order": False,
# z_buff_func is only used if the flag above is set to True. # z_buff_func is only used if the flag above is set to True.
@ -58,36 +64,72 @@ class Camera(object):
self.canvas = None self.canvas = None
return copy.copy(self) 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): def resize_frame_shape(self, fixed_dimension=0):
""" """
Changes frame_shape to match the aspect ratio Changes frame_shape to match the aspect ratio
of pixel_shape, where fixed_dimension determines of the pixels, where fixed_dimension determines
whether frame_shape[0] (height) or frame_shape[1] (width) whether frame_height or frame_width
remains fixed while the other changes accordingly. remains fixed while the other changes accordingly.
""" """
aspect_ratio = float(self.pixel_shape[1]) / self.pixel_shape[0] pixel_height = self.get_pixel_height()
frame_width, frame_height = self.frame_shape 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: if fixed_dimension == 0:
frame_height = aspect_ratio * frame_width frame_height = frame_width / aspect_ratio
else: else:
frame_width = frame_height / aspect_ratio frame_width = aspect_ratio * frame_height
self.frame_shape = (frame_width, frame_height) self.set_frame_height(frame_height)
self.set_frame_width(frame_width)
def init_background(self): def init_background(self):
height = self.get_pixel_height()
width = self.get_pixel_width()
if self.background_image is not None: if self.background_image is not None:
path = get_full_raster_image_path(self.background_image) path = get_full_raster_image_path(self.background_image)
image = Image.open(path).convert(self.image_mode) image = Image.open(path).convert(self.image_mode)
height, width = self.pixel_shape
# TODO, how to gracefully handle backgrounds # TODO, how to gracefully handle backgrounds
# with different sizes? # with different sizes?
self.background = np.array(image)[:height, :width] self.background = np.array(image)[:height, :width]
self.background = self.background.astype(self.pixel_array_dtype) self.background = self.background.astype(self.pixel_array_dtype)
else: else:
background_rgba = color_to_int_rgba( background_rgba = color_to_int_rgba(
self.background_color, alpha=self.background_alpha self.background_color, self.background_opacity
) )
self.background = np.zeros( self.background = np.zeros(
list(self.pixel_shape) + [self.n_rgb_coords], (height, width, self.n_rgb_coords),
dtype=self.pixel_array_dtype dtype=self.pixel_array_dtype
) )
self.background[:, :] = background_rgba self.background[:, :] = background_rgba
@ -105,10 +147,10 @@ class Camera(object):
retval = np.array(pixel_array) retval = np.array(pixel_array)
if convert_from_floats: if convert_from_floats:
retval = np.apply_along_axis( retval = np.apply_along_axis(
lambda f: ( lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype),
f * self.rgb_max_val).astype(self.pixel_array_dtype),
2, 2,
retval) retval
)
return retval return retval
def set_pixel_array(self, pixel_array, convert_from_floats=False): def set_pixel_array(self, pixel_array, convert_from_floats=False):
@ -148,6 +190,7 @@ class Camera(object):
def reset(self): def reset(self):
self.set_pixel_array(self.background) self.set_pixel_array(self.background)
return self
#### ####
@ -201,7 +244,7 @@ class Camera(object):
type_func_pairs = [ type_func_pairs = [
(VMobject, self.display_multiple_vectorized_mobjects), (VMobject, self.display_multiple_vectorized_mobjects),
(PMobject, self.display_multiple_point_cloud_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 (Mobject, lambda batch: batch), # Do nothing
] ]
@ -301,8 +344,7 @@ class Camera(object):
# points = self.adjust_out_of_range_points(points) # points = self.adjust_out_of_range_points(points)
if len(points) == 0: if len(points) == 0:
continue continue
aligned_points = self.align_points_to_camera(points) coords = self.points_to_pixel_coords(points)
coords = self.points_to_pixel_coords(aligned_points)
coord_strings = coords.flatten().astype(str) coord_strings = coords.flatten().astype(str)
# Start new path string with M # Start new path string with M
coord_strings[0] = "M" + coord_strings[0] coord_strings[0] = "M" + coord_strings[0]
@ -342,7 +384,6 @@ class Camera(object):
def display_point_cloud(self, points, rgbas, thickness): def display_point_cloud(self, points, rgbas, thickness):
if len(points) == 0: if len(points) == 0:
return return
points = self.align_points_to_camera(points)
pixel_coords = self.points_to_pixel_coords(points) pixel_coords = self.points_to_pixel_coords(points)
pixel_coords = self.thickened_coordinates( pixel_coords = self.thickened_coordinates(
pixel_coords, thickness pixel_coords, thickness
@ -358,7 +399,8 @@ class Camera(object):
pixel_coords = pixel_coords[on_screen_indices] pixel_coords = pixel_coords[on_screen_indices]
rgbas = rgbas[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 = np.array([1, pw], dtype='int')
flattener = flattener.reshape((2, 1)) flattener = flattener.reshape((2, 1))
@ -378,94 +420,56 @@ class Camera(object):
ul_coords, ur_coords, dl_coords = corner_coords ul_coords, ur_coords, dl_coords = corner_coords
right_vect = ur_coords - ul_coords right_vect = ur_coords - ul_coords
down_vect = dl_coords - ul_coords down_vect = dl_coords - ul_coords
center_coords = ul_coords + (right_vect + down_vect) / 2
impa = image_mobject.pixel_array sub_image = Image.fromarray(
image_mobject.get_pixel_array(),
oh, ow = self.pixel_array.shape[:2] # Outer width and height mode="RGBA"
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
) )
self.pixel_array[..., :3] = out_rgb * self.rgb_max_val # Reshape
self.pixel_array[..., 3] = out_a * self.rgb_max_val 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): # Rotate
# This is where projection should live angle = angle_of_vector(right_vect)
return points - self.space_center 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): def adjust_out_of_range_points(self, points):
if not np.any(points > self.max_allowable_norm): if not np.any(points > self.max_allowable_norm):
@ -483,31 +487,43 @@ class Camera(object):
return points return points
def points_to_pixel_coords(self, points): def points_to_pixel_coords(self, points):
shifted_points = points - self.get_frame_center()
result = np.zeros((len(points), 2)) result = np.zeros((len(points), 2))
ph, pw = self.pixel_shape pixel_height = self.get_pixel_height()
sh, sw = self.frame_shape pixel_width = self.get_pixel_width()
width_mult = pw / sw frame_height = self.get_frame_height()
width_add = pw / 2 frame_width = self.get_frame_width()
height_mult = ph / sh width_mult = pixel_width / frame_width
height_add = ph / 2 width_add = pixel_width / 2
height_mult = pixel_height / frame_height
height_add = pixel_height / 2
# Flip on y-axis as you go # Flip on y-axis as you go
height_mult *= -1 height_mult *= -1
result[:, 0] = points[:, 0] * width_mult + width_add result[:, 0] = shifted_points[:, 0] * width_mult + width_add
result[:, 1] = points[:, 1] * height_mult + height_add result[:, 1] = shifted_points[:, 1] * height_mult + height_add
return result.astype('int') return result.astype('int')
def on_screen_pixels(self, pixel_coords): def on_screen_pixels(self, pixel_coords):
return reduce(op.and_, [ return reduce(op.and_, [
pixel_coords[:, 0] >= 0, 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] >= 0,
pixel_coords[:, 1] < self.pixel_shape[0], pixel_coords[:, 1] < self.get_pixel_height(),
]) ])
def adjusted_thickness(self, thickness): def adjusted_thickness(self, thickness):
big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"] # TODO: This seems...unsystematic
factor = sum(big_shape) / sum(self.pixel_shape) 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 return 1 + (thickness - 1) / factor
def get_thickening_nudges(self, thickness): def get_thickening_nudges(self, thickness):
@ -525,13 +541,20 @@ class Camera(object):
def get_coords_of_all_pixels(self): def get_coords_of_all_pixels(self):
# These are in x, y order, to help me keep things straight # These are in x, y order, to help me keep things straight
full_space_dims = np.array(self.frame_shape)[::-1] full_space_dims = np.array([
full_pixel_dims = np.array(self.pixel_shape)[::-1] 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 # These are addressed in the same y, x order as in pixel_array, but the values in them
# are listed in x, y order # are listed in x, y order
uncentered_pixel_coords = np.indices(self.pixel_shape)[ uncentered_pixel_coords = np.indices(
::-1].transpose(1, 2, 0) [self.get_pixel_height(), self.get_pixel_width()]
)[::-1].transpose(1, 2, 0)
uncentered_space_coords = fdiv( uncentered_space_coords = fdiv(
uncentered_pixel_coords * full_space_dims, uncentered_pixel_coords * full_space_dims,
full_pixel_dims) full_pixel_dims)
@ -541,7 +564,8 @@ class Camera(object):
# overflow is unlikely to be a problem) # overflow is unlikely to be a problem)
centered_space_coords = ( 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 # Have to also flip the y coordinates to account for pixel array being listed in
# top-to-bottom order, opposite of screen coordinate convention # top-to-bottom order, opposite of screen coordinate convention

View file

@ -43,8 +43,9 @@ class MappingCamera(Camera):
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a # TODO: Add optional separator borders between cameras (or perhaps peel this off into a
# CameraPlusOverlay class) # CameraPlusOverlay class)
# TODO, the classes below should likely be deleted
class MultiCamera(Camera): class OldMultiCamera(Camera):
def __init__(self, *cameras_with_start_positions, **kwargs): def __init__(self, *cameras_with_start_positions, **kwargs):
self.shifted_cameras = [ self.shifted_cameras = [
DictAsObject( DictAsObject(
@ -52,8 +53,8 @@ class MultiCamera(Camera):
"camera": camera_with_start_positions[0], "camera": camera_with_start_positions[0],
"start_x": camera_with_start_positions[1][1], "start_x": camera_with_start_positions[1][1],
"start_y": camera_with_start_positions[1][0], "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_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].pixel_shape[0], "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 for camera_with_start_positions in cameras_with_start_positions
] ]
@ -92,23 +93,23 @@ class MultiCamera(Camera):
for shifted_camera in self.shifted_cameras: for shifted_camera in self.shifted_cameras:
shifted_camera.camera.init_background() 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 # 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): def __init__(self, left_camera, right_camera, **kwargs):
digest_config(self, kwargs) digest_config(self, kwargs)
self.left_camera = left_camera self.left_camera = left_camera
self.right_camera = right_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]: for camera in [self.left_camera, self.right_camera]:
# TODO: Round up on one if width is odd # TODO: Round up on one if width is odd
camera.pixel_shape = (self.pixel_shape[0], half_width) camera.reset_pixel_shape(camera.get_pixel_height(), half_width)
camera.init_background()
camera.resize_frame_shape()
camera.reset()
MultiCamera.__init__(self, (left_camera, (0, 0)), OldMultiCamera.__init__(
(right_camera, (0, half_width))) self,
(left_camera, (0, 0)),
(right_camera, (0, half_width)),
)

View file

@ -1,40 +1,77 @@
from __future__ import absolute_import from __future__ import absolute_import
from constants import FRAME_HEIGHT from constants import FRAME_HEIGHT
from constants import WHITE
from camera.camera import Camera from camera.camera import Camera
from mobject.frame import ScreenRectangle from mobject.frame import ScreenRectangle
from utils.config_ops import digest_config
class MovingCamera(Camera): class MovingCamera(Camera):
""" """
Stays in line with the height, width and position Stays in line with the height, width and position of it's 'frame', which is a Rectangle
of a given mobject
""" """
CONFIG = { 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): def __init__(self, frame=None, **kwargs):
""" """
frame is a Mobject, (should be a rectangle) determining frame is a Mobject, (should almost certainly be a rectangle)
which region of space the camera displys determining which region of space the camera displys
""" """
digest_config(self, kwargs)
if frame is None: if frame is None:
frame = ScreenRectangle(height=FRAME_HEIGHT) frame = ScreenRectangle(height=FRAME_HEIGHT)
frame.fade(1) frame.set_stroke(
self.default_frame_stroke_color,
self.default_frame_stroke_width,
)
self.frame = frame self.frame = frame
Camera.__init__(self, **kwargs) Camera.__init__(self, **kwargs)
def capture_mobjects(self, *args, **kwargs): # TODO, make these work for a rotated frame
self.space_center = self.frame.get_center() def get_frame_height(self):
self.realign_frame_shape() return self.frame.get_height()
Camera.capture_mobjects(self, *args, **kwargs)
def realign_frame_shape(self): def get_frame_width(self):
height, width = self.frame_shape return self.frame.get_width()
if self.aligned_dimension == "height":
self.frame_shape = (self.frame.get_height(), width) def get_frame_center(self):
else: return self.frame.get_center()
self.frame_shape = (height, self.frame.get_width())
self.resize_frame_shape(0 if self.aligned_dimension == "height" else 1) 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]

59
camera/multi_camera.py Normal file
View file

@ -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
]

View file

@ -12,6 +12,8 @@ from utils.bezier import interpolate
from utils.space_ops import rotation_about_z from utils.space_ops import rotation_about_z
from utils.space_ops import rotation_matrix from utils.space_ops import rotation_matrix
# TODO: Make sure this plays well with latest camera updates
class CameraWithPerspective(Camera): class CameraWithPerspective(Camera):
CONFIG = { CONFIG = {
@ -49,7 +51,7 @@ class ThreeDCamera(CameraWithPerspective):
self.rotation_mobject = VectorizedPoint() self.rotation_mobject = VectorizedPoint()
# moving_center lives in the x-y-z space # moving_center lives in the x-y-z space
# It representes the center of rotation # 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) self.set_position(self.phi, self.theta, self.distance)
def modified_rgb(self, vmobject, rgb): def modified_rgb(self, vmobject, rgb):
@ -163,7 +165,7 @@ class ThreeDCamera(CameraWithPerspective):
center_of_rotation = self.get_center_of_rotation( center_of_rotation = self.get_center_of_rotation(
center_x, center_y, center_z) center_x, center_y, center_z)
self.moving_center.move_to(center_of_rotation) 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): def get_view_transformation_matrix(self):
return (self.default_distance / self.get_distance()) * np.dot( return (self.default_distance / self.get_distance()) * np.dot(
@ -174,6 +176,6 @@ class ThreeDCamera(CameraWithPerspective):
def points_to_pixel_coords(self, points): def points_to_pixel_coords(self, points):
matrix = self.get_view_transformation_matrix() matrix = self.get_view_transformation_matrix()
new_points = np.dot(points, matrix.T) 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) return Camera.points_to_pixel_coords(self, new_points)

View file

@ -17,17 +17,20 @@ LOW_QUALITY_FRAME_DURATION = 1. / 15
MEDIUM_QUALITY_FRAME_DURATION = 1. / 30 MEDIUM_QUALITY_FRAME_DURATION = 1. / 30
PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60 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 = { 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 = { MEDIUM_QUALITY_CAMERA_CONFIG = {
"pixel_shape": (720, 1280), "pixel_height": 720,
"pixel_width": 1280,
} }
LOW_QUALITY_CAMERA_CONFIG = { LOW_QUALITY_CAMERA_CONFIG = {
"pixel_shape": (480, 854), "pixel_height": 480,
"pixel_width": 854,
} }
DEFAULT_POINT_DENSITY_2D = 25 DEFAULT_POINT_DENSITY_2D = 25

View file

@ -224,11 +224,8 @@ def get_module_posix(file_name):
module_name = file_name.replace(".py", "") module_name = file_name.replace(".py", "")
last_module = imp.load_module(".", *imp.find_module(".")) last_module = imp.load_module(".", *imp.find_module("."))
for part in module_name.split(os.sep): for part in module_name.split(os.sep):
try: load_args = imp.find_module(part, last_module.__path__)
load_args = imp.find_module(part, last_module.__path__) last_module = imp.load_module(part, *load_args)
last_module = imp.load_module(part, *load_args)
except ImportError:
continue
return last_module return last_module

View file

@ -199,6 +199,15 @@ class Mobject(Container):
) )
return self 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): def apply_matrix(self, matrix, **kwargs):
# Default to applying matrix about the origin, not mobjects center # Default to applying matrix about the origin, not mobjects center
if len(kwargs) == 0: if len(kwargs) == 0:

View file

@ -7,25 +7,51 @@ from PIL import Image
from constants import * from constants import *
from mobject.mobject import Mobject from mobject.mobject import Mobject
from mobject.shape_matchers import SurroundingRectangle
from utils.bezier import interpolate from utils.bezier import interpolate
from utils.color import color_to_int_rgb from utils.color import color_to_int_rgb
from utils.config_ops import digest_config from utils.config_ops import digest_config
from utils.images import get_full_raster_image_path from utils.images import get_full_raster_image_path
class ImageMobject(Mobject): class AbstractImageMobject(Mobject):
""" """
Automatically filters out black pixels Automatically filters out black pixels
""" """
CONFIG = { CONFIG = {
"filter_color": "black",
"invert": False,
# "use_cache" : True,
"height": 2.0, "height": 2.0,
"image_mode": "RGBA",
"pixel_array_dtype": "uint8", "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): def __init__(self, filename_or_array, **kwargs):
digest_config(self, kwargs) digest_config(self, kwargs)
if isinstance(filename_or_array, str): if isinstance(filename_or_array, str):
@ -37,7 +63,7 @@ class ImageMobject(Mobject):
self.change_to_rgba_array() self.change_to_rgba_array()
if self.invert: if self.invert:
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3] self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
Mobject.__init__(self, **kwargs) AbstractImageMobject.__init__(self, **kwargs)
def change_to_rgba_array(self): def change_to_rgba_array(self):
pa = self.pixel_array pa = self.pixel_array
@ -53,6 +79,9 @@ class ImageMobject(Mobject):
pa = np.append(pa, alphas, axis=2) pa = np.append(pa, alphas, axis=2)
self.pixel_array = pa self.pixel_array = pa
def get_pixel_array(self):
return self.pixel_array
def set_color(self, color, alpha=None, family=True): def set_color(self, color, alpha=None, family=True):
rgb = color_to_int_rgb(color) rgb = color_to_int_rgb(color)
self.pixel_array[:, :, :3] = rgb self.pixel_array[:, :, :3] = rgb
@ -63,19 +92,6 @@ class ImageMobject(Mobject):
self.color = color self.color = color
return self 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): def set_opacity(self, alpha):
self.pixel_array[:, :, 3] = int(255 * alpha) self.pixel_array[:, :, 3] = int(255 * alpha)
return self return self
@ -90,5 +106,30 @@ class ImageMobject(Mobject):
mobject1.pixel_array, mobject2.pixel_array, alpha mobject1.pixel_array, mobject2.pixel_array, alpha
).astype(self.pixel_array_dtype) ).astype(self.pixel_array_dtype)
def copy(self): # TODO, add the ability to have the dimensions/orientation of this
return self.deepcopy() # 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

View file

@ -668,7 +668,7 @@ class ColorMappedByFuncScene(Scene):
# We hash just based on output image # We hash just based on output image
# Thus, multiple scenes with same output image can re-use it # Thus, multiple scenes with same output image can re-use it
# without recomputation # 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( self.background_image_file = self.short_path_to_long_path(
"color_mapped_bg_hash_" + str(full_hash) + ".png" "color_mapped_bg_hash_" + str(full_hash) + ".png"
) )

View file

@ -2884,7 +2884,7 @@ class ZeroFoundOnBoundary(Scene):
class AllOfTheVideos(Scene): class AllOfTheVideos(Scene):
CONFIG = { CONFIG = {
"camera_config" : { "camera_config" : {
"background_alpha" : 255, "background_opacity" : 1,
} }
} }
def construct(self): def construct(self):
@ -3274,5 +3274,6 @@ class Thumbnail(SearchSpacePerimeterVsArea):

View file

@ -2056,7 +2056,7 @@ class IPTScene1(PiCreatureScene):
# use the following for the zoomed inset # use the following for the zoomed inset
if show_detail: if show_detail:
self.camera.frame_shape = (0.02 * FRAME_HEIGHT, 0.02 * FRAME_WIDTH) 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_SCALE = 0.01
SCREEN_THICKNESS = 0.02 SCREEN_THICKNESS = 0.02

View file

@ -1479,7 +1479,7 @@ class AskAboutLayers(PreviewMNistNetwork):
class BreakUpMacroPatterns(IntroduceEachLayer): class BreakUpMacroPatterns(IntroduceEachLayer):
CONFIG = { CONFIG = {
"camera_config" : {"background_alpha" : 255}, "camera_config" : {"background_opacity" : 1},
"prefixes" : [ "prefixes" : [
"nine", "eight", "four", "nine", "eight", "four",
"upper_loop", "right_line", "upper_loop", "right_line",
@ -1868,7 +1868,7 @@ class BreakUpMicroPatterns(BreakUpMacroPatterns):
class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer): class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer):
CONFIG = { CONFIG = {
"camera_config" : { "camera_config" : {
"background_alpha" : 255, "background_opacity" : 1,
}, },
"network_mob_config" : { "network_mob_config" : {
"layer_to_layer_buff" : 2, "layer_to_layer_buff" : 2,
@ -2081,7 +2081,7 @@ class SecondLayerIsLittleEdgeLayer(IntroduceEachLayer):
class EdgeDetection(Scene): class EdgeDetection(Scene):
CONFIG = { CONFIG = {
"camera_config" : {"background_alpha" : 255} "camera_config" : {"background_opacity" : 1}
} }
def construct(self): def construct(self):
lion = ImageMobject("Lion") lion = ImageMobject("Lion")

View file

@ -468,7 +468,7 @@ class TauFalls(Scene):
class EulerWrites628(Scene): class EulerWrites628(Scene):
CONFIG = { CONFIG = {
"camera_config" : { "camera_config" : {
"background_alpha" : 255, "background_opacity" : 1,
} }
} }
def construct(self): def construct(self):
@ -548,7 +548,7 @@ class EulerWrites628(Scene):
class HeroAndVillain(Scene): class HeroAndVillain(Scene):
CONFIG = { CONFIG = {
"camera_config" : { "camera_config" : {
"background_alpha" : 255, "background_opacity" : 1,
} }
} }
def construct(self): def construct(self):
@ -987,7 +987,7 @@ class SpecialThanks(Scene):
class EndScene(PatreonEndScreen): class EndScene(PatreonEndScreen):
CONFIG = { CONFIG = {
"camera_config" : { "camera_config" : {
"background_alpha" : 255, "background_opacity" : 1,
} }
} }
def construct(self): def construct(self):

View file

@ -4958,7 +4958,7 @@ class KeeperAndSailorForSineProduct(KeeperAndSailor):
class Conclusion(TeacherStudentsScene): class Conclusion(TeacherStudentsScene):
CONFIG = { CONFIG = {
"camera_config": {"background_alpha": 255}, "camera_config": {"background_opacity": 1},
} }
def construct(self): def construct(self):

View file

@ -1,26 +1,32 @@
from __future__ import absolute_import from __future__ import absolute_import
from constants import *
from scene.scene import Scene from scene.scene import Scene
from camera.moving_camera import MovingCamera from camera.moving_camera import MovingCamera
from mobject.frame import ScreenRectangle from utils.iterables import list_update
class MovingCameraScene(Scene): class MovingCameraScene(Scene):
CONFIG = {
"camera_class": MovingCamera
}
def setup(self): def setup(self):
self.camera_frame = ScreenRectangle(height=FRAME_HEIGHT) Scene.setup(self)
self.camera_frame.set_stroke(width=0) assert(isinstance(self.camera, MovingCamera))
self.camera = MovingCamera( self.camera_frame = self.camera.frame
self.camera_frame, **self.camera_config # Hmm, this currently relies on the fact that MovingCamera
) # willd default to a full-sized frame. Is that okay?
return self return self
def get_moving_mobjects(self, *animations): def get_moving_mobjects(self, *animations):
# TODO: Code repetition from ZoomedScene
moving_mobjects = Scene.get_moving_mobjects(self, *animations) moving_mobjects = Scene.get_moving_mobjects(self, *animations)
if self.camera_frame in moving_mobjects: all_moving_mobjects = self.camera.extract_mobject_family_members(
# When the camera is moving, so is everything, moving_mobjects
return self.mobjects )
else: movement_indicators = self.camera.get_mobjects_indicating_movement()
return moving_mobjects 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

View file

@ -572,7 +572,8 @@ class Scene(Container):
self.args_to_rename_file = (temp_file_path, file_path) self.args_to_rename_file = (temp_file_path, file_path)
fps = int(1 / self.frame_duration) 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 = [ command = [
FFMPEG_BIN, FFMPEG_BIN,

View file

@ -1,20 +1,19 @@
from __future__ import absolute_import from __future__ import absolute_import
import numpy as np from scene.moving_camera_scene import MovingCameraScene
from scene.scene import Scene
from animation.creation import FadeIn
from camera.moving_camera import MovingCamera 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 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): class ZoomedScene(MovingCameraScene):
"""
Move around self.little_rectangle to determine
which part of the screen is zoomed in on.
"""
CONFIG = { CONFIG = {
"camera_class": MultiCamera, "camera_class": MultiCamera,
"zoomed_display_height": 3, "zoomed_display_height": 3,
@ -33,109 +32,62 @@ class ZoomedScene(Scene):
"zoom_activated": False, "zoom_activated": False,
} }
def activate_zooming(self): def setup(self):
self.generate_big_rectangle() MovingCameraScene.setup(self)
self.setup_zoomed_canvas() # Initialize camera and display
self.setup_zoomed_camera() zoomed_camera = MovingCamera(**self.zoomed_camera_config)
self.zoom_activated = True zoomed_display = ImageMobjectFromCamera(
zoomed_camera, **self.zoomed_camera_image_mobject_config
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: zoomed_display.add_display_frame()
self.big_rectangle.shift(self.zoomed_canvas_center) for mob in zoomed_camera.frame, zoomed_display:
elif self.zoomed_canvas_corner is not None: mob.stretch_to_fit_height(self.zoomed_display_height)
self.big_rectangle.to_corner( mob.stretch_to_fit_width(self.zoomed_display_width)
self.zoomed_canvas_corner, zoomed_camera.frame.scale(self.zoom_factor)
buff=self.zoomed_canvas_corner_buff
)
self.add(self.big_rectangle)
def setup_zoomed_canvas(self): # Position camera and display
upper_left = self.big_rectangle.get_corner(UP + LEFT) zoomed_camera.frame.move_to(self.zoomed_camera_frame_starting_position)
lower_right = self.big_rectangle.get_corner(DOWN + RIGHT) if self.zoomed_display_center is not None:
pixel_coords = self.camera.points_to_pixel_coords( zoomed_display.move_to(self.zoomed_display_center)
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
else: 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()
)

View file

@ -39,7 +39,8 @@ def color_to_int_rgb(color):
return (255 * color_to_rgb(color)).astype('uint8') 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) return np.append(color_to_int_rgb(color), alpha)

View file

@ -34,7 +34,7 @@ def get_scene_output_directory(scene_class):
def get_movie_output_directory(scene_class, camera_config, frame_duration): def get_movie_output_directory(scene_class, camera_config, frame_duration):
directory = get_scene_output_directory(scene_class) directory = get_scene_output_directory(scene_class)
sub_dir = "%dp%d" % ( sub_dir = "%dp%d" % (
camera_config["pixel_shape"][0], camera_config["pixel_height"],
int(1.0 / frame_duration) int(1.0 / frame_duration)
) )
return guarantee_existance(os.path.join(directory, sub_dir)) return guarantee_existance(os.path.join(directory, sub_dir))