Merge pull request #280 from 3b1b/quaternions

Quaternions
This commit is contained in:
Grant Sanderson 2018-09-04 16:15:18 -07:00 committed by GitHub
commit ae8ccffe71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 3502 additions and 116 deletions

File diff suppressed because it is too large Load diff

View file

@ -115,6 +115,8 @@ class AnimationOnSurroundingRectangle(AnimationGroup):
rect = SurroundingRectangle( rect = SurroundingRectangle(
mobject, **self.surrounding_rectangle_config mobject, **self.surrounding_rectangle_config
) )
if "surrounding_rectangle_config" in kwargs:
kwargs.pop("surrounding_rectangle_config")
AnimationGroup.__init__(self, self.rect_to_animation(rect, **kwargs)) AnimationGroup.__init__(self, self.rect_to_animation(rect, **kwargs))

View file

@ -14,7 +14,6 @@ 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
from mobject.value_tracker import ValueTracker
from utils.color import color_to_int_rgba from utils.color import color_to_int_rgba
from utils.color import rgb_to_hex from utils.color import rgb_to_hex
from utils.config_ops import digest_config from utils.config_ops import digest_config
@ -202,8 +201,7 @@ class Camera(object):
def extract_mobject_family_members( def extract_mobject_family_members(
self, mobjects, self, mobjects,
only_those_with_points=False, only_those_with_points=False):
ignore_value_trackers=False):
if only_those_with_points: if only_those_with_points:
method = Mobject.family_members_with_points method = Mobject.family_members_with_points
else: else:
@ -213,20 +211,16 @@ class Camera(object):
method(m) method(m)
for m in mobjects for m in mobjects
if not (isinstance(m, VMobject) and m.is_subpath) if not (isinstance(m, VMobject) and m.is_subpath)
if not (ignore_value_trackers and isinstance(m, ValueTracker))
]) ])
)) ))
def get_mobjects_to_display( def get_mobjects_to_display(
self, mobjects, self, mobjects,
include_submobjects=True, include_submobjects=True,
ignore_value_trackers=True,
excluded_mobjects=None): excluded_mobjects=None):
if include_submobjects: if include_submobjects:
mobjects = self.extract_mobject_family_members( mobjects = self.extract_mobject_family_members(
mobjects, mobjects, only_those_with_points=True,
only_those_with_points=True,
ignore_value_trackers=ignore_value_trackers,
) )
if excluded_mobjects: if excluded_mobjects:
all_excluded = self.extract_mobject_family_members( all_excluded = self.extract_mobject_family_members(
@ -347,6 +341,8 @@ class Camera(object):
points = self.transform_points_pre_display( points = self.transform_points_pre_display(
vmob, vmob.points vmob, vmob.points
) )
if np.any(np.isnan(points)) or np.any(points == np.inf):
points = np.zeros((1, 3))
ctx.new_sub_path() ctx.new_sub_path()
ctx.move_to(*points[0][:2]) ctx.move_to(*points[0][:2])
for triplet in zip(points[1::3], points[2::3], points[3::3]): for triplet in zip(points[1::3], points[2::3], points[3::3]):

View file

@ -6,6 +6,7 @@ from constants import *
from camera.camera import Camera from camera.camera import Camera
from mobject.types.point_cloud_mobject import Point from mobject.types.point_cloud_mobject import Point
from mobject.types.vectorized_mobject import VMobject
from mobject.three_d_utils import get_3d_vmob_start_corner from mobject.three_d_utils import get_3d_vmob_start_corner
from mobject.three_d_utils import get_3d_vmob_start_corner_unit_normal from mobject.three_d_utils import get_3d_vmob_start_corner_unit_normal
from mobject.three_d_utils import get_3d_vmob_end_corner from mobject.three_d_utils import get_3d_vmob_end_corner
@ -94,22 +95,22 @@ class ThreeDCamera(Camera):
vmobject, vmobject.get_fill_rgbas() vmobject, vmobject.get_fill_rgbas()
) )
def display_multiple_vectorized_mobjects(self, vmobjects, pixel_array): def get_mobjects_to_display(self, *args, **kwargs):
mobjects = Camera.get_mobjects_to_display(
self, *args, **kwargs
)
rot_matrix = self.get_rotation_matrix() rot_matrix = self.get_rotation_matrix()
def z_key(vmob): def z_key(mob):
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
return np.inf
# Assign a number to a three dimensional mobjects # Assign a number to a three dimensional mobjects
# based on how close it is to the camera # based on how close it is to the camera
if vmob.shade_in_3d: return np.dot(
return np.dot( mob.get_center(),
vmob.get_center(), rot_matrix.T
rot_matrix.T )[2]
)[2] return sorted(mobjects, key=z_key)
else:
return np.inf
Camera.display_multiple_vectorized_mobjects(
self, sorted(vmobjects, key=z_key), pixel_array
)
def get_phi(self): def get_phi(self):
return self.phi_tracker.get_value() return self.phi_tracker.get_value()
@ -180,7 +181,8 @@ class ThreeDCamera(Camera):
factor[lt0] = (distance / (distance - zs[lt0])) factor[lt0] = (distance / (distance - zs[lt0]))
else: else:
factor = (distance / (distance - zs)) factor = (distance / (distance - zs))
clip_in_place(factor, 0, 10**6) factor[(distance - zs) < 0] = 10**6
# clip_in_place(factor, 0, 10**6)
points[:, i] *= factor points[:, i] *= factor
points += frame_center points += frame_center
return points return points

View file

@ -193,14 +193,14 @@ class PatreonEndScreen(PatreonThanks):
black_rect = Rectangle( black_rect = Rectangle(
fill_color=BLACK, fill_color=BLACK,
fill_opacity=1, fill_opacity=1,
stroke_width=0, stroke_width=3,
stroke_color=BLACK,
width=FRAME_WIDTH, width=FRAME_WIDTH,
height=0.6 * FRAME_HEIGHT, height=0.6 * FRAME_HEIGHT,
) )
black_rect.to_edge(UP, buff=0) black_rect.to_edge(UP, buff=0)
line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT) line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT)
line.move_to(ORIGIN) line.move_to(ORIGIN)
self.add(line)
thanks = TextMobject("Funded by the community, with special thanks to:") thanks = TextMobject("Funded by the community, with special thanks to:")
thanks.scale(0.9) thanks.scale(0.9)
@ -210,7 +210,6 @@ class PatreonEndScreen(PatreonThanks):
underline.set_width(thanks.get_width() + MED_SMALL_BUFF) underline.set_width(thanks.get_width() + MED_SMALL_BUFF)
underline.next_to(thanks, DOWN, SMALL_BUFF) underline.next_to(thanks, DOWN, SMALL_BUFF)
thanks.add(underline) thanks.add(underline)
self.add(thanks)
patrons = VGroup(*list(map(TextMobject, self.specific_patrons))) patrons = VGroup(*list(map(TextMobject, self.specific_patrons)))
patrons.scale(self.patron_scale_val) patrons.scale(self.patron_scale_val)
@ -234,12 +233,10 @@ class PatreonEndScreen(PatreonThanks):
thanks.align_to(columns, alignment_vect=RIGHT) thanks.align_to(columns, alignment_vect=RIGHT)
self.add(columns, black_rect, line, thanks)
self.play( self.play(
columns.move_to, 2 * DOWN, DOWN, columns.move_to, 2 * DOWN, DOWN,
columns.to_edge, RIGHT, columns.to_edge, RIGHT,
Animation(black_rect),
Animation(line),
Animation(thanks),
rate_func=None, rate_func=None,
run_time=self.run_time, run_time=self.run_time,
) )

View file

@ -554,10 +554,10 @@ class Arrow(Line):
self.second_tip.get_anchors()[1:] self.second_tip.get_anchors()[1:]
) )
self.rect.set_points_as_corners([ self.rect.set_points_as_corners([
tip_base + perp_vect * width / 2,
start + perp_vect * width / 2,
start - perp_vect * width / 2,
tip_base - perp_vect * width / 2, tip_base - perp_vect * width / 2,
start - perp_vect * width / 2,
start + perp_vect * width / 2,
tip_base + perp_vect * width / 2,
]) ])
self.stem = self.rect # Alternate name self.stem = self.rect # Alternate name
return self return self

View file

@ -170,8 +170,11 @@ class Mobject(Container):
def get_updaters(self): def get_updaters(self):
return self.updaters return self.updaters
def add_updater(self, update_function, call_updater=True): def add_updater(self, update_function, index=None, call_updater=True):
self.updaters.append(update_function) if index is None:
self.updaters.append(update_function)
else:
self.updaters.insert(index, update_function)
if call_updater: if call_updater:
self.update(0) self.update(0)
return self return self
@ -254,7 +257,7 @@ class Mobject(Container):
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 ("about_point" not in kwargs) and ("about_edge" not in kwargs):
kwargs["about_point"] = ORIGIN kwargs["about_point"] = ORIGIN
full_matrix = np.identity(self.dim) full_matrix = np.identity(self.dim)
matrix = np.array(matrix) matrix = np.array(matrix)
@ -982,7 +985,8 @@ class Mobject(Container):
""" """
self.align_data(mobject) self.align_data(mobject)
for sm1, sm2 in zip(self.get_family(), mobject.get_family()): for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
sm1.interpolate(sm1, sm2, 1) sm1.points = np.array(sm2.points)
sm1.interpolate_color(sm1, sm2, 1)
return self return self

View file

@ -29,7 +29,10 @@ class DecimalNumber(VMobject):
shows_zero = np.round(number, self.num_decimal_places) == 0 shows_zero = np.round(number, self.num_decimal_places) == 0
if num_string.startswith("-") and shows_zero: if num_string.startswith("-") and shows_zero:
num_string = num_string[1:] if self.include_sign:
num_string = "+" + num_string[1:]
else:
num_string = num_string[1:]
self.add(*[ self.add(*[
SingleStringTexMobject(char, **kwargs) SingleStringTexMobject(char, **kwargs)
@ -114,6 +117,7 @@ class DecimalNumber(VMobject):
# of animated mobjects # of animated mobjects
mob.points[:] = 0 mob.points[:] = 0
self.number = number self.number = number
return self
def get_value(self): def get_value(self):
return self.number return self.number

View file

@ -128,6 +128,13 @@ class SVGMobject(VMobject):
self.ref_to_element[ref] self.ref_to_element[ref]
) )
def attribute_to_float(self, attr):
stripped_attr = "".join([
char for char in attr
if char in string.digits + "." + "-"
])
return float(stripped_attr)
def polygon_to_mobject(self, polygon_element): def polygon_to_mobject(self, polygon_element):
# TODO, This seems hacky... # TODO, This seems hacky...
path_string = polygon_element.getAttribute("points") path_string = polygon_element.getAttribute("points")
@ -140,7 +147,9 @@ class SVGMobject(VMobject):
def circle_to_mobject(self, circle_element): def circle_to_mobject(self, circle_element):
x, y, r = [ x, y, r = [
float(circle_element.getAttribute(key)) self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key) if circle_element.hasAttribute(key)
else 0.0 else 0.0
for key in ("cx", "cy", "r") for key in ("cx", "cy", "r")
@ -149,7 +158,9 @@ class SVGMobject(VMobject):
def ellipse_to_mobject(self, circle_element): def ellipse_to_mobject(self, circle_element):
x, y, rx, ry = [ x, y, rx, ry = [
float(circle_element.getAttribute(key)) self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key) if circle_element.hasAttribute(key)
else 0.0 else 0.0
for key in ("cx", "cy", "rx", "ry") for key in ("cx", "cy", "rx", "ry")
@ -183,8 +194,12 @@ class SVGMobject(VMobject):
if corner_radius == 0: if corner_radius == 0:
mob = Rectangle( mob = Rectangle(
width=float(rect_element.getAttribute("width")), width=self.attribute_to_float(
height=float(rect_element.getAttribute("height")), rect_element.getAttribute("width")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
),
stroke_width=stroke_width, stroke_width=stroke_width,
stroke_color=stroke_color, stroke_color=stroke_color,
fill_color=fill_color, fill_color=fill_color,
@ -192,8 +207,12 @@ class SVGMobject(VMobject):
) )
else: else:
mob = RoundedRectangle( mob = RoundedRectangle(
width=float(rect_element.getAttribute("width")), width=self.attribute_to_float(
height=float(rect_element.getAttribute("height")), rect_element.getAttribute("width")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
),
stroke_width=stroke_width, stroke_width=stroke_width,
stroke_color=stroke_color, stroke_color=stroke_color,
fill_color=fill_color, fill_color=fill_color,
@ -207,9 +226,9 @@ class SVGMobject(VMobject):
def handle_transforms(self, element, mobject): def handle_transforms(self, element, mobject):
x, y = 0, 0 x, y = 0, 0
try: try:
x = float(element.getAttribute('x')) x = self.attribute_to_float(element.getAttribute('x'))
# Flip y # Flip y
y = -float(element.getAttribute('y')) y = -self.attribute_to_float(element.getAttribute('y'))
mobject.shift(x * RIGHT + y * UP) mobject.shift(x * RIGHT + y * UP)
except: except:
pass pass

View file

@ -38,11 +38,11 @@ def get_3d_vmob_unit_normal(vmob, point_index):
if len(vmob.get_anchors()) <= 2: if len(vmob.get_anchors()) <= 2:
return np.array(UP) return np.array(UP)
i = point_index i = point_index
im1 = i - 1 if i > 0 else (n_points - 2) im3 = i - 3 if i > 2 else (n_points - 4)
ip1 = i + 1 if i < (n_points - 1) else 1 ip3 = i + 3 if i < (n_points - 3) else 3
unit_normal = get_unit_normal( unit_normal = get_unit_normal(
vmob.points[ip1] - vmob.points[i], vmob.points[ip3] - vmob.points[i],
vmob.points[im1] - vmob.points[i], vmob.points[im3] - vmob.points[i],
) )
if get_norm(unit_normal) == 0: if get_norm(unit_normal) == 0:
return np.array(UP) return np.array(UP)

View file

@ -32,6 +32,7 @@ class ParametricSurface(VGroup):
"stroke_color": LIGHT_GREY, "stroke_color": LIGHT_GREY,
"stroke_width": 0.5, "stroke_width": 0.5,
"should_make_jagged": False, "should_make_jagged": False,
"pre_function_handle_to_anchor_scale_factor": 0.00001,
} }
def __init__(self, func, **kwargs): def __init__(self, func, **kwargs):
@ -70,6 +71,10 @@ class ParametricSurface(VGroup):
faces.add(face) faces.add(face)
face.u_index = i face.u_index = i
face.v_index = j face.v_index = j
face.u1 = u1
face.u2 = u2
face.v1 = v1
face.v2 = v2
faces.set_fill( faces.set_fill(
color=self.fill_color, color=self.fill_color,
opacity=self.fill_opacity opacity=self.fill_opacity

View file

@ -308,6 +308,10 @@ class VMobject(Mobject):
self.color_using_background_image(vmobject.get_background_image_file()) self.color_using_background_image(vmobject.get_background_image_file())
return self return self
def set_shade_in_3d(self, value=True):
for submob in self.get_family():
submob.shade_in_3d = value
# Drawing # Drawing
def start_at(self, point): def start_at(self, point):
if len(self.points) == 0: if len(self.points) == 0:

View file

@ -2,7 +2,10 @@ import numpy as np
from constants import OUT from constants import OUT
from constants import RIGHT from constants import RIGHT
from constants import PI
from constants import TAU
from functools import reduce from functools import reduce
from utils.iterables import adjacent_pairs
# Matrix operations # Matrix operations
@ -97,12 +100,15 @@ def project_along_vector(point, vector):
return np.dot(point, matrix.T) return np.dot(point, matrix.T)
def normalize(vect): def normalize(vect, fall_back=None):
norm = get_norm(vect) norm = get_norm(vect)
if norm > 0: if norm > 0:
return vect / norm return np.array(vect) / norm
else: else:
return np.zeros(len(vect)) if fall_back is not None:
return fall_back
else:
return np.zeros(len(vect))
def cross(v1, v2): def cross(v1, v2):
@ -164,3 +170,12 @@ def line_intersection(line1, line2):
x = det(d, x_diff) / div x = det(d, x_diff) / div
y = det(d, y_diff) / div y = det(d, y_diff) / div
return np.array([x, y, 0]) return np.array([x, y, 0])
def get_winding_number(points):
total_angle = 0
for p1, p2 in adjacent_pairs(points):
d_angle = angle_of_vector(p2) - angle_of_vector(p1)
d_angle = ((d_angle + PI) % TAU) - PI
total_angle += d_angle
return total_angle / TAU