Merge pull request #276 from 3b1b/quaternions

Quaternions
This commit is contained in:
Grant Sanderson
2018-08-27 17:54:23 -07:00
committed by GitHub
20 changed files with 2889 additions and 93 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -191,6 +191,14 @@ class FadeOutAndShiftDown(FadeOutAndShift):
}
class FadeInFromLarge(Transform):
def __init__(self, mobject, scale_factor=2, **kwargs):
target = mobject.copy()
mobject.scale(scale_factor)
mobject.fade(1)
Transform.__init__(self, mobject, target, **kwargs)
class VFadeIn(Animation):
"""
VFadeIn and VFadeOut only work for VMobjects, but they can be applied

View File

@@ -1,6 +1,4 @@
from constants import *
from animation.animation import Animation
from utils.bezier import interpolate
from utils.config_ops import digest_config

View File

@@ -51,6 +51,7 @@ from mobject.svg.brace import *
from mobject.svg.drawings import *
from mobject.svg.svg_mobject import *
from mobject.svg.tex_mobject import *
from mobject.three_d_utils import *
from mobject.three_dimensions import *
from mobject.types.image_mobject import *
from mobject.types.point_cloud_mobject import *

View File

@@ -2,14 +2,10 @@
import itertools as it
import numpy as np
import operator as op
import copy
# import aggdraw
import copy
import time
from PIL import Image
from colour import Color
from scipy.spatial.distance import pdist
import cairo
@@ -18,6 +14,7 @@ from mobject.types.image_mobject import AbstractImageMobject
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
@@ -203,7 +200,10 @@ class Camera(object):
####
def extract_mobject_family_members(self, mobjects, only_those_with_points=False):
def extract_mobject_family_members(
self, mobjects,
only_those_with_points=False,
ignore_value_trackers=False):
if only_those_with_points:
method = Mobject.family_members_with_points
else:
@@ -213,17 +213,20 @@ class Camera(object):
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,
excluded_mobjects=None,
):
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
mobjects,
only_those_with_points=True,
ignore_value_trackers=ignore_value_trackers,
)
if excluded_mobjects:
all_excluded = self.extract_mobject_family_members(

View File

@@ -18,6 +18,7 @@ from utils.space_ops import rotation_about_z
from utils.space_ops import rotation_matrix
from utils.space_ops import center_of_mass
from utils.simple_functions import fdiv
from utils.simple_functions import clip_in_place
class ThreeDCamera(Camera):
@@ -32,6 +33,7 @@ class ThreeDCamera(Camera):
"light_source_start_point": 9 * DOWN + 7 * LEFT + 10 * OUT,
"frame_center": ORIGIN,
"should_apply_shading": True,
"exponential_projection": False,
}
def __init__(self, *args, **kwargs):
@@ -42,7 +44,7 @@ class ThreeDCamera(Camera):
self.gamma_tracker = ValueTracker(self.gamma)
self.light_source = Point(self.light_source_start_point)
self.frame_center = Point(self.frame_center)
self.fixed_orientation_mobjects = set()
self.fixed_orientation_mobjects = dict()
self.fixed_in_frame_mobjects = set()
self.reset_rotation_matrix()
@@ -168,13 +170,17 @@ class ThreeDCamera(Camera):
points = np.dot(points, rot_matrix.T)
zs = points[:, 2]
for i in 0, 1:
# Proper projedtion would involve multiplying
# x and y by d / (d-z). But for points with high
# z value, this causes weird artifacts, and applying
# the exponential helps smooth it out.
factor = np.exp(zs / distance)
lt0 = zs < 0
factor[lt0] = (distance / (distance - zs[lt0]))
if self.exponential_projection:
# Proper projedtion would involve multiplying
# x and y by d / (d-z). But for points with high
# z value that causes weird artifacts, and applying
# the exponential helps smooth it out.
factor = np.exp(zs / distance)
lt0 = zs < 0
factor[lt0] = (distance / (distance - zs[lt0]))
else:
factor = (distance / (distance - zs))
clip_in_place(factor, 0, 10**6)
points[:, i] *= factor
points += frame_center
return points
@@ -189,15 +195,33 @@ class ThreeDCamera(Camera):
if fixed_in_frame:
return points
if fixed_orientation:
center = center_of_mass(points)
# center = center_of_mass(points)
center_func = self.fixed_orientation_mobjects[mobject]
center = center_func()
new_center = self.project_point(center)
return points + (new_center - center)
else:
return self.project_points(points)
def add_fixed_orientation_mobjects(self, *mobjects):
for mobject in self.extract_mobject_family_members(mobjects):
self.fixed_orientation_mobjects.add(mobject)
def add_fixed_orientation_mobjects(
self, *mobjects,
use_static_center_func=False,
center_func=None):
# This prevents the computation of mobject.get_center
# every single time a projetion happens
def get_static_center_func(mobject):
point = mobject.get_center()
return (lambda: point)
for mobject in mobjects:
if center_func:
func = center_func
elif use_static_center_func:
func = get_static_center_func(mobject)
else:
func = mobject.get_center
for submob in mobject.get_family():
self.fixed_orientation_mobjects[submob] = func
def add_fixed_in_frame_mobjects(self, *mobjects):
for mobject in self.extract_mobject_family_members(mobjects):

View File

@@ -140,7 +140,7 @@ with open(TEMPLATE_TEX_FILE, "r") as infile:
TEMPLATE_TEXT_FILE_BODY = infile.read()
TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace(
TEX_TEXT_TO_REPLACE,
"$${}$$".format(TEX_TEXT_TO_REPLACE)
"\\begin{align*}" + TEX_TEXT_TO_REPLACE + "\\end{align*}",
)
FFMPEG_BIN = "ffmpeg"

View File

@@ -305,49 +305,63 @@ class ThreeLeggedPiCreature(PiCreature):
class Eyes(VMobject):
CONFIG = {
"height": 0.3,
"thing_looked_at": None,
"thing_to_look_at": None,
"mode": "plain",
}
def __init__(self, mobject, **kwargs):
def __init__(self, body, **kwargs):
VMobject.__init__(self, **kwargs)
self.mobject = mobject
self.submobjects = self.get_eyes().submobjects
self.body = body
eyes = self.create_eyes()
self.become(eyes, copy_submobjects=False)
def get_eyes(self, mode=None, thing_to_look_at=None):
mode = mode or self.mode
def create_eyes(self, mode=None, thing_to_look_at=None):
if mode is None:
mode = self.mode
if thing_to_look_at is None:
thing_to_look_at = self.thing_looked_at
thing_to_look_at = self.thing_to_look_at
self.thing_to_look_at = thing_to_look_at
self.mode = mode
looking_direction = None
pi = Randolph(mode=mode)
pi = PiCreature(mode=mode)
eyes = VGroup(pi.eyes, pi.pupils)
pi.scale(self.height / eyes.get_height())
if self.submobjects:
eyes.match_height(self)
eyes.move_to(self, DOWN)
looking_direction = self[1].get_center() - self[0].get_center()
else:
eyes.move_to(self.mobject.get_top(), DOWN)
eyes.set_height(self.height)
eyes.move_to(self.body.get_top(), DOWN)
height = eyes.get_height()
if thing_to_look_at is not None:
pi.look_at(thing_to_look_at)
elif looking_direction is not None:
pi.look(looking_direction)
eyes.set_height(height)
return eyes
def change_mode_anim(self, mode, **kwargs):
self.mode = mode
return Transform(self, self.get_eyes(mode=mode), **kwargs)
def look_at_anim(self, point_or_mobject, **kwargs):
self.thing_looked_at = point_or_mobject
return Transform(
self, self.get_eyes(thing_to_look_at=point_or_mobject),
**kwargs
def change_mode(self, mode, thing_to_look_at=None):
new_eyes = self.create_eyes(
mode=mode,
thing_to_look_at=thing_to_look_at
)
self.become(new_eyes, copy_submobjects=False)
return self
def blink_anim(self, **kwargs):
target = self.copy()
def look_at(self, thing_to_look_at):
self.change_mode(
self.mode,
thing_to_look_at=thing_to_look_at
)
return self
def blink(self, **kwargs): # TODO, change Blink
bottom_y = self.get_bottom()[1]
for submob in target:
for submob in self:
submob.apply_function(
lambda p: [p[0], bottom_y, p[2]]
)
if "rate_func" not in kwargs:
kwargs["rate_func"] = squish_rate_func(there_and_back)
return Transform(self, target, **kwargs)
return self

View File

@@ -63,12 +63,14 @@ class PiCreatureScene(Scene):
return self.pi_creatures[0]
def any_pi_creatures_on_screen(self):
mobjects = self.get_mobjects()
return any([pi in mobjects for pi in self.get_pi_creatures()])
return len(self.get_on_screen_pi_creatures()) > 0
def get_on_screen_pi_creatures(self):
mobjects = self.get_mobjects()
return VGroup(*[pi for pi in self.get_pi_creatures() if pi in mobjects])
mobjects = self.get_mobject_family_members()
return VGroup(*[
pi for pi in self.get_pi_creatures()
if pi in mobjects
])
def introduce_bubble(self, *args, **kwargs):
if isinstance(args[0], PiCreature):

View File

@@ -48,16 +48,18 @@ class Axes(VGroup):
config.update(extra_config)
return NumberLine(x_min=min_val, x_max=max_val, **config)
def coords_to_point(self, x, y):
def coords_to_point(self, *coords):
origin = self.x_axis.number_to_point(0)
x_axis_projection = self.x_axis.number_to_point(x)
y_axis_projection = self.y_axis.number_to_point(y)
return x_axis_projection + y_axis_projection - origin
result = np.array(origin)
for axis, coord in zip(self, coords):
result += (axis.number_to_point(coord) - origin)
return result
def point_to_coords(self, point):
return tuple([
axis.point_to_number(point)
for axis in self
if isinstance(axis, NumberLine)
])
def get_graph(
@@ -137,9 +139,10 @@ class ThreeDAxes(Axes):
def add_3d_pieces(self):
for axis in self:
axis.add(VGroup(
axis.pieces = VGroup(
*axis.main_line.get_pieces(self.num_axis_pieces)
))
)
axis.add(axis.pieces)
axis.main_line.set_stroke(width=0, family=False)
for submob in axis.family_members_with_points():
submob.shade_in_3d = True
@@ -364,10 +367,11 @@ class ComplexPlane(NumberPlane):
numbers += [
complex(0, y)
for y in range(-int(self.y_radius), int(self.y_radius) + 1)
if y != 0
]
for number in numbers:
if number == complex(0, 0):
continue
# if number == complex(0, 0):
# continue
point = self.number_to_point(number)
num_str = str(number).replace("j", "i")
if num_str.startswith("0"):

View File

@@ -600,7 +600,7 @@ class Arrow(Line):
return self
def get_normal_vector(self):
p0, p1, p2 = self.tip[0].get_anchors()
p0, p1, p2 = self.tip[0].get_anchors()[:3]
result = np.cross(p2 - p1, p1 - p0)
norm = get_norm(result)
if norm == 0:

View File

@@ -170,8 +170,11 @@ class Mobject(Container):
def get_updaters(self):
return self.updaters
def add_updater(self, update_function):
def add_updater(self, update_function, call_updater=True):
self.updaters.append(update_function)
if call_updater:
self.update(0)
return self
def remove_updater(self, update_function):
while update_function in self.updaters:
@@ -854,11 +857,11 @@ class Mobject(Container):
submob.shuffle_submobjects(recursive=True)
random.shuffle(self.submobjects)
def print_get_family(self, n_tabs=0):
def print_submobject_family(self, n_tabs=0):
"""For debugging purposes"""
print("\t" * n_tabs, self, id(self))
for submob in self.submobjects:
submob.print_get_family(n_tabs + 1)
submob.print_submobject_family(n_tabs + 1)
# Alignment
def align_data(self, mobject):
@@ -972,14 +975,15 @@ class Mobject(Container):
def pointwise_become_partial(self, mobject, a, b):
pass # To implement in subclass
def become(self, mobject):
def become(self, mobject, copy_submobjects=True):
"""
Edit points, colors and submobjects to be idential
to another mobject
"""
self.align_points(mobject)
self.interpolate(self, mobject, 1)
self.submobjects = [sm.copy() for sm in mobject.submobjects]
self.align_data(mobject)
for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
sm1.interpolate(sm1, sm2, 1)
return self
class Group(Mobject):

View File

@@ -2,7 +2,7 @@
from constants import *
from mobject.svg.tex_mobject import TexMobject
from mobject.numbers import DecimalNumber
from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from mobject.geometry import Arrow
@@ -10,6 +10,8 @@ from mobject.geometry import Line
from utils.bezier import interpolate
from utils.config_ops import digest_config
from utils.space_ops import get_norm
from utils.space_ops import normalize
from utils.simple_functions import fdiv
class NumberLine(VMobject):
@@ -31,6 +33,9 @@ class NumberLine(VMobject):
"line_to_number_buff": MED_SMALL_BUFF,
"include_tip": False,
"propagate_style_to_family": True,
"decimal_number_config": {
"num_decimal_places": 0,
}
}
def __init__(self, **kwargs):
@@ -118,11 +123,11 @@ class NumberLine(VMobject):
# TODO, handle decimals
if len(numbers) == 0:
numbers = self.default_numbers_to_display()
if "force_integers" in kwargs and kwargs["force_integers"]:
numbers = list(map(int, numbers))
result = VGroup()
for number in numbers:
mob = TexMobject(str(number))
mob = DecimalNumber(
number, **self.decimal_number_config
)
mob.scale(self.number_scale_val)
mob.next_to(
self.number_to_point(number),

View File

@@ -1,11 +1,7 @@
from constants import *
import operator as op
from mobject.svg.tex_mobject import SingleStringTexMobject
from mobject.types.vectorized_mobject import VMobject
from functools import reduce
class DecimalNumber(VMobject):
@@ -17,6 +13,7 @@ class DecimalNumber(VMobject):
"show_ellipsis": False,
"unit": None, # Aligned to bottom unless it starts with "^"
"include_background_rectangle": False,
"edge_to_fix": LEFT,
}
def __init__(self, number, **kwargs):
@@ -30,7 +27,8 @@ class DecimalNumber(VMobject):
formatter = self.get_formatter()
num_string = formatter.format(number)
if num_string.startswith("-") and number == 0:
shows_zero = np.round(number, self.num_decimal_places) == 0
if num_string.startswith("-") and shows_zero:
num_string = num_string[1:]
self.add(*[
@@ -101,14 +99,20 @@ class DecimalNumber(VMobject):
])
def set_value(self, number, **config):
full_config = dict(self.initial_config)
full_config = dict(self.CONFIG)
full_config.update(self.initial_config)
full_config.update(config)
new_decimal = DecimalNumber(number, **full_config)
new_decimal.match_height(self)
new_decimal.move_to(self, LEFT)
new_decimal.move_to(self, self.edge_to_fix)
new_decimal.match_style(self)
old_family = self.get_family()
self.submobjects = new_decimal.submobjects
for mob in old_family:
# Dumb hack...due to how scene handles families
# of animated mobjects
mob.points[:] = 0
self.number = number
def get_value(self):

View File

@@ -81,7 +81,10 @@ class SingleStringTexMobject(SVGMobject):
# Handle imbalanced \left and \right
num_lefts, num_rights = [
len([s for s in tex.split(substr)[1:] if s[0] in "(){}[]|.\\"])
len([
s for s in tex.split(substr)[1:]
if s and s[0] in "(){}[]|.\\"
])
for substr in ("\\left", "\\right")
]
if num_lefts != num_rights:

View File

@@ -44,7 +44,7 @@ class ParametricSurface(VGroup):
def setup_in_uv_space(self):
res = tuplify(self.resolution)
if len(res) == 1:
u_res = v_res = res
u_res = v_res = res[0]
else:
u_res, v_res = res
u_min = self.u_min

View File

@@ -640,7 +640,7 @@ class VectorizedPoint(VMobject):
return self.artificial_height
def get_location(self):
return self.get_anchors()[0]
return self.points[0]
def set_location(self, new_loc):
self.set_points(np.array([new_loc]))

View File

@@ -1273,7 +1273,7 @@ class WrapCosineGraphAroundCircle(FourierMachineScene):
def get_winding_frequency_label(self):
freq = self.initial_winding_frequency
winding_freq_label = VGroup(
DecimalNumber(freq, num_decimal_places = 2),
DecimalNumber(freq, num_decimal_places=2),
TextMobject("cycles/second")
)
winding_freq_label.arrange_submobjects(RIGHT)
@@ -1639,8 +1639,14 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
added_anims = kwargs.get("added_anims", [])
anims = [self.get_frequency_change_animation(self.graph, new_freq)]
if hasattr(self, "winding_freq_label"):
freq_label = [sm for sm in self.winding_freq_label if isinstance(sm, DecimalNumber)][0]
anims.append(ChangeDecimalToValue(freq_label, new_freq))
freq_label = [
sm for sm in self.winding_freq_label
if isinstance(sm, DecimalNumber)
][0]
self.add(freq_label)
anims.append(
ChangeDecimalToValue(freq_label, new_freq)
)
if hasattr(self, "v_lines_indicating_periods"):
anims.append(self.get_period_v_lines_update_anim())
if hasattr(self, "center_of_mass_dot"):
@@ -1838,8 +1844,8 @@ class ShowLinearity(DrawFrequencyPlot):
"high_freq_color": YELLOW,
"low_freq_color": PINK,
"sum_color": GREEN,
"low_freq" : 2.0,
"high_freq" : 3.0,
"low_freq" : 3.0,
"high_freq" : 4.0,
"circle_plane_config" : {
"x_radius" : 2.5,
"y_radius" : 2.7,

View File

@@ -350,7 +350,7 @@ class Scene(Container):
# point forward.
animation_mobjects = [anim.mobject for anim in animations]
ca_mobjects = [ca.mobject for ca in self.continual_animations]
mobjects = self.get_mobjects()
mobjects = self.get_mobject_family_members()
for i, mob in enumerate(mobjects):
update_possibilities = [
mob in animation_mobjects,

View File

@@ -22,7 +22,7 @@ class ThreeDScene(Scene):
if gamma is not None:
self.camera.set_gamma(gamma)
def begin_ambient_camera_rotation(self, rate=0.1):
def begin_ambient_camera_rotation(self, rate=0.05):
self.ambient_camera_rotation = ContinualGrowValue(
self.camera.theta_tracker,
rate=rate
@@ -73,8 +73,8 @@ class ThreeDScene(Scene):
return self.mobjects
return moving_mobjects
def add_fixed_orientation_mobjects(self, *mobjects):
self.camera.add_fixed_orientation_mobjects(*mobjects)
def add_fixed_orientation_mobjects(self, *mobjects, **kwargs):
self.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs)
def add_fixed_in_frame_mobjects(self, *mobjects):
self.camera.add_fixed_in_frame_mobjects(*mobjects)