Merge pull request #129 from 3b1b/uncertainty

Uncertainty
This commit is contained in:
Grant Sanderson 2018-02-16 12:16:59 -08:00 committed by GitHub
commit d53412d738
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1511 additions and 201 deletions

View file

@ -32,6 +32,7 @@ from topics.graph_scene import *
USE_ALMOST_FOURIER_BY_DEFAULT = True USE_ALMOST_FOURIER_BY_DEFAULT = True
NUM_SAMPLES_FOR_FFT = 1000 NUM_SAMPLES_FOR_FFT = 1000
DEFAULT_COMPLEX_TO_REAL_FUNC = lambda z : z.real
def get_fourier_graph( def get_fourier_graph(
@ -53,7 +54,7 @@ def get_fourier_graph(
graph = VMobject() graph = VMobject()
graph.set_points_smoothly([ graph.set_points_smoothly([
axes.coords_to_point( axes.coords_to_point(
x, 200.0*complex_to_real_func(y)/n_samples, x, complex_to_real_func(y)/n_samples,
) )
for x, y in zip(frequencies, fft_output[:n_samples//2]) for x, y in zip(frequencies, fft_output[:n_samples//2])
]) ])
@ -62,18 +63,17 @@ def get_fourier_graph(
def get_fourier_transform( def get_fourier_transform(
func, t_min, t_max, func, t_min, t_max,
complex_to_real_func = lambda z : z.real, complex_to_real_func = DEFAULT_COMPLEX_TO_REAL_FUNC,
use_almost_fourier = USE_ALMOST_FOURIER_BY_DEFAULT, use_almost_fourier = USE_ALMOST_FOURIER_BY_DEFAULT,
**kwargs ##Just eats these
): ):
scalar = 1./(t_max - t_min) if use_almost_fourier else 1.0 scalar = 1./(t_max - t_min) if use_almost_fourier else 1.0
def fourier_transform(f): def fourier_transform(f):
return scalar*scipy.integrate.quad( z = scalar*scipy.integrate.quad(
lambda t : complex_to_real_func( lambda t : func(t)*np.exp(complex(0, -TAU*f*t)),
# f(t) e^{-TAU*i*f*t}
func(t)*np.exp(complex(0, -TAU*f*t))
),
t_min, t_max t_min, t_max
)[0] )[0]
return complex_to_real_func(z)
return fourier_transform return fourier_transform
## ##
@ -939,11 +939,28 @@ class FourierMachineScene(Scene):
if not hasattr(self, "frequency_axes"): if not hasattr(self, "frequency_axes"):
self.get_frequency_axes() self.get_frequency_axes()
func = time_graph.underlying_function func = time_graph.underlying_function
t_min = self.time_axes.x_min t_axis = self.time_axes.x_axis
t_max = self.time_axes.x_max t_min = t_axis.point_to_number(time_graph.points[0])
t_max = t_axis.point_to_number(time_graph.points[-1])
f_max = self.frequency_axes.x_max
# result = get_fourier_graph(
# self.frequency_axes, func, t_min, t_max,
# **kwargs
# )
# too_far_right_point_indices = [
# i
# for i, point in enumerate(result.points)
# if self.frequency_axes.x_axis.point_to_number(point) > f_max
# ]
# if too_far_right_point_indices:
# i = min(too_far_right_point_indices)
# prop = float(i)/len(result.points)
# result.pointwise_become_partial(result, 0, prop)
# return result
return self.frequency_axes.get_graph( return self.frequency_axes.get_graph(
get_fourier_transform(func, t_min, t_max, **kwargs), get_fourier_transform(func, t_min, t_max, **kwargs),
color = self.center_of_mass_color, color = self.center_of_mass_color,
**kwargs
) )
def get_polarized_mobject(self, mobject, freq = 1.0): def get_polarized_mobject(self, mobject, freq = 1.0):
@ -999,7 +1016,8 @@ class FourierMachineScene(Scene):
vector = Vector(UP, color = WHITE) vector = Vector(UP, color = WHITE)
graph_copy = graph.copy() graph_copy = graph.copy()
x_axis = self.time_axes.x_axis x_axis = self.time_axes.x_axis
x_min, x_max = x_axis.x_min, x_axis.x_max x_min = x_axis.point_to_number(graph.points[0])
x_max = x_axis.point_to_number(graph.points[-1])
def update_vector(vector, alpha): def update_vector(vector, alpha):
x = interpolate(x_min, x_max, alpha) x = interpolate(x_min, x_max, alpha)
vector.put_start_and_end_on( vector.put_start_and_end_on(
@ -1016,7 +1034,13 @@ class FourierMachineScene(Scene):
origin = self.circle_plane.coords_to_point(0, 0) origin = self.circle_plane.coords_to_point(0, 0)
graph_copy = polarized_graph.copy() graph_copy = polarized_graph.copy()
def update_vector(vector, alpha): def update_vector(vector, alpha):
point = graph_copy.point_from_proportion(alpha) # Not sure why this is needed, but without smoothing
# out the alpha like this, the vector would occasionally
# jump around
point = center_of_mass([
graph_copy.point_from_proportion(alpha+d)
for d in np.linspace(-0.001, 0.001, 5)
])
vector.put_start_and_end_on_with_projection(origin, point) vector.put_start_and_end_on_with_projection(origin, point)
return vector return vector
return UpdateFromAlphaFunc(vector, update_vector, **config) return UpdateFromAlphaFunc(vector, update_vector, **config)
@ -1299,6 +1323,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
CONFIG = { CONFIG = {
"initial_winding_frequency" : 3.0, "initial_winding_frequency" : 3.0,
"center_of_mass_color" : RED, "center_of_mass_color" : RED,
"center_of_mass_multiple" : 1,
} }
def construct(self): def construct(self):
self.remove(self.pi_creature) self.remove(self.pi_creature)
@ -1581,10 +1606,11 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
def get_pol_graph_center_of_mass(self): def get_pol_graph_center_of_mass(self):
pg = self.graph.polarized_mobject pg = self.graph.polarized_mobject
result = center_of_mass([ result = center_of_mass(pg.get_anchors())
pg.point_from_proportion(alpha) if self.center_of_mass_multiple != 1:
for alpha in np.linspace(0, 1, 1000) mult = self.center_of_mass_multiple
]) origin = self.circle_plane.coords_to_point(0, 0)
result = mult*(result - origin) + origin
return result return result
def generate_fourier_dot_transform(self, fourier_graph): def generate_fourier_dot_transform(self, fourier_graph):
@ -1626,7 +1652,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
def change_frequency(self, new_freq, **kwargs): def change_frequency(self, new_freq, **kwargs):
kwargs["run_time"] = kwargs.get("run_time", 3) kwargs["run_time"] = kwargs.get("run_time", 3)
kwargs["rate_func"] = kwargs.get( rate_func = kwargs.pop(
"rate_func", bezier([0, 0, 1, 1]) "rate_func", bezier([0, 0, 1, 1])
) )
added_anims = kwargs.get("added_anims", []) added_anims = kwargs.get("added_anims", [])
@ -1645,6 +1671,8 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
anims.append(self.fourier_graph_dot_anim) anims.append(self.fourier_graph_dot_anim)
if hasattr(self, "fourier_graph_drawing_update_anim"): if hasattr(self, "fourier_graph_drawing_update_anim"):
anims.append(self.fourier_graph_drawing_update_anim) anims.append(self.fourier_graph_drawing_update_anim)
for anim in anims:
anim.rate_func = rate_func
anims += added_anims anims += added_anims
self.play(*anims, **kwargs) self.play(*anims, **kwargs)

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@ class Camera(object):
"max_allowable_norm" : 2*SPACE_WIDTH, "max_allowable_norm" : 2*SPACE_WIDTH,
"image_mode" : "RGBA", "image_mode" : "RGBA",
"n_rgb_coords" : 4, "n_rgb_coords" : 4,
"background_alpha" : 0, #Out of color_max_val "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.
@ -40,7 +40,7 @@ class Camera(object):
def __init__(self, background = None, **kwargs): def __init__(self, background = None, **kwargs):
digest_config(self, kwargs, locals()) digest_config(self, kwargs, locals())
self.color_max_val = np.iinfo(self.pixel_array_dtype).max self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
self.init_background() self.init_background()
self.resize_space_shape() self.resize_space_shape()
self.reset() self.reset()
@ -92,7 +92,7 @@ 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 : (f * self.color_max_val).astype(self.pixel_array_dtype), lambda f : (f * self.rgb_max_val).astype(self.pixel_array_dtype),
2, 2,
retval) retval)
return retval return retval
@ -177,29 +177,30 @@ class Camera(object):
def capture_mobjects(self, mobjects, **kwargs): def capture_mobjects(self, mobjects, **kwargs):
mobjects = self.get_mobjects_to_display(mobjects, **kwargs) mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
vmobjects = []
for mobject in mobjects: # Organize this list into batches of the same type, and
if isinstance(mobject, VMobject): # apply corresponding function to those batches
vmobjects.append(mobject) type_func_pairs = [
elif len(vmobjects) > 0: (VMobject, self.display_multiple_vectorized_mobjects),
self.display_multiple_vectorized_mobjects(vmobjects) (PMobject, self.display_multiple_point_cloud_mobjects),
vmobjects = [] (ImageMobject, self.display_multiple_image_mobjects),
(Mobject, lambda batch : batch), #Do nothing
if isinstance(mobject, PMobject): ]
self.display_point_cloud( def get_mobject_type(mobject):
mobject.points, mobject.rgbas, for mobject_type, func in type_func_pairs:
self.adjusted_thickness(mobject.stroke_width) if isinstance(mobject, mobject_type):
) return mobject_type
elif isinstance(mobject, ImageMobject): raise Exception(
self.display_image_mobject(mobject) "Trying to display something which is not of type Mobject"
elif isinstance(mobject, Mobject): )
pass #Remainder of loop will handle submobjects batch_type_pairs = batch_by_property(mobjects, get_mobject_type)
else:
raise Exception( #Display in these batches
"Unknown mobject type: " + mobject.__class__.__name__ for batch, batch_type in batch_type_pairs:
) #check what the type is, and call the appropriate function
#TODO, more? Call out if it's unknown? for mobject_type, func in type_func_pairs:
self.display_multiple_vectorized_mobjects(vmobjects) if batch_type == mobject_type:
func(batch)
## Methods associated with svg rendering ## Methods associated with svg rendering
@ -215,12 +216,12 @@ class Camera(object):
def display_multiple_vectorized_mobjects(self, vmobjects): def display_multiple_vectorized_mobjects(self, vmobjects):
if len(vmobjects) == 0: if len(vmobjects) == 0:
return return
batches = batch_by_property( batch_file_pairs = batch_by_property(
vmobjects, vmobjects,
lambda vm : vm.get_background_image_file() lambda vm : vm.get_background_image_file()
) )
for batch in batches: for batch, file_name in batch_file_pairs:
if batch[0].get_background_image_file(): if file_name:
self.display_multiple_background_colored_vmobject(batch) self.display_multiple_background_colored_vmobject(batch)
else: else:
self.display_multiple_non_background_colored_vmobjects(batch) self.display_multiple_non_background_colored_vmobjects(batch)
@ -252,7 +253,7 @@ class Camera(object):
stroke_hex = rgb_to_hex(stroke_rgb) stroke_hex = rgb_to_hex(stroke_rgb)
pen = aggdraw.Pen(stroke_hex, stroke_width) pen = aggdraw.Pen(stroke_hex, stroke_width)
fill_opacity = int(self.color_max_val*vmobject.get_fill_opacity()) fill_opacity = int(self.rgb_max_val*vmobject.get_fill_opacity())
if fill_opacity == 0: if fill_opacity == 0:
fill = None fill = None
else: else:
@ -305,13 +306,19 @@ class Camera(object):
def display_multiple_background_colored_vmobject(self, cvmobjects): def display_multiple_background_colored_vmobject(self, cvmobjects):
displayer = self.get_background_colored_vmobject_displayer() displayer = self.get_background_colored_vmobject_displayer()
cvmobject_pixel_array = displayer.display(*cvmobjects) cvmobject_pixel_array = displayer.display(*cvmobjects)
self.pixel_array[:,:] = np.maximum( self.overlay_rgba_array(cvmobject_pixel_array)
self.pixel_array, cvmobject_pixel_array
)
return self return self
## Methods for other rendering ## Methods for other rendering
def display_multiple_point_cloud_mobjects(self, pmobjects):
for pmobject in pmobjects:
self.display_point_cloud(
pmobject.points,
pmobject.rgbas,
self.adjusted_thickness(pmobject.stroke_width)
)
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
@ -322,7 +329,7 @@ class Camera(object):
) )
rgba_len = self.pixel_array.shape[2] rgba_len = self.pixel_array.shape[2]
rgbas = (self.color_max_val*rgbas).astype(self.pixel_array_dtype) rgbas = (self.rgb_max_val*rgbas).astype(self.pixel_array_dtype)
target_len = len(pixel_coords) target_len = len(pixel_coords)
factor = target_len/len(rgbas) factor = target_len/len(rgbas)
rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len)) rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len))
@ -342,6 +349,10 @@ class Camera(object):
new_pa[indices] = rgbas new_pa[indices] = rgbas
self.pixel_array = new_pa.reshape((ph, pw, rgba_len)) self.pixel_array = new_pa.reshape((ph, pw, rgba_len))
def display_multiple_image_mobjects(self, image_mobjects):
for image_mobject in image_mobjects:
self.display_image_mobject(image_mobject)
def display_image_mobject(self, image_mobject): def display_image_mobject(self, image_mobject):
corner_coords = self.points_to_pixel_coords(image_mobject.points) corner_coords = self.points_to_pixel_coords(image_mobject.points)
ul_coords, ur_coords, dl_coords = corner_coords ul_coords, ur_coords, dl_coords = corner_coords
@ -410,17 +421,23 @@ class Camera(object):
self.overlay_rgba_array(image) self.overlay_rgba_array(image)
def overlay_rgba_array(self, arr): def overlay_rgba_array(self, arr):
# """ Overlays arr onto self.pixel_array with relevant alphas""" fg = arr
bg, fg = fdiv(self.pixel_array, self.color_max_val), fdiv(arr, self.color_max_val) bg = self.pixel_array
bga, fga = [arr[:,:,3:] for arr in bg, fg] # rgba_max_val = self.rgb_max_val
alpha_sum = fga + (1-fga)*bga src_rgb, src_a, dst_rgb, dst_a = [
with np.errstate(divide = 'ignore', invalid='ignore'): a.astype(np.float32)/self.rgb_max_val
bg[:,:,:3] = reduce(op.add, [ for a in fg[...,:3], fg[...,3], bg[...,:3], bg[...,3]
np.divide(fg[:,:,:3]*fga, alpha_sum), ]
np.divide(bg[:,:,:3]*(1-fga)*bga, alpha_sum),
]) out_a = src_a + dst_a*(1.0-src_a)
bg[:,:,3:] = 1 - (1 - bga)*(1 - fga) out_rgb = fdiv(
self.pixel_array = (self.color_max_val*bg).astype(self.pixel_array_dtype) src_rgb*src_a[..., None] + \
dst_rgb*dst_a[..., None]*(1.0-src_a[..., None]),
out_a[..., None]
)
self.pixel_array[..., :3] = out_rgb*self.rgb_max_val
self.pixel_array[..., 3] = out_a*self.rgb_max_val
def align_points_to_camera(self, points): def align_points_to_camera(self, points):
## This is where projection should live ## This is where projection should live
@ -537,8 +554,7 @@ class BackgroundColoredVMobjectDisplayer(object):
mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB"
return self.resize_background_array(background_array, width, height, mode) return self.resize_background_array(background_array, width, height, mode)
def get_background_array(self, cvmobject): def get_background_array(self, file_name):
file_name = cvmobject.get_background_image_file()
if file_name in self.file_name_to_pixel_array_map: if file_name in self.file_name_to_pixel_array_map:
return self.file_name_to_pixel_array_map[file_name] return self.file_name_to_pixel_array_map[file_name]
full_path = get_full_raster_image_path(file_name) full_path = get_full_raster_image_path(file_name)
@ -553,12 +569,12 @@ class BackgroundColoredVMobjectDisplayer(object):
return array return array
def display(self, *cvmobjects): def display(self, *cvmobjects):
batches = batch_by_property( batch_image_file_pairs = batch_by_property(
cvmobjects, lambda cv : cv.get_background_image_file() cvmobjects, lambda cv : cv.get_background_image_file()
) )
curr_array = None curr_array = None
for batch in batches: for batch, image_file in batch_image_file_pairs:
background_array = self.get_background_array(batch[0]) background_array = self.get_background_array(image_file)
for cvmobject in batch: for cvmobject in batch:
self.camera.display_vectorized(cvmobject, self.canvas) self.camera.display_vectorized(cvmobject, self.canvas)
self.canvas.flush() self.canvas.flush()

View file

@ -227,22 +227,30 @@ def adjacent_pairs(objects):
return zip(objects, list(objects[1:])+[objects[0]]) return zip(objects, list(objects[1:])+[objects[0]])
def batch_by_property(items, property_func): def batch_by_property(items, property_func):
batches = [] """
def add_batch(batch): Takes in a list, and returns a list of tuples, (batch, prop)
such that all items in a batch have the same output when
put into property_func, and such that chaining all these
batches together would give the original list.
"""
batch_prop_pairs = []
def add_batch_prop_pair(batch):
if len(batch) > 0: if len(batch) > 0:
batches.append(batch) batch_prop_pairs.append(
(batch, property_func(batch[0]))
)
curr_batch = [] curr_batch = []
curr_prop = None curr_prop = None
for item in items: for item in items:
prop = property_func(item) prop = property_func(item)
if prop != curr_prop: if prop != curr_prop:
add_batch(curr_batch) add_batch_prop_pair(curr_batch)
curr_prop = prop curr_prop = prop
curr_batch = [item] curr_batch = [item]
else: else:
curr_batch.append(item) curr_batch.append(item)
add_batch(curr_batch) add_batch_prop_pair(curr_batch)
return batches return batch_prop_pairs
def complex_to_R3(complex_num): def complex_to_R3(complex_num):
return np.array((complex_num.real, complex_num.imag, 0)) return np.array((complex_num.real, complex_num.imag, 0))

View file

@ -141,9 +141,15 @@ class TexMobject(SVGMobject):
self.submobjects = new_submobjects self.submobjects = new_submobjects
return self return self
def get_parts_by_tex(self, tex, substring = True): def get_parts_by_tex(self, tex, substring = True, case_sensitive = True):
def test(tex1, tex2): def test(tex1, tex2):
return tex1 == tex2 or (substring and tex1 in tex2) if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
tex_submobjects = filter( tex_submobjects = filter(
lambda m : isinstance(m, TexMobject), lambda m : isinstance(m, TexMobject),

View file

@ -122,7 +122,7 @@ class VMobject(Mobject):
return self return self
def get_fill_rgb(self): def get_fill_rgb(self):
return self.fill_rgb return np.clip(self.fill_rgb, 0, 1)
def get_fill_color(self): def get_fill_color(self):
try: try:
@ -135,7 +135,7 @@ class VMobject(Mobject):
return np.clip(self.fill_opacity, 0, 1) return np.clip(self.fill_opacity, 0, 1)
def get_stroke_rgb(self): def get_stroke_rgb(self):
return self.stroke_rgb return np.clip(self.stroke_rgb, 0, 1)
def get_stroke_color(self): def get_stroke_color(self):
try: try:

View file

@ -35,90 +35,6 @@ VELOCITY_COLOR = GREEN
#### Warning, scenes here not updated based on most recent GraphScene changes ####### #### Warning, scenes here not updated based on most recent GraphScene changes #######
class Car(SVGMobject):
CONFIG = {
"file_name" : "Car",
"height" : 1,
"color" : "#BBBBBB",
}
def __init__(self, **kwargs):
SVGMobject.__init__(self, **kwargs)
self.scale_to_fit_height(self.height)
self.set_stroke(color = WHITE, width = 0)
self.set_fill(self.color, opacity = 1)
randy = Randolph(mode = "happy")
randy.scale_to_fit_height(0.6*self.get_height())
randy.stretch(0.8, 0)
randy.look(RIGHT)
randy.move_to(self)
randy.shift(0.07*self.height*(RIGHT+UP))
self.randy = randy
self.add_to_back(randy)
orientation_line = Line(self.get_left(), self.get_right())
orientation_line.set_stroke(width = 0)
self.add(orientation_line)
self.orientation_line = orientation_line
self.add_treds_to_tires()
def move_to(self, point_or_mobject):
vect = rotate_vector(
UP+LEFT, self.orientation_line.get_angle()
)
self.next_to(point_or_mobject, vect, buff = 0)
return self
def get_front_line(self):
return DashedLine(
self.get_corner(UP+RIGHT),
self.get_corner(DOWN+RIGHT),
color = DISTANCE_COLOR,
dashed_segment_length = 0.05,
)
def add_treds_to_tires(self):
for tire in self.get_tires():
radius = tire.get_width()/2
center = tire.get_center()
tred = Line(
0.9*radius*RIGHT, 1.4*radius*RIGHT,
stroke_width = 2,
color = BLACK
)
tred.rotate_in_place(np.pi/4)
for theta in np.arange(0, 2*np.pi, np.pi/4):
new_tred = tred.copy()
new_tred.rotate(theta)
new_tred.shift(center)
tire.add(new_tred)
return self
def get_tires(self):
return VGroup(self[1][1], self[1][3])
class MoveCar(ApplyMethod):
CONFIG = {
"moving_forward" : True,
}
def __init__(self, car, target_point, **kwargs):
ApplyMethod.__init__(self, car.move_to, target_point, **kwargs)
displacement = self.target_mobject.get_right()-self.starting_mobject.get_right()
distance = np.linalg.norm(displacement)
if not self.moving_forward:
distance *= -1
tire_radius = car.get_tires()[0].get_width()/2
self.total_tire_radians = -distance/tire_radius
def update_mobject(self, alpha):
ApplyMethod.update_mobject(self, alpha)
if alpha == 0:
return
radians = alpha*self.total_tire_radians
for tire in self.mobject.get_tires():
tire.rotate_in_place(radians)
class IncrementNumber(Succession): class IncrementNumber(Succession):
CONFIG = { CONFIG = {
"start_num" : 0, "start_num" : 0,

View file

@ -219,7 +219,29 @@ class Axes(VGroup):
return graph return graph
def input_to_graph_point(self, x, graph): def input_to_graph_point(self, x, graph):
return self.coords_to_point(x, graph.underlying_function(x)) if hasattr(graph, "underlying_function"):
return self.coords_to_point(x, graph.underlying_function(x))
else:
#binary search
lh, rh = 0, 1
while abs(lh - rh) > 0.001:
mh = np.mean([lh, rh])
hands = [lh, mh, rh]
points = map(graph.point_from_proportion, hands)
lx, mx, rx = map(self.x_axis.point_to_number, points)
if lx <= x and rx >= x:
if mx > x:
rh = mh
else:
lh = mh
elif lx <= x and rx <= x:
return points[2]
elif lx >= x and rx >= x:
return points[0]
elif lx > x and rx < x:
lh, rh = rh, lh
return points[1]
class ThreeDAxes(Axes): class ThreeDAxes(Axes):
CONFIG = { CONFIG = {

View file

@ -486,6 +486,104 @@ class ThoughtBubble(Bubble):
self.submobjects[-1].set_fill(GREEN_SCREEN, opacity = 1) self.submobjects[-1].set_fill(GREEN_SCREEN, opacity = 1)
return self return self
class Car(SVGMobject):
CONFIG = {
"file_name" : "Car",
"height" : 1,
"color" : LIGHT_GREY,
"light_colors" : [BLACK, BLACK],
}
def __init__(self, **kwargs):
SVGMobject.__init__(self, **kwargs)
self.scale_to_fit_height(self.height)
self.set_stroke(color = WHITE, width = 0)
self.set_fill(self.color, opacity = 1)
from topics.characters import Randolph
randy = Randolph(mode = "happy")
randy.scale_to_fit_height(0.6*self.get_height())
randy.stretch(0.8, 0)
randy.look(RIGHT)
randy.move_to(self)
randy.shift(0.07*self.height*(RIGHT+UP))
self.randy = self.pi_creature = randy
self.add_to_back(randy)
orientation_line = Line(self.get_left(), self.get_right())
orientation_line.set_stroke(width = 0)
self.add(orientation_line)
self.orientation_line = orientation_line
for light, color in zip(self.get_lights(), self.light_colors):
light.set_fill(color, 1)
light.is_subpath = False
self.add_treds_to_tires()
def move_to(self, point_or_mobject):
vect = rotate_vector(
UP+LEFT, self.orientation_line.get_angle()
)
self.next_to(point_or_mobject, vect, buff = 0)
return self
def get_front_line(self):
return DashedLine(
self.get_corner(UP+RIGHT),
self.get_corner(DOWN+RIGHT),
color = DISTANCE_COLOR,
dashed_segment_length = 0.05,
)
def add_treds_to_tires(self):
for tire in self.get_tires():
radius = tire.get_width()/2
center = tire.get_center()
tred = Line(
0.9*radius*RIGHT, 1.4*radius*RIGHT,
stroke_width = 2,
color = BLACK
)
tred.rotate_in_place(np.pi/4)
for theta in np.arange(0, 2*np.pi, np.pi/4):
new_tred = tred.copy()
new_tred.rotate(theta, about_point = ORIGIN)
new_tred.shift(center)
tire.add(new_tred)
return self
def get_tires(self):
return VGroup(self[1][1], self[1][3])
def get_lights(self):
return VGroup(self.get_front_light(), self.get_rear_light())
def get_front_light(self):
return self[1][5]
def get_rear_light(self):
return self[1][8]
class MoveCar(ApplyMethod):
CONFIG = {
"moving_forward" : True,
}
def __init__(self, car, target_point, **kwargs):
ApplyMethod.__init__(self, car.move_to, target_point, **kwargs)
displacement = self.target_mobject.get_right()-self.starting_mobject.get_right()
distance = np.linalg.norm(displacement)
if not self.moving_forward:
distance *= -1
tire_radius = car.get_tires()[0].get_width()/2
self.total_tire_radians = -distance/tire_radius
def update_mobject(self, alpha):
ApplyMethod.update_mobject(self, alpha)
if alpha == 0:
return
radians = alpha*self.total_tire_radians
for tire in self.mobject.get_tires():
tire.rotate_in_place(radians)
#TODO: Where should this live? #TODO: Where should this live?
class Broadcast(LaggedStart): class Broadcast(LaggedStart):