mirror of
https://github.com/3b1b/manim.git
synced 2025-09-19 04:41:56 +00:00
Merge pull request #108 from 3b1b/WindingNumber
Z buffer sorting added to camera, set_background_by_func fixed, other small fixes
This commit is contained in:
commit
6829ff1152
5 changed files with 237 additions and 111 deletions
|
@ -200,25 +200,36 @@ class EquationSolver1d(GraphScene, ZoomedScene):
|
|||
self.drawGraph()
|
||||
self.solveEquation()
|
||||
|
||||
colorslist = map(color_to_rgba, ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"])
|
||||
|
||||
def rev_to_color(alpha):
|
||||
def rev_to_rgba(alpha):
|
||||
alpha = alpha % 1
|
||||
colors = ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"]
|
||||
colors = colorslist
|
||||
num_colors = len(colors)
|
||||
beta = (alpha % (1.0/num_colors)) * num_colors
|
||||
start_index = int(np.floor(num_colors * alpha)) % num_colors
|
||||
end_index = (start_index + 1) % num_colors
|
||||
|
||||
return interpolate_color(colors[start_index], colors[end_index], beta)
|
||||
return interpolate(colors[start_index], colors[end_index], beta)
|
||||
|
||||
def point_to_rev((x, y)):
|
||||
def rev_to_color(alpha):
|
||||
return rgba_to_color(rev_to_rgba(alpha))
|
||||
|
||||
def point_to_rev((x, y), allow_origin = False):
|
||||
# Warning: np.arctan2 would happily discontinuously returns the value 0 for (0, 0), due to
|
||||
# design choices in the underlying atan2 library call, but for our purposes, this is
|
||||
# illegitimate, and all winding number calculations must be set up to avoid this
|
||||
if (x, y) == (0, 0):
|
||||
if not(allow_origin) and (x, y) == (0, 0):
|
||||
print "Error! Angle of (0, 0) computed!"
|
||||
return None
|
||||
return np.true_divide(np.arctan2(y, x), TAU)
|
||||
return
|
||||
return fdiv(np.arctan2(y, x), TAU)
|
||||
|
||||
def point_to_rgba(point):
|
||||
rev = point_to_rev(point, allow_origin = True)
|
||||
rgba = rev_to_rgba(rev)
|
||||
base_size = np.sqrt(point[0]**2 + point[1]**2)
|
||||
rescaled_size = np.sqrt(base_size/(base_size + 1))
|
||||
return rgba * rescaled_size
|
||||
|
||||
# Returns the value with the same fractional component as x, closest to m
|
||||
def resit_near(x, m):
|
||||
|
@ -232,7 +243,7 @@ def resit_near(x, m):
|
|||
def make_alpha_winder(func, start, end, num_checkpoints):
|
||||
check_points = [None for i in range(num_checkpoints)]
|
||||
check_points[0] = func(start)
|
||||
step_size = np.true_divide(end - start, num_checkpoints)
|
||||
step_size = fdiv(end - start, num_checkpoints)
|
||||
for i in range(num_checkpoints - 1):
|
||||
check_points[i + 1] = \
|
||||
resit_near(
|
||||
|
@ -284,7 +295,7 @@ class RectangleData():
|
|||
if dim == 0:
|
||||
return_data = [RectangleData(new_interval, y_interval) for new_interval in split_interval(x_interval)]
|
||||
elif dim == 1:
|
||||
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)]
|
||||
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)[::-1]]
|
||||
else:
|
||||
print "RectangleData.splits_on_dim passed illegitimate dimension!"
|
||||
|
||||
|
@ -317,6 +328,8 @@ def plane_func_from_complex_func(f):
|
|||
def point_func_from_complex_func(f):
|
||||
return lambda (x, y, z): complex_to_R3(f(complex(x, y)))
|
||||
|
||||
test_map_func = point_func_from_complex_func(lambda c: c**2)
|
||||
|
||||
empty_animation = Animation(Mobject(), run_time = 0)
|
||||
def EmptyAnimation():
|
||||
return empty_animation
|
||||
|
@ -329,14 +342,15 @@ class WalkerAnimation(Animation):
|
|||
"coords_to_point" : None
|
||||
}
|
||||
|
||||
def __init__(self, walk_func, rev_func, coords_to_point, scale_factor, **kwargs):
|
||||
def __init__(self, walk_func, rev_func, coords_to_point, show_arrows = True, **kwargs):
|
||||
self.walk_func = walk_func
|
||||
self.rev_func = rev_func
|
||||
self.coords_to_point = coords_to_point
|
||||
self.compound_walker = VGroup()
|
||||
self.compound_walker.walker = PiCreature(color = RED)
|
||||
self.compound_walker.walker.scale(scale_factor)
|
||||
self.compound_walker.arrow = Arrow(ORIGIN, RIGHT) #, buff = 0)
|
||||
base_walker = Dot().scale(5) # PiCreature().scale(0.8) #
|
||||
self.compound_walker.walker = base_walker.scale(0.35).set_stroke(BLACK, 1.5) #PiCreature()
|
||||
if show_arrows:
|
||||
self.compound_walker.arrow = Arrow(ORIGIN, 0.5 * RIGHT, buff = 0).set_stroke(BLACK, 1.5)
|
||||
self.compound_walker.digest_mobject_attrs()
|
||||
Animation.__init__(self, self.compound_walker, **kwargs)
|
||||
|
||||
|
@ -348,21 +362,22 @@ class WalkerAnimation(Animation):
|
|||
Animation.update_mobject(self, alpha)
|
||||
cur_x, cur_y = cur_coords = self.walk_func(alpha)
|
||||
cur_point = self.coords_to_point(cur_x, cur_y)
|
||||
self.mobject.walker.move_to(cur_point)
|
||||
self.mobject.shift(cur_point - self.mobject.walker.get_center())
|
||||
rev = self.rev_func(cur_coords)
|
||||
self.mobject.walker.set_color(rev_to_color(rev))
|
||||
self.mobject.arrow.set_color(rev_to_color(rev))
|
||||
self.mobject.arrow.rotate(
|
||||
rev * TAU,
|
||||
about_point = ORIGIN #self.mobject.arrow.get_start()
|
||||
)
|
||||
self.mobject.walker.set_fill(rev_to_color(rev))
|
||||
if show_arrows:
|
||||
self.mobject.arrow.set_fill(rev_to_color(rev))
|
||||
self.mobject.arrow.rotate(
|
||||
rev * TAU,
|
||||
about_point = self.mobject.arrow.get_start()
|
||||
)
|
||||
|
||||
def walker_animation_with_display(
|
||||
walk_func,
|
||||
rev_func,
|
||||
coords_to_point,
|
||||
number_update_func = None,
|
||||
scale_factor = 0.35,
|
||||
number_update_func = None,
|
||||
show_arrows = True,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
|
@ -370,13 +385,19 @@ def walker_animation_with_display(
|
|||
walk_func = walk_func,
|
||||
rev_func = rev_func,
|
||||
coords_to_point = coords_to_point,
|
||||
scale_factor = scale_factor,
|
||||
show_arrows = show_arrows,
|
||||
**kwargs)
|
||||
walker = walker_anim.compound_walker.walker
|
||||
|
||||
if number_update_func != None:
|
||||
display = DecimalNumber(0, include_background_rectangle = True)
|
||||
displaycement = scale_factor * DOWN # How about that pun, eh?
|
||||
display = DecimalNumber(0,
|
||||
num_decimal_points = 1,
|
||||
fill_color = WHITE,
|
||||
include_background_rectangle = True)
|
||||
display.background_rectangle.fill_opacity = 0.5
|
||||
display.background_rectangle.fill_color = GREY
|
||||
display.background_rectangle.scale(1.2)
|
||||
displaycement = 0.5 * DOWN # How about that pun, eh?
|
||||
display.move_to(walker.get_center() + displaycement)
|
||||
display_anim = ChangingDecimal(display,
|
||||
number_update_func,
|
||||
|
@ -393,6 +414,7 @@ def LinearWalker(
|
|||
coords_to_point,
|
||||
rev_func,
|
||||
number_update_func = None,
|
||||
show_arrows = True,
|
||||
**kwargs
|
||||
):
|
||||
walk_func = lambda alpha : interpolate(start_coords, end_coords, alpha)
|
||||
|
@ -401,23 +423,45 @@ def LinearWalker(
|
|||
coords_to_point = coords_to_point,
|
||||
rev_func = rev_func,
|
||||
number_update_func = number_update_func,
|
||||
show_arrows = show_arrows,
|
||||
**kwargs)
|
||||
|
||||
class PiWalker(Scene):
|
||||
class ColorMappedByFuncScene(Scene):
|
||||
CONFIG = {
|
||||
"func" : plane_func_from_complex_func(lambda c : c**2),
|
||||
"walk_coords" : [],
|
||||
"step_run_time" : 1
|
||||
"func" : lambda p : p,
|
||||
"num_plane" : NumberPlane(),
|
||||
"display_output_color_map" : False
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
display_func = self.func if not self.display_output_color_map else lambda p : p
|
||||
|
||||
self.num_plane.fade()
|
||||
self.add(self.num_plane)
|
||||
self.camera.set_background_from_func(
|
||||
lambda (x, y): point_to_rgba(
|
||||
display_func(
|
||||
# Should be self.num_plane.point_to_coords_cheap(np.array([x, y, 0])),
|
||||
# but for cheapness, we'll go with just (x, y), having never altered
|
||||
# any num_plane's from default settings so far
|
||||
(x, y)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
class PiWalker(ColorMappedByFuncScene):
|
||||
CONFIG = {
|
||||
"walk_coords" : [],
|
||||
"step_run_time" : 1,
|
||||
"show_arrows" : True
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
ColorMappedByFuncScene.construct(self)
|
||||
num_plane = self.num_plane
|
||||
|
||||
rev_func = lambda p : point_to_rev(self.func(p))
|
||||
|
||||
num_plane = NumberPlane()
|
||||
num_plane.fade()
|
||||
self.add(num_plane)
|
||||
|
||||
walk_coords = self.walk_coords
|
||||
for i in range(len(walk_coords)):
|
||||
start_x, start_y = start_coords = walk_coords[i]
|
||||
|
@ -425,19 +469,19 @@ class PiWalker(Scene):
|
|||
end_x, end_y = end_coords = walk_coords[(i + 1) % len(walk_coords)]
|
||||
end_point = num_plane.coords_to_point(end_x, end_y)
|
||||
self.play(
|
||||
ShowCreation(Line(start_point, end_point), rate_func = None),
|
||||
LinearWalker(
|
||||
start_coords = start_coords,
|
||||
end_coords = end_coords,
|
||||
coords_to_point = num_plane.coords_to_point,
|
||||
rev_func = rev_func,
|
||||
remover = (i < len(walk_coords) - 1)
|
||||
remover = (i < len(walk_coords) - 1),
|
||||
show_arrows = self.show_arrows
|
||||
),
|
||||
ShowCreation(Line(start_point, end_point), rate_func = None),
|
||||
run_time = self.step_run_time)
|
||||
|
||||
# TODO: Allow smooth paths instead of breaking them up into lines, and
|
||||
# use point_from_proportion to get points along the way
|
||||
|
||||
|
||||
self.wait()
|
||||
|
||||
|
@ -447,6 +491,7 @@ class PiWalkerRect(PiWalker):
|
|||
"start_y" : 1,
|
||||
"walk_width" : 2,
|
||||
"walk_height" : 2,
|
||||
"func" : plane_func_from_complex_func(lambda c: c**2)
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
|
@ -473,23 +518,22 @@ class PiWalkerCircle(PiWalker):
|
|||
# TODO: Perhaps restructure this to avoid using AnimationGroup, and instead
|
||||
# use lists of animations or lists or other such data, to be merged and processed into parallel
|
||||
# animations later
|
||||
class EquationSolver2d(Scene):
|
||||
class EquationSolver2d(ColorMappedByFuncScene):
|
||||
CONFIG = {
|
||||
"func" : plane_poly_with_roots((1, 2), (-1, 3)),
|
||||
"initial_lower_x" : -5.1,
|
||||
"initial_upper_x" : 5.1,
|
||||
"initial_lower_y" : -3.1,
|
||||
"initial_upper_y" : 3.1,
|
||||
"num_iterations" : 5,
|
||||
"num_checkpoints" : 10,
|
||||
"display_in_parallel" : True
|
||||
# TODO: Consider adding a "find_all_roots" flag, which could be turned off
|
||||
# to only explore one of the two candidate subrectangles when both are viable
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
num_plane = NumberPlane()
|
||||
num_plane.fade()
|
||||
self.add(num_plane)
|
||||
ColorMappedByFuncScene.construct(self)
|
||||
num_plane = self.num_plane
|
||||
|
||||
rev_func = lambda p : point_to_rev(self.func(p))
|
||||
clockwise_rev_func = lambda p : -rev_func(p)
|
||||
|
@ -500,13 +544,14 @@ class EquationSolver2d(Scene):
|
|||
if cur_depth >= self.num_iterations:
|
||||
return EmptyAnimation()
|
||||
|
||||
def draw_line_return_wind(start, end, start_wind):
|
||||
def draw_line_return_wind(start, end, start_wind, should_linger = False):
|
||||
alpha_winder = make_alpha_winder(clockwise_rev_func, start, end, self.num_checkpoints)
|
||||
a0 = alpha_winder(0)
|
||||
rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind
|
||||
thin_line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end),
|
||||
stroke_width = 2,
|
||||
color = RED)
|
||||
|
||||
walker_anim = LinearWalker(
|
||||
start_coords = start,
|
||||
end_coords = end,
|
||||
|
@ -515,12 +560,17 @@ class EquationSolver2d(Scene):
|
|||
number_update_func = rebased_winder,
|
||||
remover = True
|
||||
)
|
||||
|
||||
if should_linger: # Do we need an "and not self.display_in_parallel" here?
|
||||
rate_func = lingering
|
||||
else:
|
||||
rate_func = None
|
||||
|
||||
line_draw_anim = AnimationGroup(
|
||||
ShowCreation(thin_line),
|
||||
walker_anim,
|
||||
rate_func = None)
|
||||
anim = line_draw_anim
|
||||
return (anim, rebased_winder(1))
|
||||
rate_func = rate_func)
|
||||
return (line_draw_anim, rebased_winder(1))
|
||||
|
||||
wind_so_far = 0
|
||||
anim = EmptyAnimation()
|
||||
|
@ -530,8 +580,9 @@ class EquationSolver2d(Scene):
|
|||
rect.get_bottom(),
|
||||
rect.get_left()
|
||||
]
|
||||
for (start, end) in sides:
|
||||
(next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far)
|
||||
for (i, (start, end)) in enumerate(sides):
|
||||
(next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far,
|
||||
should_linger = i == len(sides) - 1)
|
||||
anim = Succession(anim, next_anim)
|
||||
|
||||
total_wind = round(wind_so_far)
|
||||
|
@ -561,12 +612,17 @@ class EquationSolver2d(Scene):
|
|||
]
|
||||
mid_line_coords = rect.split_line_on_dim(dim_to_split)
|
||||
mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords]
|
||||
mid_line = DashedLine(*mid_line_points)
|
||||
mid_line = DashedLine(*mid_line_points) # TODO: Have this match rectangle line style, apart from dashes and thin-ness?
|
||||
if len(sub_anims) > 0:
|
||||
if self.display_in_parallel:
|
||||
recursive_anim = AnimationGroup(*sub_anims)
|
||||
else:
|
||||
recursive_anim = Succession(*sub_anims)
|
||||
else:
|
||||
recursive_anim = empty_animation # Have to do this because Succession doesn't currently handle empty animations
|
||||
return Succession(anim,
|
||||
ShowCreation(mid_line),
|
||||
# FadeOut(mid_line), # TODO: Can change timing so this fades out at just the time it would be overdrawn
|
||||
# TODO: Investigate weirdness with changing z buffer order on mid_line vs. rectangle lines
|
||||
AnimationGroup(*sub_anims)
|
||||
ShowCreation(mid_line),
|
||||
recursive_anim
|
||||
)
|
||||
|
||||
lower_x = self.initial_lower_x
|
||||
|
@ -613,7 +669,7 @@ class LinePulser(ContinualAnimation):
|
|||
end = self.line.get_end()
|
||||
for i in range(self.num_bullets):
|
||||
position = interpolate(start, end,
|
||||
np.true_divide((i + alpha),(self.num_bullets)))
|
||||
fdiv((i + alpha),(self.num_bullets)))
|
||||
self.bullets[i].move_to(position)
|
||||
if self.output_func:
|
||||
position_2d = (position[0], position[1])
|
||||
|
@ -635,7 +691,7 @@ class ArrowCircleTest(Scene):
|
|||
return x
|
||||
|
||||
num_arrows = 8 * 3
|
||||
arrows = [rev_rotate(base_arrow.copy(), (np.true_divide(i, num_arrows))) for i in range(num_arrows)]
|
||||
arrows = [rev_rotate(base_arrow.copy(), (fdiv(i, num_arrows))) for i in range(num_arrows)]
|
||||
arrows_vgroup = VGroup(*arrows)
|
||||
|
||||
self.play(ShowCreation(arrows_vgroup), run_time = 2.5, rate_func = None)
|
||||
|
@ -690,7 +746,7 @@ class OdometerScene(Scene):
|
|||
dashed_line.rotate(-self.dashed_line_angle * TAU, about_point = ORIGIN)
|
||||
self.add(dashed_line)
|
||||
|
||||
num_display = DecimalNumber(0, include_background_rectangle = True)
|
||||
num_display = DecimalNumber(0, include_background_rectangle = False).set_stroke(1)
|
||||
num_display.move_to(2 * DOWN)
|
||||
|
||||
display_val_bias = 0
|
||||
|
@ -1040,7 +1096,8 @@ class LoopSplitSceneMapped(LoopSplitScene):
|
|||
class FundThmAlg(EquationSolver2d):
|
||||
CONFIG = {
|
||||
"func" : plane_poly_with_roots((1, 2), (-1, 1.5), (-1, 1.5)),
|
||||
"num_iterations" : 10,
|
||||
"num_iterations" : 4,
|
||||
"display_in_parallel" : False
|
||||
}
|
||||
|
||||
# TODO: Borsuk-Ulam visuals
|
||||
|
@ -1085,6 +1142,10 @@ class DiffOdometer(OdometerScene):
|
|||
|
||||
# Writing new Pi walker scenes by parametrizing general template
|
||||
|
||||
# Domain coloring scenes by parametrizing general template
|
||||
|
||||
# (All the above are basically trivial tinkering at this point)
|
||||
|
||||
# ----
|
||||
|
||||
# Pi creature emotion stuff
|
||||
|
@ -1093,9 +1154,16 @@ class DiffOdometer(OdometerScene):
|
|||
|
||||
# Borsuk-Ulam visuals
|
||||
|
||||
# Domain coloring
|
||||
# TODO: Add to camera an option for lower-quality (faster-rendered) background than pixel_array,
|
||||
# helpful for previews
|
||||
|
||||
# TODO: Add to camera an option for low-quality background than other rendering, helpful
|
||||
# for previews
|
||||
####################
|
||||
|
||||
# FIN
|
||||
class MapPiWalkerRect(PiWalkerRect):
|
||||
CONFIG = {
|
||||
"camera_class" : MappingCamera,
|
||||
"camera_config" : {"mapping_func" : test_map_func},
|
||||
"display_output_color_map" : True
|
||||
}
|
||||
|
||||
# FIN
|
|
@ -411,9 +411,6 @@ class Succession(Animation):
|
|||
else:
|
||||
run_time = sum(self.run_times)
|
||||
self.num_anims = len(animations)
|
||||
if self.num_anims == 0:
|
||||
# TODO: Handle this; it should be easy enough, but requires some special cases below
|
||||
print "Warning! Successions with zero animations are not currently handled!"
|
||||
self.animations = animations
|
||||
#Have to keep track of this run_time, because Scene.play
|
||||
#might very well mess with it.
|
||||
|
@ -422,7 +419,7 @@ class Succession(Animation):
|
|||
# critical_alphas[i] is the start alpha of self.animations[i]
|
||||
# critical_alphas[i + 1] is the end alpha of self.animations[i]
|
||||
critical_times = np.concatenate(([0], np.cumsum(self.run_times)))
|
||||
self.critical_alphas = map (lambda x : np.true_divide(x, run_time), critical_times)
|
||||
self.critical_alphas = map (lambda x : np.true_divide(x, run_time), critical_times) if self.num_anims > 0 else [0.0]
|
||||
|
||||
# self.scene_mobjects_at_time[i] is the scene's mobjects at start of self.animations[i]
|
||||
# self.scene_mobjects_at_time[i + 1] is the scene mobjects at end of self.animations[i]
|
||||
|
@ -433,9 +430,12 @@ class Succession(Animation):
|
|||
self.animations[i].clean_up(self.scene_mobjects_at_time[i + 1])
|
||||
|
||||
self.current_alpha = 0
|
||||
self.current_anim_index = 0 #TODO: What if self.num_anims == 0?
|
||||
self.mobject = self.scene_mobjects_at_time[0]
|
||||
self.mobject.add(self.animations[0].mobject)
|
||||
self.current_anim_index = 0 # If self.num_anims == 0, this is an invalid index, but so it goes
|
||||
if self.num_anims > 0:
|
||||
self.mobject = self.scene_mobjects_at_time[0]
|
||||
self.mobject.add(self.animations[0].mobject)
|
||||
else:
|
||||
self.mobject = Group()
|
||||
|
||||
Animation.__init__(self, self.mobject, run_time = run_time, **kwargs)
|
||||
|
||||
|
@ -446,14 +446,17 @@ class Succession(Animation):
|
|||
def jump_to_start_of_anim(self, index):
|
||||
if index != self.current_anim_index:
|
||||
self.mobject.remove(*self.mobject.submobjects) # Should probably have a cleaner "remove_all" method...
|
||||
self.mobject.add(self.animations[index].mobject)
|
||||
for m in self.scene_mobjects_at_time[index].submobjects:
|
||||
self.mobject.add(m)
|
||||
self.mobject.add(self.animations[index].mobject)
|
||||
|
||||
self.current_anim_index = index
|
||||
self.current_alpha = self.critical_alphas[index]
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
if self.num_anims == 0:
|
||||
return
|
||||
|
||||
i = 0
|
||||
while self.critical_alphas[i + 1] < alpha:
|
||||
i = i + 1
|
||||
|
|
120
camera/camera.py
120
camera/camera.py
|
@ -13,7 +13,14 @@ class Camera(object):
|
|||
CONFIG = {
|
||||
"background_image" : None,
|
||||
"pixel_shape" : (DEFAULT_HEIGHT, DEFAULT_WIDTH),
|
||||
#this will be resized to match pixel_shape
|
||||
# Note 1: space_shape will be resized to match pixel_shape
|
||||
#
|
||||
# Note 2: While pixel_shape indicates the actual full height
|
||||
# and width of the pixel array, space_shape indicates only
|
||||
# half the height and half the width of space (extending from
|
||||
# -space_height to +space_height vertically and from
|
||||
# -space_widtdh to +space_width horizontally)
|
||||
# TODO: Rename these to SPACE_X_RADIUS, SPACE_Y_RADIUS
|
||||
"space_shape" : (SPACE_HEIGHT, SPACE_WIDTH),
|
||||
"space_center" : ORIGIN,
|
||||
"background_color" : BLACK,
|
||||
|
@ -22,12 +29,13 @@ class Camera(object):
|
|||
"max_allowable_norm" : 2*SPACE_WIDTH,
|
||||
"image_mode" : "RGBA",
|
||||
"n_rgb_coords" : 4,
|
||||
"background_alpha" : 0, #Out of 255
|
||||
"background_alpha" : 0, #Out of color_max_val
|
||||
"pixel_array_dtype" : 'uint8'
|
||||
}
|
||||
|
||||
def __init__(self, background = None, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
self.color_max_val = np.iinfo(self.pixel_array_dtype).max
|
||||
self.init_background()
|
||||
self.resize_space_shape()
|
||||
self.reset()
|
||||
|
@ -75,11 +83,39 @@ class Camera(object):
|
|||
def get_pixel_array(self):
|
||||
return self.pixel_array
|
||||
|
||||
def set_pixel_array(self, pixel_array):
|
||||
self.pixel_array = np.array(pixel_array)
|
||||
def convert_pixel_array(self, pixel_array, convert_from_floats = False):
|
||||
retval = np.array(pixel_array)
|
||||
if convert_from_floats:
|
||||
retval = np.apply_along_axis(
|
||||
lambda f : (f * self.color_max_val).astype(self.pixel_array_dtype),
|
||||
2,
|
||||
retval)
|
||||
return retval
|
||||
|
||||
def set_background(self, pixel_array):
|
||||
self.background = np.array(pixel_array)
|
||||
def set_pixel_array(self, pixel_array, convert_from_floats = False):
|
||||
self.pixel_array = self.convert_pixel_array(pixel_array, convert_from_floats)
|
||||
|
||||
def set_background(self, pixel_array, convert_from_floats = False):
|
||||
self.background = self.convert_pixel_array(pixel_array, convert_from_floats)
|
||||
|
||||
def set_background_from_func(self, coords_to_colors_func):
|
||||
"""
|
||||
Sets background by using coords_to_colors_func to determine each pixel's color. Each input
|
||||
to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
|
||||
pixel coordinates), and each output is expected to be an RGBA array of 4 floats.
|
||||
"""
|
||||
|
||||
print "Starting set_background_from_func"
|
||||
|
||||
coords = self.get_coords_of_all_pixels()
|
||||
new_background = np.apply_along_axis(
|
||||
coords_to_colors_func,
|
||||
2,
|
||||
coords
|
||||
)
|
||||
self.set_background(new_background, convert_from_floats = True)
|
||||
|
||||
print "Ending set_background_from_func"
|
||||
|
||||
def reset(self):
|
||||
self.set_pixel_array(self.background)
|
||||
|
@ -103,6 +139,7 @@ class Camera(object):
|
|||
self, mobjects,
|
||||
include_submobjects = True,
|
||||
excluded_mobjects = None,
|
||||
z_buff_func = lambda m : m.get_center()[2]
|
||||
):
|
||||
if include_submobjects:
|
||||
mobjects = self.extract_mobject_family_members(
|
||||
|
@ -113,7 +150,8 @@ class Camera(object):
|
|||
excluded_mobjects
|
||||
)
|
||||
mobjects = list_difference_update(mobjects, all_excluded)
|
||||
return mobjects
|
||||
|
||||
return sorted(mobjects, lambda a, b: cmp(z_buff_func(a), z_buff_func(b)))
|
||||
|
||||
def capture_mobject(self, mobject, **kwargs):
|
||||
return self.capture_mobjects([mobject], **kwargs)
|
||||
|
@ -173,7 +211,7 @@ class Camera(object):
|
|||
)
|
||||
fill = aggdraw.Brush(
|
||||
self.color_to_hex_l(self.get_fill_color(vmobject)),
|
||||
opacity = int(255*vmobject.get_fill_opacity())
|
||||
opacity = int(self.color_max_val*vmobject.get_fill_opacity())
|
||||
)
|
||||
return (pen, fill)
|
||||
|
||||
|
@ -222,7 +260,7 @@ class Camera(object):
|
|||
)
|
||||
rgba_len = self.pixel_array.shape[2]
|
||||
|
||||
rgbas = (255*rgbas).astype('uint8')
|
||||
rgbas = (self.color_max_val*rgbas).astype(self.pixel_array_dtype)
|
||||
target_len = len(pixel_coords)
|
||||
factor = target_len/len(rgbas)
|
||||
rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len))
|
||||
|
@ -311,7 +349,7 @@ class Camera(object):
|
|||
|
||||
def overlay_rgba_array(self, arr):
|
||||
# """ Overlays arr onto self.pixel_array with relevant alphas"""
|
||||
bg, fg = self.pixel_array/255.0, arr/255.0
|
||||
bg, fg = fdiv(self.pixel_array, self.color_max_val), fdiv(arr, self.color_max_val)
|
||||
bga, fga = [arr[:,:,3:] for arr in bg, fg]
|
||||
alpha_sum = fga + (1-fga)*bga
|
||||
with np.errstate(divide = 'ignore', invalid='ignore'):
|
||||
|
@ -320,7 +358,7 @@ class Camera(object):
|
|||
np.divide(bg[:,:,:3]*(1-fga)*bga, alpha_sum),
|
||||
])
|
||||
bg[:,:,3:] = 1 - (1 - bga)*(1 - fga)
|
||||
self.pixel_array = (255*bg).astype(self.pixel_array_dtype)
|
||||
self.pixel_array = (self.color_max_val*bg).astype(self.pixel_array_dtype)
|
||||
|
||||
def align_points_to_camera(self, points):
|
||||
## This is where projection should live
|
||||
|
@ -382,37 +420,29 @@ class Camera(object):
|
|||
size = pixel_coords.size
|
||||
return pixel_coords.reshape((size/2, 2))
|
||||
|
||||
def get_points_of_all_pixels(self):
|
||||
"""
|
||||
Returns an array a such that a[i, j] gives the spatial
|
||||
coordinates associated with the pixel self.pixel_array[i, j]
|
||||
"""
|
||||
shape = self.pixel_array.shape
|
||||
indices = np.indices(shape[:2], dtype = 'float64')
|
||||
all_point_coords = np.zeros((shape[0], shape[1], 3))
|
||||
for i, space_dim in enumerate([SPACE_HEIGHT, SPACE_WIDTH]):
|
||||
all_point_coords[:,:,i] = \
|
||||
indices[i,:,:]*2*space_dim/shape[i] - space_dim
|
||||
return all_point_coords
|
||||
def get_coords_of_all_pixels(self):
|
||||
# These are in x, y order, to help me keep things straight
|
||||
full_space_dims = np.array(self.space_shape)[::-1] * 2
|
||||
full_pixel_dims = np.array(self.pixel_shape)[::-1]
|
||||
|
||||
def set_background_by_color_function(self, point_to_rgba_func):
|
||||
"""
|
||||
point_to_rgba_func should take in a point in R^2, an array
|
||||
of two floats, and output a four element array representing
|
||||
rgba values, all between 0 and 1.
|
||||
"""
|
||||
# These are addressed in the same y, x order as in pixel_array, but the values in them
|
||||
# are listed in x, y order
|
||||
uncentered_pixel_coords = np.indices(self.pixel_shape)[::-1].transpose(1, 2, 0)
|
||||
uncentered_space_coords = fdiv(
|
||||
uncentered_pixel_coords * full_space_dims,
|
||||
full_pixel_dims)
|
||||
# Could structure above line's computation slightly differently, but figured (without much
|
||||
# thought) multiplying by space_shape first, THEN dividing by pixel_shape, is probably
|
||||
# better than the other order, for avoiding underflow quantization in the division (whereas
|
||||
# overflow is unlikely to be a problem)
|
||||
|
||||
# point_to_rgba = lambda p : [1, 1, 0, 0]
|
||||
def float_rgba_to_int_rgba(rgba):
|
||||
return (255*np.array(rgba)).astype(self.pixel_array_dtype)
|
||||
centered_space_coords = (uncentered_space_coords - fdiv(full_space_dims, 2))
|
||||
|
||||
points_of_all_pixels = self.get_points_of_all_pixels()
|
||||
self.set_background(np.apply_along_axis(
|
||||
lambda p : float_rgba_to_int_rgba(point_to_rgba_func(p)),
|
||||
2, points_of_all_pixels
|
||||
))
|
||||
self.reset() # Perhaps this really belongs in set_background?
|
||||
# Have to also flip the y coordinates to account for pixel array being listed in
|
||||
# top-to-bottom order, opposite of screen coordinate convention
|
||||
centered_space_coords = centered_space_coords * (1, -1)
|
||||
|
||||
return centered_space_coords
|
||||
|
||||
class MovingCamera(Camera):
|
||||
"""
|
||||
|
@ -497,20 +527,24 @@ class MultiCamera(Camera):
|
|||
shifted_camera.start_x:shifted_camera.end_x] \
|
||||
= shifted_camera.camera.pixel_array
|
||||
|
||||
def set_background(self, pixel_array):
|
||||
def set_background(self, pixel_array, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_background(
|
||||
pixel_array[
|
||||
shifted_camera.start_y:shifted_camera.end_y,
|
||||
shifted_camera.start_x:shifted_camera.end_x])
|
||||
shifted_camera.start_x:shifted_camera.end_x],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def set_pixel_array(self, pixel_array):
|
||||
Camera.set_pixel_array(self, pixel_array)
|
||||
def set_pixel_array(self, pixel_array, **kwargs):
|
||||
Camera.set_pixel_array(self, pixel_array, **kwargs)
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_pixel_array(
|
||||
pixel_array[
|
||||
shifted_camera.start_y:shifted_camera.end_y,
|
||||
shifted_camera.start_x:shifted_camera.end_x])
|
||||
shifted_camera.start_x:shifted_camera.end_x],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def init_background(self):
|
||||
Camera.init_background(self)
|
||||
|
|
10
helpers.py
10
helpers.py
|
@ -552,6 +552,13 @@ def squish_rate_func(func, a = 0.4, b = 0.6):
|
|||
return func((t-a)/(b-a))
|
||||
return result
|
||||
|
||||
# Stylistically, should this take parameters (with default values)?
|
||||
# Ultimately, the functionality is entirely subsumed by squish_rate_func,
|
||||
# but it may be useful to have a nice name for with nice default params for
|
||||
# "lingering", different from squish_rate_func's default params
|
||||
def lingering(t):
|
||||
return squish_rate_func(lambda t: t, 0, 0.8)(t)
|
||||
|
||||
### Functional Functions ###
|
||||
|
||||
def composition(func_list):
|
||||
|
@ -641,3 +648,6 @@ class DictAsObject(object):
|
|||
def __init__(self, dict):
|
||||
self.__dict__ = dict
|
||||
|
||||
# Just to have a less heavyweight name for this extremely common operation
|
||||
def fdiv(a, b):
|
||||
return np.true_divide(a,b)
|
||||
|
|
|
@ -321,6 +321,17 @@ class NumberPlane(VMobject):
|
|||
y = new_point[1]/self.get_y_unit_size()
|
||||
return x, y
|
||||
|
||||
# Does not recompute center, unit_sizes for each call; useful for
|
||||
# iterating over large lists of points, but does assume these
|
||||
# attributes are kept accurate. (Could alternatively have a method
|
||||
# which returns a function dynamically created after a single
|
||||
# call to each of get_center(), get_x_unit_size(), etc.)
|
||||
def point_to_coords_cheap(self, point):
|
||||
new_point = point - self.center_point
|
||||
x = new_point[0]/self.x_unit_size
|
||||
y = new_point[1]/self.y_unit_size
|
||||
return x, y
|
||||
|
||||
def get_x_unit_size(self):
|
||||
return self.axes.get_width() / (2.0*self.x_radius)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue