3b1b-manim/topics/fractals.py

662 lines
20 KiB
Python
Raw Normal View History

2016-11-22 15:16:40 -08:00
# from mobject import Mobject, Point, Mobject1D
from mobject.vectorized_mobject import VMobject, VGroup, VectorizedPoint
2015-12-13 15:42:20 -08:00
from scene import Scene
from animation.transform import Transform
from animation.simple_animations import ShowCreation
2017-01-25 12:59:46 -08:00
from topics.geometry import Line, Polygon, RegularPolygon, Square, Circle
2017-01-24 14:55:05 -08:00
from characters import PiCreature, Randolph, get_all_pi_creature_modes
2015-12-13 15:42:20 -08:00
from helpers import *
def rotate(points, angle = np.pi, axis = OUT):
if axis is None:
return points
matrix = rotation_matrix(angle, axis)
points = np.dot(points, np.transpose(matrix))
return points
2017-01-17 17:14:32 -08:00
def fractalify(vmobject, order = 3, *args, **kwargs):
for x in range(order):
fractalification_iteration(vmobject)
return vmobject
2017-01-24 14:55:05 -08:00
def fractalification_iteration(vmobject, dimension = 1.05, num_inserted_anchors_range = range(1, 4)):
2017-01-17 17:14:32 -08:00
num_points = vmobject.get_num_points()
if num_points > 0:
# original_anchors = vmobject.get_anchors()
original_anchors = [
vmobject.point_from_proportion(x)
for x in np.linspace(0, 1-1./num_points, num_points)
2017-01-17 17:14:32 -08:00
]
new_anchors = []
for p1, p2, in zip(original_anchors, original_anchors[1:]):
num_inserts = random.choice(num_inserted_anchors_range)
inserted_points = [
interpolate(p1, p2, alpha)
for alpha in np.linspace(0, 1, num_inserts+2)[1:-1]
]
mass_scaling_factor = 1./(num_inserts+1)
length_scaling_factor = mass_scaling_factor**(1./dimension)
target_length = np.linalg.norm(p1-p2)*length_scaling_factor
curr_length = np.linalg.norm(p1-p2)*mass_scaling_factor
#offset^2 + curr_length^2 = target_length^2
offset_len = np.sqrt(target_length**2 - curr_length**2)
unit_vect = (p1-p2)/np.linalg.norm(p1-p2)
offset_unit_vect = rotate_vector(unit_vect, np.pi/2)
inserted_points = [
point + u*offset_len*offset_unit_vect
for u, point in zip(it.cycle([-1, 1]), inserted_points)
]
new_anchors += [p1] + inserted_points
new_anchors.append(original_anchors[-1])
vmobject.set_points_as_corners(new_anchors)
vmobject.submobjects = [
fractalification_iteration(submob, dimension, num_inserted_anchors_range)
for submob in vmobject.submobjects
]
return vmobject
2015-12-13 15:42:20 -08:00
2017-01-16 11:43:59 -08:00
class SelfSimilarFractal(VMobject):
CONFIG = {
"order" : 5,
"num_subparts" : 3,
"height" : 4,
"colors" : [RED, WHITE],
"stroke_width" : 1,
"fill_opacity" : 1,
2018-01-15 18:16:50 -08:00
"propagate_style_to_family" : True,
2017-01-16 11:43:59 -08:00
}
def init_colors(self):
VMobject.init_colors(self)
self.set_color_by_gradient(*self.colors)
2017-01-16 11:43:59 -08:00
def generate_points(self):
2017-01-16 13:26:46 -08:00
order_n_self = self.get_order_n_self(self.order)
if self.order == 0:
self.submobjects = [order_n_self]
else:
self.submobjects = order_n_self.submobjects
return self
2017-01-16 11:43:59 -08:00
def get_order_n_self(self, order):
if order == 0:
result = self.get_seed_shape()
else:
lower_order = self.get_order_n_self(order - 1)
2017-01-16 11:43:59 -08:00
subparts = [
lower_order.copy()
2017-01-16 11:43:59 -08:00
for x in range(self.num_subparts)
]
self.arrange_subparts(*subparts)
result = VGroup(*subparts)
result.scale_to_fit_height(self.height)
result.center()
return result
def get_seed_shape(self):
raise Exception("Not implemented")
def arrange_subparts(self, *subparts):
raise Exception("Not implemented")
class Sierpinski(SelfSimilarFractal):
def get_seed_shape(self):
return Polygon(
RIGHT, np.sqrt(3)*UP, LEFT,
)
def arrange_subparts(self, *subparts):
tri1, tri2, tri3 = subparts
tri1.move_to(tri2.get_corner(DOWN+LEFT), UP)
tri3.move_to(tri2.get_corner(DOWN+RIGHT), UP)
class DiamondFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 4,
2017-01-16 13:26:46 -08:00
"height" : 4,
"colors" : [GREEN_E, YELLOW],
2017-01-16 11:43:59 -08:00
}
def get_seed_shape(self):
return RegularPolygon(n = 4)
def arrange_subparts(self, *subparts):
# VGroup(*subparts).rotate(np.pi/4)
for part, vect in zip(subparts, compass_directions(start_vect = UP+RIGHT)):
part.next_to(ORIGIN, vect, buff = 0)
VGroup(*subparts).rotate(np.pi/4, about_point = ORIGIN)
2017-01-16 11:43:59 -08:00
2017-01-17 17:14:32 -08:00
class PentagonalFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 5,
"colors" : [MAROON_B, YELLOW, RED],
"height" : 6,
2017-01-17 17:14:32 -08:00
}
def get_seed_shape(self):
return RegularPolygon(n = 5, start_angle = np.pi/2)
def arrange_subparts(self, *subparts):
for x, part in enumerate(subparts):
part.shift(0.95*part.get_height()*UP)
part.rotate(2*np.pi*x/5, about_point = ORIGIN)
2017-01-17 17:14:32 -08:00
2017-01-24 14:55:05 -08:00
class PentagonalPiCreatureFractal(PentagonalFractal):
def init_colors(self):
SelfSimilarFractal.init_colors(self)
internal_pis = [
pi
for pi in self.submobject_family()
if isinstance(pi, PiCreature)
]
colors = color_gradient(self.colors, len(internal_pis))
for pi, color in zip(internal_pis, colors):
pi.init_colors()
pi.body.set_stroke(color, width = 0.5)
2018-03-30 11:51:31 -07:00
pi.set_color(color)
def get_seed_shape(self):
return Randolph(mode = "shruggie")
def arrange_subparts(self, *subparts):
for part in subparts:
part.rotate(2*np.pi/5, about_point = ORIGIN)
PentagonalFractal.arrange_subparts(self, *subparts)
2017-01-17 17:14:32 -08:00
2017-01-24 14:55:05 -08:00
class PiCreatureFractal(VMobject):
CONFIG = {
"order" : 7,
2017-01-25 12:59:46 -08:00
"scale_val" : 2.5,
2017-01-24 14:55:05 -08:00
"start_mode" : "hooray",
"height" : 6,
"colors" : [
BLUE_D, BLUE_B, MAROON_B, MAROON_D, GREY,
YELLOW, RED, GREY_BROWN, RED, RED_E,
],
"random_seed" : 0,
"stroke_width" : 0,
}
def init_colors(self):
VMobject.init_colors(self)
internal_pis = [
pi
for pi in self.submobject_family()
if isinstance(pi, PiCreature)
]
random.seed(self.random_seed)
for pi in reversed(internal_pis):
color = random.choice(self.colors)
2018-03-30 11:51:31 -07:00
pi.set_color(color)
2017-01-24 14:55:05 -08:00
pi.set_stroke(color, width = 0)
def generate_points(self):
random.seed(self.random_seed)
modes = get_all_pi_creature_modes()
seed = PiCreature(mode = self.start_mode)
seed.scale_to_fit_height(self.height)
seed.to_edge(DOWN)
creatures = [seed]
2017-01-25 12:59:46 -08:00
self.add(VGroup(seed))
2017-01-24 14:55:05 -08:00
for x in range(self.order):
new_creatures = []
for creature in creatures:
2017-01-25 12:59:46 -08:00
for eye, vect in zip(creature.eyes, [LEFT, RIGHT]):
2017-01-24 14:55:05 -08:00
new_creature = PiCreature(
mode = random.choice(modes)
)
2017-01-25 12:59:46 -08:00
new_creature.scale_to_fit_height(
self.scale_val*eye.get_height()
)
new_creature.next_to(
eye, vect,
buff = 0,
aligned_edge = DOWN
2017-01-24 14:55:05 -08:00
)
new_creatures.append(new_creature)
2017-01-25 12:59:46 -08:00
creature.look_at(random.choice(new_creatures))
2017-01-24 14:55:05 -08:00
self.add_to_back(VGroup(*new_creatures))
creatures = new_creatures
# def init_colors(self):
# VMobject.init_colors(self)
# self.set_color_by_gradient(*self.colors)
2017-01-24 14:55:05 -08:00
class WonkyHexagonFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 7
}
def get_seed_shape(self):
return RegularPolygon(n=6)
def arrange_subparts(self, *subparts):
for i, piece in enumerate(subparts):
piece.rotate(i*np.pi/12, about_point = ORIGIN)
2017-01-24 14:55:05 -08:00
p1, p2, p3, p4, p5, p6, p7 = subparts
center_row = VGroup(p1, p4, p7)
center_row.arrange_submobjects(RIGHT, buff = 0)
for p in p2, p3, p5, p6:
p.scale_to_fit_width(p1.get_width())
p2.move_to(p1.get_top(), DOWN+LEFT)
p3.move_to(p1.get_bottom(), UP+LEFT)
p5.move_to(p4.get_top(), DOWN+LEFT)
p6.move_to(p4.get_bottom(), UP+LEFT)
2017-01-25 12:59:46 -08:00
class CircularFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 3,
"colors" : [GREEN, BLUE, GREY]
}
def get_seed_shape(self):
return Circle()
2017-01-24 14:55:05 -08:00
2017-01-25 12:59:46 -08:00
def arrange_subparts(self, *subparts):
if not hasattr(self, "been_here"):
self.num_subparts = 3+self.order
self.been_here = True
for i, part in enumerate(subparts):
theta = np.pi/self.num_subparts
part.next_to(
ORIGIN, UP,
buff = self.height/(2*np.tan(theta))
)
part.rotate(i*2*np.pi/self.num_subparts, about_point = ORIGIN)
2017-01-25 12:59:46 -08:00
self.num_subparts -= 1
2017-01-24 14:55:05 -08:00
2017-01-16 11:43:59 -08:00
######## Space filling curves ############
2017-01-25 12:59:46 -08:00
class JaggedCurvePiece(VMobject):
def insert_n_anchor_points(self, n):
if self.get_num_anchor_points() == 0:
self.points = np.zeros((1, 3))
anchors = self.get_anchors()
indices = np.linspace(0, len(anchors)-1, n+len(anchors)).astype('int')
self.set_points_as_corners(anchors[indices])
2017-01-17 17:14:32 -08:00
class FractalCurve(VMobject):
2016-02-27 16:32:53 -08:00
CONFIG = {
2015-12-13 15:42:20 -08:00
"radius" : 3,
"order" : 5,
2017-01-05 11:57:30 -08:00
"colors" : [RED, GREEN],
2017-01-25 12:59:46 -08:00
"num_submobjects" : 20,
2017-01-16 11:43:59 -08:00
"monochromatic" : False,
2017-01-25 12:59:46 -08:00
"order_to_stroke_width_map" : {
3 : 3,
4 : 2,
5 : 1,
},
2018-01-15 18:16:50 -08:00
"propagate_style_to_family" : True,
2015-12-13 15:42:20 -08:00
}
def generate_points(self):
points = self.get_anchor_points()
2017-01-25 12:59:46 -08:00
self.set_points_as_corners(points)
if not self.monochromatic:
alphas = np.linspace(0, 1, self.num_submobjects)
for alpha_pair in zip(alphas, alphas[1:]):
submobject = JaggedCurvePiece()
submobject.pointwise_become_partial(
self, *alpha_pair
)
self.add(submobject)
self.points = np.zeros((0, 3))
2017-01-17 17:14:32 -08:00
def init_colors(self):
VMobject.init_colors(self)
self.set_color_by_gradient(*self.colors)
2017-01-25 12:59:46 -08:00
for order in sorted(self.order_to_stroke_width_map.keys()):
if self.order >= order:
self.set_stroke(width = self.order_to_stroke_width_map[order])
2015-12-13 15:42:20 -08:00
def get_anchor_points(self):
raise Exception("Not implemented")
2017-01-17 17:14:32 -08:00
class LindenmayerCurve(FractalCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2015-12-13 15:42:20 -08:00
"axiom" : "A",
"rule" : {},
"scale_factor" : 2,
"radius" : 3,
"start_step" : RIGHT,
"angle" : np.pi/2,
}
def expand_command_string(self, command):
result = ""
for letter in command:
if letter in self.rule:
result += self.rule[letter]
else:
result += letter
return result
def get_command_string(self):
result = self.axiom
for x in range(self.order):
result = self.expand_command_string(result)
return result
def get_anchor_points(self):
step = float(self.radius) * self.start_step
step /= (self.scale_factor**self.order)
curr = np.zeros(3)
result = [curr]
for letter in self.get_command_string():
if letter is "+":
step = rotate(step, self.angle)
elif letter is "-":
step = rotate(step, -self.angle)
else:
curr = curr + step
result.append(curr)
return np.array(result) - center_of_mass(result)
2017-01-17 17:14:32 -08:00
class SelfSimilarSpaceFillingCurve(FractalCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2015-12-13 15:42:20 -08:00
"offsets" : [],
#keys must awkwardly be in string form...
"offset_to_rotation_axis" : {},
"scale_factor" : 2,
"radius_scale_factor" : 0.5,
}
def transform(self, points, offset):
"""
How to transform the copy of points shifted by
offset. Generally meant to be extended in subclasses
"""
copy = np.array(points)
if str(offset) in self.offset_to_rotation_axis:
copy = rotate(
copy,
axis = self.offset_to_rotation_axis[str(offset)]
)
copy /= self.scale_factor,
copy += offset*self.radius*self.radius_scale_factor
return copy
def refine_into_subparts(self, points):
transformed_copies = [
self.transform(points, offset)
for offset in self.offsets
]
return reduce(
lambda a, b : np.append(a, b, axis = 0),
transformed_copies
)
def get_anchor_points(self):
points = np.zeros((1, 3))
for count in range(self.order):
points = self.refine_into_subparts(points)
return points
def generate_grid(self):
raise Exception("Not implemented")
2015-12-13 15:42:20 -08:00
class HilbertCurve(SelfSimilarSpaceFillingCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2015-12-13 15:42:20 -08:00
"offsets" : [
LEFT+DOWN,
LEFT+UP,
RIGHT+UP,
RIGHT+DOWN,
],
"offset_to_rotation_axis" : {
str(LEFT+DOWN) : RIGHT+UP,
str(RIGHT+DOWN) : RIGHT+DOWN,
},
}
class HilbertCurve3D(SelfSimilarSpaceFillingCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-12-20 20:31:02 +08:00
"offsets" : [
RIGHT+DOWN+IN,
LEFT+DOWN+IN,
LEFT+DOWN+OUT,
RIGHT+DOWN+OUT,
RIGHT+UP+OUT,
LEFT+UP+OUT,
LEFT+UP+IN,
RIGHT+UP+IN,
2015-12-13 15:42:20 -08:00
],
2017-12-20 20:31:02 +08:00
"offset_to_rotation_axis_and_angle" : {
str(RIGHT+DOWN+IN) : (LEFT+UP+OUT , 2*np.pi/3),
str(LEFT+DOWN+IN) : (RIGHT+DOWN+IN, 2*np.pi/3),
str(LEFT+DOWN+OUT) : (RIGHT+DOWN+IN, 2*np.pi/3),
str(RIGHT+DOWN+OUT) : (UP , np.pi ),
str(RIGHT+UP+OUT) : (UP , np.pi ),
str(LEFT+UP+OUT) : (LEFT+DOWN+OUT, 2*np.pi/3),
str(LEFT+UP+IN) : (LEFT+DOWN+OUT, 2*np.pi/3),
str(RIGHT+UP+IN) : (RIGHT+UP+IN , 2*np.pi/3),
},
2015-12-13 15:42:20 -08:00
}
2017-12-20 20:31:02 +08:00
# Rewrote transform method to include the rotation angle
def transform(self, points, offset):
copy = np.array(points)
copy = rotate(
copy,
axis = self.offset_to_rotation_axis_and_angle[str(offset)][0],
angle = self.offset_to_rotation_axis_and_angle[str(offset)][1],
)
copy /= self.scale_factor,
copy += offset*self.radius*self.radius_scale_factor
return copy
2015-12-13 15:42:20 -08:00
class PeanoCurve(SelfSimilarSpaceFillingCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-01-05 11:57:30 -08:00
"colors" : [PURPLE, TEAL],
2015-12-13 15:42:20 -08:00
"offsets" : [
LEFT+DOWN,
LEFT,
LEFT+UP,
UP,
ORIGIN,
DOWN,
RIGHT+DOWN,
RIGHT,
RIGHT+UP,
],
"offset_to_rotation_axis" : {
str(LEFT) : UP,
str(UP) : RIGHT,
str(ORIGIN) : LEFT+UP,
str(DOWN) : RIGHT,
str(RIGHT) : UP,
},
"scale_factor" : 3,
"radius_scale_factor" : 2.0/3,
}
class TriangleFillingCurve(SelfSimilarSpaceFillingCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-01-05 11:57:30 -08:00
"colors" : [MAROON, YELLOW],
2015-12-13 15:42:20 -08:00
"offsets" : [
LEFT/4.+DOWN/6.,
ORIGIN,
RIGHT/4.+DOWN/6.,
UP/3.,
],
"offset_to_rotation_axis" : {
str(ORIGIN): RIGHT,
str(UP/3.) : UP,
},
"scale_factor" : 2,
"radius_scale_factor" : 1.5,
}
# class HexagonFillingCurve(SelfSimilarSpaceFillingCurve):
2016-02-27 16:32:53 -08:00
# CONFIG = {
2015-12-13 15:42:20 -08:00
# "start_color" : WHITE,
# "end_color" : BLUE_D,
# "axis_offset_pairs" : [
# (None, 1.5*DOWN + 0.5*np.sqrt(3)*LEFT),
# (UP+np.sqrt(3)*RIGHT, 1.5*DOWN + 0.5*np.sqrt(3)*RIGHT),
# (np.sqrt(3)*UP+RIGHT, ORIGIN),
# ((UP, RIGHT), np.sqrt(3)*LEFT),
# (None, 1.5*UP + 0.5*np.sqrt(3)*LEFT),
# (None, 1.5*UP + 0.5*np.sqrt(3)*RIGHT),
# (RIGHT, np.sqrt(3)*RIGHT),
# ],
# "scale_factor" : 3,
# "radius_scale_factor" : 2/(3*np.sqrt(3)),
# }
# def refine_into_subparts(self, points):
# return SelfSimilarSpaceFillingCurve.refine_into_subparts(
# self,
# rotate(points, np.pi/6, IN)
# )
class UtahFillingCurve(SelfSimilarSpaceFillingCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-01-05 11:57:30 -08:00
"colors" : [WHITE, BLUE_D],
2015-12-13 15:42:20 -08:00
"axis_offset_pairs" : [
],
"scale_factor" : 3,
"radius_scale_factor" : 2/(3*np.sqrt(3)),
}
class FlowSnake(LindenmayerCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-01-05 11:57:30 -08:00
"colors" : [YELLOW, GREEN],
2015-12-13 15:42:20 -08:00
"axiom" : "A",
"rule" : {
"A" : "A-B--B+A++AA+B-",
"B" : "+A-BB--B-A++A+B",
},
"radius" : 6, #TODO, this is innaccurate
"scale_factor" : np.sqrt(7),
"start_step" : RIGHT,
"angle" : -np.pi/3,
}
def __init__(self, **kwargs):
LindenmayerCurve.__init__(self, **kwargs)
self.rotate(-self.order*np.pi/9, about_point = ORIGIN)
2015-12-13 15:42:20 -08:00
2017-01-16 11:43:59 -08:00
class SierpinskiCurve(LindenmayerCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-01-16 11:43:59 -08:00
"colors" : [RED, WHITE],
"axiom" : "B",
2015-12-13 15:42:20 -08:00
"rule" : {
"A" : "+B-A-B+",
"B" : "-A+B+A-",
},
"radius" : 6, #TODO, this is innaccurate
"scale_factor" : 2,
"start_step" : RIGHT,
"angle" : -np.pi/3,
}
2017-01-16 11:43:59 -08:00
class KochSnowFlake(LindenmayerCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2017-01-17 17:14:32 -08:00
"colors" : [BLUE_D, WHITE, BLUE_D],
2016-01-15 11:46:45 -08:00
"axiom" : "A--A--A--",
"rule" : {
"A" : "A+A--A+A"
},
"radius" : 4,
"scale_factor" : 3,
"start_step" : RIGHT,
2017-01-25 12:59:46 -08:00
"angle" : np.pi/3,
"order_to_stroke_width_map" : {
3 : 3,
5 : 2,
6 : 1,
},
2016-01-15 11:46:45 -08:00
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
self.scale_factor = 2*(1+np.cos(self.angle))
LindenmayerCurve.__init__(self, **kwargs)
2017-01-16 11:43:59 -08:00
class KochCurve(KochSnowFlake):
CONFIG = {
"axiom" : "A--"
}
class QuadraticKoch(LindenmayerCurve):
CONFIG = {
"colors" : [YELLOW, WHITE, MAROON_B],
"axiom" : "A",
"rule" : {
"A" : "A+A-A-AA+A+A-A"
},
"radius" : 4,
"scale_factor" : 4,
"start_step" : RIGHT,
"angle" : np.pi/2
}
2015-12-13 15:42:20 -08:00
class QuadraticKochIsland(QuadraticKoch):
CONFIG = {
"axiom" : "A+A+A+A"
}
class StellarCurve(LindenmayerCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
"start_color" : RED,
"end_color" : BLUE_E,
"rule" : {
"A" : "+B-A-B+A-B+",
"B" : "-A+B+A-B+A-",
},
"scale_factor" : 3,
"angle" : 2*np.pi/5,
}
2017-01-17 17:14:32 -08:00
class SnakeCurve(FractalCurve):
2016-02-27 16:32:53 -08:00
CONFIG = {
2015-12-13 15:42:20 -08:00
"start_color" : BLUE,
"end_color" : YELLOW,
}
def get_anchor_points(self):
result = []
resolution = 2**self.order
2015-12-21 22:50:53 -08:00
step = 2.0*self.radius / resolution
2015-12-13 15:42:20 -08:00
lower_left = ORIGIN + \
2015-12-21 22:50:53 -08:00
LEFT*(self.radius - step/2) + \
DOWN*(self.radius - step/2)
2015-12-13 15:42:20 -08:00
for y in range(resolution):
x_range = range(resolution)
if y%2 == 0:
x_range.reverse()
for x in x_range:
result.append(
lower_left + x*step*RIGHT + y*step*UP
)
return result