mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
commit
d53412d738
9 changed files with 1511 additions and 201 deletions
|
@ -32,6 +32,7 @@ from topics.graph_scene import *
|
|||
|
||||
USE_ALMOST_FOURIER_BY_DEFAULT = True
|
||||
NUM_SAMPLES_FOR_FFT = 1000
|
||||
DEFAULT_COMPLEX_TO_REAL_FUNC = lambda z : z.real
|
||||
|
||||
|
||||
def get_fourier_graph(
|
||||
|
@ -53,7 +54,7 @@ def get_fourier_graph(
|
|||
graph = VMobject()
|
||||
graph.set_points_smoothly([
|
||||
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])
|
||||
])
|
||||
|
@ -62,18 +63,17 @@ def get_fourier_graph(
|
|||
|
||||
def get_fourier_transform(
|
||||
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,
|
||||
**kwargs ##Just eats these
|
||||
):
|
||||
scalar = 1./(t_max - t_min) if use_almost_fourier else 1.0
|
||||
def fourier_transform(f):
|
||||
return scalar*scipy.integrate.quad(
|
||||
lambda t : complex_to_real_func(
|
||||
# f(t) e^{-TAU*i*f*t}
|
||||
func(t)*np.exp(complex(0, -TAU*f*t))
|
||||
),
|
||||
z = scalar*scipy.integrate.quad(
|
||||
lambda t : func(t)*np.exp(complex(0, -TAU*f*t)),
|
||||
t_min, t_max
|
||||
)[0]
|
||||
return complex_to_real_func(z)
|
||||
return fourier_transform
|
||||
|
||||
##
|
||||
|
@ -939,11 +939,28 @@ class FourierMachineScene(Scene):
|
|||
if not hasattr(self, "frequency_axes"):
|
||||
self.get_frequency_axes()
|
||||
func = time_graph.underlying_function
|
||||
t_min = self.time_axes.x_min
|
||||
t_max = self.time_axes.x_max
|
||||
t_axis = self.time_axes.x_axis
|
||||
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(
|
||||
get_fourier_transform(func, t_min, t_max, **kwargs),
|
||||
color = self.center_of_mass_color,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_polarized_mobject(self, mobject, freq = 1.0):
|
||||
|
@ -999,7 +1016,8 @@ class FourierMachineScene(Scene):
|
|||
vector = Vector(UP, color = WHITE)
|
||||
graph_copy = graph.copy()
|
||||
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):
|
||||
x = interpolate(x_min, x_max, alpha)
|
||||
vector.put_start_and_end_on(
|
||||
|
@ -1016,7 +1034,13 @@ class FourierMachineScene(Scene):
|
|||
origin = self.circle_plane.coords_to_point(0, 0)
|
||||
graph_copy = polarized_graph.copy()
|
||||
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)
|
||||
return vector
|
||||
return UpdateFromAlphaFunc(vector, update_vector, **config)
|
||||
|
@ -1299,6 +1323,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
|
|||
CONFIG = {
|
||||
"initial_winding_frequency" : 3.0,
|
||||
"center_of_mass_color" : RED,
|
||||
"center_of_mass_multiple" : 1,
|
||||
}
|
||||
def construct(self):
|
||||
self.remove(self.pi_creature)
|
||||
|
@ -1581,10 +1606,11 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
|
|||
|
||||
def get_pol_graph_center_of_mass(self):
|
||||
pg = self.graph.polarized_mobject
|
||||
result = center_of_mass([
|
||||
pg.point_from_proportion(alpha)
|
||||
for alpha in np.linspace(0, 1, 1000)
|
||||
])
|
||||
result = center_of_mass(pg.get_anchors())
|
||||
if self.center_of_mass_multiple != 1:
|
||||
mult = self.center_of_mass_multiple
|
||||
origin = self.circle_plane.coords_to_point(0, 0)
|
||||
result = mult*(result - origin) + origin
|
||||
return result
|
||||
|
||||
def generate_fourier_dot_transform(self, fourier_graph):
|
||||
|
@ -1626,7 +1652,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
|
|||
|
||||
def change_frequency(self, new_freq, **kwargs):
|
||||
kwargs["run_time"] = kwargs.get("run_time", 3)
|
||||
kwargs["rate_func"] = kwargs.get(
|
||||
rate_func = kwargs.pop(
|
||||
"rate_func", bezier([0, 0, 1, 1])
|
||||
)
|
||||
added_anims = kwargs.get("added_anims", [])
|
||||
|
@ -1645,6 +1671,8 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
|
|||
anims.append(self.fourier_graph_dot_anim)
|
||||
if hasattr(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
|
||||
self.play(*anims, **kwargs)
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
116
camera/camera.py
116
camera/camera.py
|
@ -30,7 +30,7 @@ class Camera(object):
|
|||
"max_allowable_norm" : 2*SPACE_WIDTH,
|
||||
"image_mode" : "RGBA",
|
||||
"n_rgb_coords" : 4,
|
||||
"background_alpha" : 0, #Out of color_max_val
|
||||
"background_alpha" : 0, #Out of rgb_max_val
|
||||
"pixel_array_dtype" : 'uint8',
|
||||
"use_z_coordinate_for_display_order" : False,
|
||||
# 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):
|
||||
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.resize_space_shape()
|
||||
self.reset()
|
||||
|
@ -92,7 +92,7 @@ class Camera(object):
|
|||
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),
|
||||
lambda f : (f * self.rgb_max_val).astype(self.pixel_array_dtype),
|
||||
2,
|
||||
retval)
|
||||
return retval
|
||||
|
@ -177,29 +177,30 @@ class Camera(object):
|
|||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
||||
vmobjects = []
|
||||
for mobject in mobjects:
|
||||
if isinstance(mobject, VMobject):
|
||||
vmobjects.append(mobject)
|
||||
elif len(vmobjects) > 0:
|
||||
self.display_multiple_vectorized_mobjects(vmobjects)
|
||||
vmobjects = []
|
||||
|
||||
if isinstance(mobject, PMobject):
|
||||
self.display_point_cloud(
|
||||
mobject.points, mobject.rgbas,
|
||||
self.adjusted_thickness(mobject.stroke_width)
|
||||
)
|
||||
elif isinstance(mobject, ImageMobject):
|
||||
self.display_image_mobject(mobject)
|
||||
elif isinstance(mobject, Mobject):
|
||||
pass #Remainder of loop will handle submobjects
|
||||
else:
|
||||
raise Exception(
|
||||
"Unknown mobject type: " + mobject.__class__.__name__
|
||||
)
|
||||
#TODO, more? Call out if it's unknown?
|
||||
self.display_multiple_vectorized_mobjects(vmobjects)
|
||||
|
||||
# Organize this list into batches of the same type, and
|
||||
# apply corresponding function to those batches
|
||||
type_func_pairs = [
|
||||
(VMobject, self.display_multiple_vectorized_mobjects),
|
||||
(PMobject, self.display_multiple_point_cloud_mobjects),
|
||||
(ImageMobject, self.display_multiple_image_mobjects),
|
||||
(Mobject, lambda batch : batch), #Do nothing
|
||||
]
|
||||
def get_mobject_type(mobject):
|
||||
for mobject_type, func in type_func_pairs:
|
||||
if isinstance(mobject, mobject_type):
|
||||
return mobject_type
|
||||
raise Exception(
|
||||
"Trying to display something which is not of type Mobject"
|
||||
)
|
||||
batch_type_pairs = batch_by_property(mobjects, get_mobject_type)
|
||||
|
||||
#Display in these batches
|
||||
for batch, batch_type in batch_type_pairs:
|
||||
#check what the type is, and call the appropriate function
|
||||
for mobject_type, func in type_func_pairs:
|
||||
if batch_type == mobject_type:
|
||||
func(batch)
|
||||
|
||||
## Methods associated with svg rendering
|
||||
|
||||
|
@ -215,12 +216,12 @@ class Camera(object):
|
|||
def display_multiple_vectorized_mobjects(self, vmobjects):
|
||||
if len(vmobjects) == 0:
|
||||
return
|
||||
batches = batch_by_property(
|
||||
batch_file_pairs = batch_by_property(
|
||||
vmobjects,
|
||||
lambda vm : vm.get_background_image_file()
|
||||
)
|
||||
for batch in batches:
|
||||
if batch[0].get_background_image_file():
|
||||
for batch, file_name in batch_file_pairs:
|
||||
if file_name:
|
||||
self.display_multiple_background_colored_vmobject(batch)
|
||||
else:
|
||||
self.display_multiple_non_background_colored_vmobjects(batch)
|
||||
|
@ -252,7 +253,7 @@ class Camera(object):
|
|||
stroke_hex = rgb_to_hex(stroke_rgb)
|
||||
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:
|
||||
fill = None
|
||||
else:
|
||||
|
@ -305,13 +306,19 @@ class Camera(object):
|
|||
def display_multiple_background_colored_vmobject(self, cvmobjects):
|
||||
displayer = self.get_background_colored_vmobject_displayer()
|
||||
cvmobject_pixel_array = displayer.display(*cvmobjects)
|
||||
self.pixel_array[:,:] = np.maximum(
|
||||
self.pixel_array, cvmobject_pixel_array
|
||||
)
|
||||
self.overlay_rgba_array(cvmobject_pixel_array)
|
||||
return self
|
||||
|
||||
## 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):
|
||||
if len(points) == 0:
|
||||
return
|
||||
|
@ -322,7 +329,7 @@ class Camera(object):
|
|||
)
|
||||
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)
|
||||
factor = target_len/len(rgbas)
|
||||
rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len))
|
||||
|
@ -342,6 +349,10 @@ class Camera(object):
|
|||
new_pa[indices] = rgbas
|
||||
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):
|
||||
corner_coords = self.points_to_pixel_coords(image_mobject.points)
|
||||
ul_coords, ur_coords, dl_coords = corner_coords
|
||||
|
@ -410,17 +421,23 @@ class Camera(object):
|
|||
self.overlay_rgba_array(image)
|
||||
|
||||
def overlay_rgba_array(self, arr):
|
||||
# """ Overlays arr onto self.pixel_array with relevant alphas"""
|
||||
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'):
|
||||
bg[:,:,:3] = reduce(op.add, [
|
||||
np.divide(fg[:,:,:3]*fga, alpha_sum),
|
||||
np.divide(bg[:,:,:3]*(1-fga)*bga, alpha_sum),
|
||||
])
|
||||
bg[:,:,3:] = 1 - (1 - bga)*(1 - fga)
|
||||
self.pixel_array = (self.color_max_val*bg).astype(self.pixel_array_dtype)
|
||||
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)
|
||||
out_rgb = fdiv(
|
||||
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):
|
||||
## This is where projection should live
|
||||
|
@ -537,8 +554,7 @@ class BackgroundColoredVMobjectDisplayer(object):
|
|||
mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB"
|
||||
return self.resize_background_array(background_array, width, height, mode)
|
||||
|
||||
def get_background_array(self, cvmobject):
|
||||
file_name = cvmobject.get_background_image_file()
|
||||
def get_background_array(self, file_name):
|
||||
if file_name in self.file_name_to_pixel_array_map:
|
||||
return self.file_name_to_pixel_array_map[file_name]
|
||||
full_path = get_full_raster_image_path(file_name)
|
||||
|
@ -553,12 +569,12 @@ class BackgroundColoredVMobjectDisplayer(object):
|
|||
return array
|
||||
|
||||
def display(self, *cvmobjects):
|
||||
batches = batch_by_property(
|
||||
batch_image_file_pairs = batch_by_property(
|
||||
cvmobjects, lambda cv : cv.get_background_image_file()
|
||||
)
|
||||
curr_array = None
|
||||
for batch in batches:
|
||||
background_array = self.get_background_array(batch[0])
|
||||
for batch, image_file in batch_image_file_pairs:
|
||||
background_array = self.get_background_array(image_file)
|
||||
for cvmobject in batch:
|
||||
self.camera.display_vectorized(cvmobject, self.canvas)
|
||||
self.canvas.flush()
|
||||
|
|
20
helpers.py
20
helpers.py
|
@ -227,22 +227,30 @@ def adjacent_pairs(objects):
|
|||
return zip(objects, list(objects[1:])+[objects[0]])
|
||||
|
||||
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:
|
||||
batches.append(batch)
|
||||
batch_prop_pairs.append(
|
||||
(batch, property_func(batch[0]))
|
||||
)
|
||||
curr_batch = []
|
||||
curr_prop = None
|
||||
for item in items:
|
||||
prop = property_func(item)
|
||||
if prop != curr_prop:
|
||||
add_batch(curr_batch)
|
||||
add_batch_prop_pair(curr_batch)
|
||||
curr_prop = prop
|
||||
curr_batch = [item]
|
||||
else:
|
||||
curr_batch.append(item)
|
||||
add_batch(curr_batch)
|
||||
return batches
|
||||
add_batch_prop_pair(curr_batch)
|
||||
return batch_prop_pairs
|
||||
|
||||
def complex_to_R3(complex_num):
|
||||
return np.array((complex_num.real, complex_num.imag, 0))
|
||||
|
|
|
@ -141,9 +141,15 @@ class TexMobject(SVGMobject):
|
|||
self.submobjects = new_submobjects
|
||||
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):
|
||||
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(
|
||||
lambda m : isinstance(m, TexMobject),
|
||||
|
|
|
@ -122,7 +122,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
def get_fill_rgb(self):
|
||||
return self.fill_rgb
|
||||
return np.clip(self.fill_rgb, 0, 1)
|
||||
|
||||
def get_fill_color(self):
|
||||
try:
|
||||
|
@ -135,7 +135,7 @@ class VMobject(Mobject):
|
|||
return np.clip(self.fill_opacity, 0, 1)
|
||||
|
||||
def get_stroke_rgb(self):
|
||||
return self.stroke_rgb
|
||||
return np.clip(self.stroke_rgb, 0, 1)
|
||||
|
||||
def get_stroke_color(self):
|
||||
try:
|
||||
|
|
|
@ -35,90 +35,6 @@ VELOCITY_COLOR = GREEN
|
|||
#### 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):
|
||||
CONFIG = {
|
||||
"start_num" : 0,
|
||||
|
|
|
@ -219,7 +219,29 @@ class Axes(VGroup):
|
|||
return 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):
|
||||
CONFIG = {
|
||||
|
|
|
@ -486,6 +486,104 @@ class ThoughtBubble(Bubble):
|
|||
self.submobjects[-1].set_fill(GREEN_SCREEN, opacity = 1)
|
||||
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?
|
||||
class Broadcast(LaggedStart):
|
||||
|
|
Loading…
Add table
Reference in a new issue