Skip to content

Commit

Permalink
Merge pull request 3b1b#280 from 3b1b/quaternions
Browse files Browse the repository at this point in the history
Quaternions
  • Loading branch information
3b1b authored Sep 4, 2018
2 parents 8468d8d + a1e21a4 commit ae8ccff
Show file tree
Hide file tree
Showing 13 changed files with 3,498 additions and 112 deletions.
3,462 changes: 3,400 additions & 62 deletions active_projects/quaternions.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions animation/indication.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ def __init__(self, mobject, **kwargs):
rect = SurroundingRectangle(
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))


Expand Down
12 changes: 4 additions & 8 deletions camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from mobject.mobject import Mobject
from mobject.types.point_cloud_mobject import PMobject
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 rgb_to_hex
from utils.config_ops import digest_config
Expand Down Expand Up @@ -202,8 +201,7 @@ def reset(self):

def extract_mobject_family_members(
self, mobjects,
only_those_with_points=False,
ignore_value_trackers=False):
only_those_with_points=False):
if only_those_with_points:
method = Mobject.family_members_with_points
else:
Expand All @@ -213,20 +211,16 @@ def extract_mobject_family_members(
method(m)
for m in mobjects
if not (isinstance(m, VMobject) and m.is_subpath)
if not (ignore_value_trackers and isinstance(m, ValueTracker))
])
))

def get_mobjects_to_display(
self, mobjects,
include_submobjects=True,
ignore_value_trackers=True,
excluded_mobjects=None):
if include_submobjects:
mobjects = self.extract_mobject_family_members(
mobjects,
only_those_with_points=True,
ignore_value_trackers=ignore_value_trackers,
mobjects, only_those_with_points=True,
)
if excluded_mobjects:
all_excluded = self.extract_mobject_family_members(
Expand Down Expand Up @@ -347,6 +341,8 @@ def set_cairo_context_path(self, ctx, vmobject):
points = self.transform_points_pre_display(
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.move_to(*points[0][:2])
for triplet in zip(points[1::3], points[2::3], points[3::3]):
Expand Down
28 changes: 15 additions & 13 deletions camera/three_d_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from camera.camera import Camera
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_unit_normal
from mobject.three_d_utils import get_3d_vmob_end_corner
Expand Down Expand Up @@ -94,22 +95,22 @@ def get_fill_rgbas(self, vmobject):
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()

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
# based on how close it is to the camera
if vmob.shade_in_3d:
return np.dot(
vmob.get_center(),
rot_matrix.T
)[2]
else:
return np.inf
Camera.display_multiple_vectorized_mobjects(
self, sorted(vmobjects, key=z_key), pixel_array
)
return np.dot(
mob.get_center(),
rot_matrix.T
)[2]
return sorted(mobjects, key=z_key)

def get_phi(self):
return self.phi_tracker.get_value()
Expand Down Expand Up @@ -180,7 +181,8 @@ def project_points(self, points):
factor[lt0] = (distance / (distance - zs[lt0]))
else:
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 += frame_center
return points
Expand Down
9 changes: 3 additions & 6 deletions for_3b1b_videos/common_scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ def scroll_through_patrons(self):
black_rect = Rectangle(
fill_color=BLACK,
fill_opacity=1,
stroke_width=0,
stroke_width=3,
stroke_color=BLACK,
width=FRAME_WIDTH,
height=0.6 * FRAME_HEIGHT,
)
black_rect.to_edge(UP, buff=0)
line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT)
line.move_to(ORIGIN)
self.add(line)

thanks = TextMobject("Funded by the community, with special thanks to:")
thanks.scale(0.9)
Expand All @@ -210,7 +210,6 @@ def scroll_through_patrons(self):
underline.set_width(thanks.get_width() + MED_SMALL_BUFF)
underline.next_to(thanks, DOWN, SMALL_BUFF)
thanks.add(underline)
self.add(thanks)

patrons = VGroup(*list(map(TextMobject, self.specific_patrons)))
patrons.scale(self.patron_scale_val)
Expand All @@ -234,12 +233,10 @@ def scroll_through_patrons(self):

thanks.align_to(columns, alignment_vect=RIGHT)

self.add(columns, black_rect, line, thanks)
self.play(
columns.move_to, 2 * DOWN, DOWN,
columns.to_edge, RIGHT,
Animation(black_rect),
Animation(line),
Animation(thanks),
rate_func=None,
run_time=self.run_time,
)
Expand Down
6 changes: 3 additions & 3 deletions mobject/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,10 @@ def set_rectangular_stem_points(self):
self.second_tip.get_anchors()[1:]
)
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,
start - perp_vect * width / 2,
start + perp_vect * width / 2,
tip_base + perp_vect * width / 2,
])
self.stem = self.rect # Alternate name
return self
Expand Down
12 changes: 8 additions & 4 deletions mobject/mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,11 @@ def get_time_based_updaters(self):
def get_updaters(self):
return self.updaters

def add_updater(self, update_function, call_updater=True):
self.updaters.append(update_function)
def add_updater(self, update_function, index=None, call_updater=True):
if index is None:
self.updaters.append(update_function)
else:
self.updaters.insert(index, update_function)
if call_updater:
self.update(0)
return self
Expand Down Expand Up @@ -254,7 +257,7 @@ def apply_function_to_submobject_positions(self, function):

def apply_matrix(self, matrix, **kwargs):
# 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
full_matrix = np.identity(self.dim)
matrix = np.array(matrix)
Expand Down Expand Up @@ -982,7 +985,8 @@ def become(self, mobject, copy_submobjects=True):
"""
self.align_data(mobject)
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


Expand Down
6 changes: 5 additions & 1 deletion mobject/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def __init__(self, number, **kwargs):

shows_zero = np.round(number, self.num_decimal_places) == 0
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(*[
SingleStringTexMobject(char, **kwargs)
Expand Down Expand Up @@ -114,6 +117,7 @@ def set_value(self, number, **config):
# of animated mobjects
mob.points[:] = 0
self.number = number
return self

def get_value(self):
return self.number
Expand Down
35 changes: 27 additions & 8 deletions mobject/svg/svg_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ def use_to_mobjects(self, use_element):
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):
# TODO, This seems hacky...
path_string = polygon_element.getAttribute("points")
Expand All @@ -140,7 +147,9 @@ def polygon_to_mobject(self, polygon_element):

def circle_to_mobject(self, circle_element):
x, y, r = [
float(circle_element.getAttribute(key))
self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key)
else 0.0
for key in ("cx", "cy", "r")
Expand All @@ -149,7 +158,9 @@ def circle_to_mobject(self, circle_element):

def ellipse_to_mobject(self, circle_element):
x, y, rx, ry = [
float(circle_element.getAttribute(key))
self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key)
else 0.0
for key in ("cx", "cy", "rx", "ry")
Expand Down Expand Up @@ -183,17 +194,25 @@ def rect_to_mobject(self, rect_element):

if corner_radius == 0:
mob = Rectangle(
width=float(rect_element.getAttribute("width")),
height=float(rect_element.getAttribute("height")),
width=self.attribute_to_float(
rect_element.getAttribute("width")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
),
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
fill_opacity=opacity
)
else:
mob = RoundedRectangle(
width=float(rect_element.getAttribute("width")),
height=float(rect_element.getAttribute("height")),
width=self.attribute_to_float(
rect_element.getAttribute("width")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
),
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
Expand All @@ -207,9 +226,9 @@ def rect_to_mobject(self, rect_element):
def handle_transforms(self, element, mobject):
x, y = 0, 0
try:
x = float(element.getAttribute('x'))
x = self.attribute_to_float(element.getAttribute('x'))
# Flip y
y = -float(element.getAttribute('y'))
y = -self.attribute_to_float(element.getAttribute('y'))
mobject.shift(x * RIGHT + y * UP)
except:
pass
Expand Down
8 changes: 4 additions & 4 deletions mobject/three_d_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ def get_3d_vmob_unit_normal(vmob, point_index):
if len(vmob.get_anchors()) <= 2:
return np.array(UP)
i = point_index
im1 = i - 1 if i > 0 else (n_points - 2)
ip1 = i + 1 if i < (n_points - 1) else 1
im3 = i - 3 if i > 2 else (n_points - 4)
ip3 = i + 3 if i < (n_points - 3) else 3
unit_normal = get_unit_normal(
vmob.points[ip1] - vmob.points[i],
vmob.points[im1] - vmob.points[i],
vmob.points[ip3] - vmob.points[i],
vmob.points[im3] - vmob.points[i],
)
if get_norm(unit_normal) == 0:
return np.array(UP)
Expand Down
5 changes: 5 additions & 0 deletions mobject/three_dimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ParametricSurface(VGroup):
"stroke_color": LIGHT_GREY,
"stroke_width": 0.5,
"should_make_jagged": False,
"pre_function_handle_to_anchor_scale_factor": 0.00001,
}

def __init__(self, func, **kwargs):
Expand Down Expand Up @@ -70,6 +71,10 @@ def setup_in_uv_space(self):
faces.add(face)
face.u_index = i
face.v_index = j
face.u1 = u1
face.u2 = u2
face.v1 = v1
face.v2 = v2
faces.set_fill(
color=self.fill_color,
opacity=self.fill_opacity
Expand Down
4 changes: 4 additions & 0 deletions mobject/types/vectorized_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ def match_background_image_file(self, vmobject):
self.color_using_background_image(vmobject.get_background_image_file())
return self

def set_shade_in_3d(self, value=True):
for submob in self.get_family():
submob.shade_in_3d = value

# Drawing
def start_at(self, point):
if len(self.points) == 0:
Expand Down
21 changes: 18 additions & 3 deletions utils/space_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

from constants import OUT
from constants import RIGHT
from constants import PI
from constants import TAU
from functools import reduce
from utils.iterables import adjacent_pairs

# Matrix operations

Expand Down Expand Up @@ -97,12 +100,15 @@ def project_along_vector(point, vector):
return np.dot(point, matrix.T)


def normalize(vect):
def normalize(vect, fall_back=None):
norm = get_norm(vect)
if norm > 0:
return vect / norm
return np.array(vect) / norm
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):
Expand Down Expand Up @@ -164,3 +170,12 @@ def det(a, b):
x = det(d, x_diff) / div
y = det(d, y_diff) / div
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

0 comments on commit ae8ccff

Please sign in to comment.