3b1b-manim/mobject/function_graphs.py

325 lines
11 KiB
Python
Raw Normal View History

2015-06-10 22:00:35 -07:00
import numpy as np
import itertools as it
from mobject import Mobject, Mobject1D, Mobject2D, CompoundMobject
2015-10-12 19:39:46 -07:00
from simple_mobjects import Arrow, Line
from image_mobject import tex_mobject
2015-06-10 22:00:35 -07:00
from constants import *
from helpers import *
class FunctionGraph(Mobject1D):
DEFAULT_CONFIG = {
"color" : "lightblue",
"x_min" : -10,
"x_max" : 10,
2015-10-06 15:27:40 -07:00
"spatial_radius" : SPACE_WIDTH,
}
def __init__(self, function, **kwargs):
digest_config(self, FunctionGraph, kwargs, locals())
Mobject1D.__init__(self, **kwargs)
2015-06-10 22:00:35 -07:00
def generate_points(self):
numerical_radius = (self.x_max - self.x_min)/2
numerical_center = (self.x_max + self.x_min)/2
2015-10-06 15:27:40 -07:00
ratio = numerical_radius / self.spatial_radius
epsilon = self.epsilon * ratio
2015-06-10 22:00:35 -07:00
self.add_points([
np.array([(x-numerical_center)/ratio, self.function(x), 0])
2015-06-10 22:00:35 -07:00
for x in np.arange(self.x_min, self.x_max, self.epsilon)
])
class ParametricFunction(Mobject):
DEFAULT_CONFIG = {
"color" : "white",
"dim" : 1,
"expected_measure" : 10.0,
"density" : None
}
def __init__(self, function, **kwargs):
digest_config(self, ParametricFunction, kwargs, locals())
if self.density:
self.epsilon = 1.0 / self.density
2015-06-10 22:00:35 -07:00
elif self.dim == 1:
self.epsilon = 1.0 / self.expected_measure / DEFAULT_POINT_DENSITY_1D
2015-06-10 22:00:35 -07:00
else:
self.epsilon = 1.0 / np.sqrt(self.expected_measure) / DEFAULT_POINT_DENSITY_2D
2015-06-10 22:00:35 -07:00
Mobject.__init__(self, *args, **kwargs)
def generate_points(self):
if self.dim == 1:
self.add_points([
self.function(t)
for t in np.arange(-1, 1, self.epsilon)
])
if self.dim == 2:
self.add_points([
self.function(s, t)
for t in np.arange(-1, 1, self.epsilon)
for s in np.arange(-1, 1, self.epsilon)
])
class NumberLine(Mobject1D):
DEFAULT_CONFIG = {
"color" : "skyblue",
"numerical_radius" : SPACE_WIDTH,
2015-10-06 15:27:40 -07:00
"unit_length_to_spatial_width" : 1,
"tick_size" : 0.1,
"tick_frequency" : 0.5,
"leftmost_tick" : None,
"number_at_center" : 0,
"numbers_with_elongated_ticks" : [0],
"longer_tick_multiple" : 2,
}
def __init__(self, **kwargs):
digest_config(self, NumberLine, kwargs)
if self.leftmost_tick is None:
self.leftmost_tick = -int(self.numerical_radius)
self.left_num = self.number_at_center - self.numerical_radius
self.right_num = self.number_at_center + self.numerical_radius
Mobject1D.__init__(self, **kwargs)
2015-06-10 22:00:35 -07:00
def generate_points(self):
2015-10-06 15:27:40 -07:00
spatial_radius = self.numerical_radius*self.unit_length_to_spatial_width
2015-06-10 22:00:35 -07:00
self.add_points([
(b*x, 0, 0)
2015-10-06 15:27:40 -07:00
for x in np.arange(0, spatial_radius, self.epsilon)
for b in [-1, 1]
2015-06-10 22:00:35 -07:00
])
self.index_of_left = np.argmin(self.points[:,0])
self.index_of_right = np.argmax(self.points[:,0])
2015-10-06 15:27:40 -07:00
spatial_tick_frequency = self.tick_frequency*self.unit_length_to_spatial_width
2015-06-10 22:00:35 -07:00
self.add_points([
(x, y, 0)
for num in self.get_tick_numbers()
2015-06-10 22:00:35 -07:00
for y in np.arange(-self.tick_size, self.tick_size, self.epsilon)
for x in [self.number_to_point(num)[0]]
2015-06-10 22:00:35 -07:00
])
for number in self.numbers_with_elongated_ticks:
self.elongate_tick_at(number, self.longer_tick_multiple)
self.number_of_points_without_numbers = self.get_num_points()
def get_tick_numbers(self):
return np.arange(self.leftmost_tick, self.right_num, self.tick_frequency)
def elongate_tick_at(self, number, multiple = 2):
x = self.number_to_point(number)[0]
self.add_points([
[x, y, 0]
for y in np.arange(
-multiple*self.tick_size,
multiple*self.tick_size,
self.epsilon
)
])
return self
def number_to_point(self, number):
return interpolate(
self.points[self.index_of_left],
self.points[self.index_of_right],
float(number-self.left_num)/(self.right_num - self.left_num)
)
def point_to_number(self, point):
2015-10-06 15:27:40 -07:00
return self.number_at_center + point[0]/self.unit_length_to_spatial_width
def default_numbers_to_display(self):
return self.get_tick_numbers()[::2]
def get_vertical_number_offset(self):
return 4*DOWN*self.tick_size
def get_number_mobjects(self, *numbers):
#TODO, handle decimals
if len(numbers) == 0:
numbers = self.default_numbers_to_display()
result = []
for number in numbers:
mob = tex_mobject(str(int(number)))
vert_scale = 2*self.tick_size/mob.get_height()
2015-10-06 15:27:40 -07:00
hori_scale = self.tick_frequency*self.unit_length_to_spatial_width/mob.get_width()
mob.scale(min(vert_scale, hori_scale))
mob.shift(self.number_to_point(number))
mob.shift(self.get_vertical_number_offset())
result.append(mob)
return result
def add_numbers(self, *numbers):
self.add(*self.get_number_mobjects(*numbers))
return self
def remove_numbers(self):
self.points = self.points[:self.number_of_points_without_numbers]
self.rgbs = self.rgbs[:self.number_of_points_without_numbers]
return self
2015-06-10 22:00:35 -07:00
class UnitInterval(NumberLine):
DEFAULT_CONFIG = {
"numerical_radius" : 0.5,
2015-10-06 15:27:40 -07:00
"unit_length_to_spatial_width" : 2*(SPACE_WIDTH-1),
"tick_frequency" : 0.1,
"leftmost_tick" : 0,
"number_at_center" : 0.5,
"numbers_with_elongated_ticks" : [0, 1],
}
def __init__(self, **kwargs):
digest_config(self, UnitInterval, kwargs)
NumberLine.__init__(self, **kwargs)
2015-06-27 04:49:10 -07:00
class Axes(CompoundMobject):
def __init__(self, **kwargs):
x_axis = NumberLine(**kwargs)
y_axis = NumberLine(**kwargs).rotate(np.pi/2, OUT)
2015-06-27 04:49:10 -07:00
CompoundMobject.__init__(self, x_axis, y_axis)
2015-10-06 15:27:40 -07:00
class NumberPlane(Mobject1D):
DEFAULT_CONFIG = {
"color" : "skyblue",
"x_radius" : SPACE_WIDTH,
"y_radius" : SPACE_HEIGHT,
"x_unit_to_spatial_width" : 1,
"y_uint_to_spatial_height" : 1,
"x_line_frequency" : 1,
"x_faded_line_frequency" : 0.5,
"y_line_frequency" : 1,
"y_faded_line_frequency" : 0.5,
"fade_factor" : 0.3,
"number_scale_factor" : 0.25,
"num_pair_at_center" : np.array((0, 0)),
}
def __init__(self, **kwargs):
digest_config(self, NumberPlane, kwargs)
Mobject1D.__init__(self, **kwargs)
def generate_points(self):
color = self.color
faded = Color(rgb = self.fade_factor*np.array(color.get_rgb()))
#Vertical Lines
2015-10-06 18:41:26 -07:00
freq_color_pairs = [
(self.x_faded_line_frequency, faded),
(self.x_line_frequency, color)
]
for freq, color in freq_color_pairs:
if not freq:
continue
2015-10-06 15:27:40 -07:00
self.add_points([
(sgns[0]*self.x_unit_to_spatial_width*x, sgns[1]*y, 0)
for x in np.arange(0, self.x_radius, freq)
for y in np.arange(0, self.y_radius, self.epsilon)
for sgns in it.product([-1, 1], [-1, 1])
2015-10-06 18:41:26 -07:00
], color = color)
2015-10-06 15:27:40 -07:00
#Horizontal lines
2015-10-06 18:41:26 -07:00
freq_color_pairs = [
(self.y_faded_line_frequency, faded),
(self.y_line_frequency, color)
]
for freq, color in freq_color_pairs:
if not freq:
continue
2015-10-06 15:27:40 -07:00
self.add_points([
(sgns[0]*x, sgns[1]*self.y_uint_to_spatial_height*y, 0)
for x in np.arange(0, self.x_radius, self.epsilon)
for y in np.arange(0, self.y_radius, freq)
for sgns in it.product([-1, 1], [-1, 1])
2015-10-06 18:41:26 -07:00
], color = color)
2015-10-06 15:27:40 -07:00
self.shift(self.get_center_point())
def get_center_point(self):
return self.num_pair_to_point(self.num_pair_at_center)
def num_pair_to_point(self, pair):
pair = pair + self.num_pair_at_center
return pair[0]*self.x_unit_to_spatial_width*RIGHT + \
pair[1]*self.y_uint_to_spatial_height*UP
def get_coordinate_labels(self, x_vals = None, y_vals = None):
result = []
nudge = 0.1*(DOWN+RIGHT)
if x_vals == None and y_vals == None:
x_vals = range(-int(self.x_radius), int(self.x_radius))
y_vals = range(-int(self.y_radius), int(self.y_radius))
for index, vals in zip([0, 1], [x_vals, y_vals]):
num_pair = [0, 0]
for val in vals:
num_pair[index] = val
point = self.num_pair_to_point(num_pair)
num = tex_mobject(str(val))
num.scale(self.number_scale_factor)
num.shift(point-num.get_corner(UP+LEFT)+nudge)
result.append(num)
return result
def add_coordinates(self, x_vals = None, y_vals = None):
self.add(*self.get_coordinate_labels(x_vals, y_vals))
return self
2015-06-27 04:49:10 -07:00
2015-10-12 19:39:46 -07:00
def get_vector(self, coords, **kwargs):
if len(coords) == 2:
coords = tuple(list(coords) + [0])
arrow = Arrow(ORIGIN, coords, **kwargs)
arrow.remove_tip()
arrow.align_data(Line(ORIGIN, SPACE_WIDTH*LEFT))
arrow.add_tip()
return arrow
2015-10-06 15:27:40 -07:00
class ComplexPlane(NumberPlane):
2015-10-12 19:39:46 -07:00
DEFAULT_CONFIG = {
"color" : "lightgreen",
"unit_to_spatial_width" : 1,
"line_frequency" : 1,
"faded_line_frequency" : 0.5,
"number_at_center" : complex(0),
}
def __init__(self, **kwargs):
digest_config(self, ComplexPlane, kwargs)
kwargs.update({
"x_unit_to_spatial_width" : self.unit_to_spatial_width,
"y_uint_to_spatial_height" : self.unit_to_spatial_width,
"x_line_frequency" : self.line_frequency,
"x_faded_line_frequency" : self.faded_line_frequency,
"y_line_frequency" : self.line_frequency,
"y_faded_line_frequency" : self.faded_line_frequency,
"num_pair_at_center" : (self.number_at_center.real, self.number_at_center.imag),
})
NumberPlane.__init__(self, **kwargs)
def number_to_point(self, number):
number = complex(number)
return self.num_pair_to_point((number.real, number.imag))
def get_coordinate_labels(self, *numbers):
result = []
nudge = 0.1*(DOWN+RIGHT)
if len(numbers) == 0:
numbers = range(-int(self.x_radius), int(self.x_radius))
numbers += [
complex(0, y)
for y in range(-int(self.y_radius), int(self.y_radius))
]
for number in numbers:
point = self.number_to_point(number)
if number == 0:
num_str = "0"
else:
num_str = str(number).replace("j", "i")
num = tex_mobject(num_str)
num.scale(self.number_scale_factor)
num.shift(point-num.get_corner(UP+LEFT)+nudge)
result.append(num)
return result
def add_coordinates(self, *numbers):
self.add(*self.get_coordinate_labels(*numbers))
return self
2015-06-27 04:49:10 -07:00
2015-06-10 22:00:35 -07:00