From b0e53b8f2a92408e48d2e3de344a66aae7ec5533 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Jan 2017 17:14:32 -0800 Subject: [PATCH] More fractal additions --- fractal_dimension.py | 225 +++++++++++++++++++++++++++++++++++++++-- mobject/svg_mobject.py | 10 ++ topics/fractals.py | 76 ++++++++++++-- 3 files changed, 296 insertions(+), 15 deletions(-) diff --git a/fractal_dimension.py b/fractal_dimension.py index c4094ca6..336769bf 100644 --- a/fractal_dimension.py +++ b/fractal_dimension.py @@ -25,6 +25,21 @@ from mobject.svg_mobject import * from mobject.tex_mobject import * +class Britain(SVGMobject): + CONFIG = { + "file_name" : "Britain.svg", + "stroke_width" : 1, + "stroke_color" : WHITE, + "fill_opacity" : 0, + "height" : 5, + "mark_paths_closed" : True, + } + def __init__(self, **kwargs): + SVGMobject.__init__(self, **kwargs) + self.scale_to_fit_height(self.height) + self.center() + + class KochTest(Scene): def construct(self): koch = KochCurve(order = 5, stroke_width = 2) @@ -51,6 +66,27 @@ class SierpinskiTest(Scene): # self.play(sierp.scale, 2, sierp.get_top()) # self.dither(3) +class FractalCreation(Scene): + CONFIG = { + "fractal_class" : PentagonalFractal, + "max_order" : 6, + "path_arc" : np.pi/6, + "submobject_mode" : "lagged_start" + } + def construct(self): + fractal = self.fractal_class(order = 0) + self.play(FadeIn(fractal)) + for order in range(1, self.max_order+1): + new_fractal = self.fractal_class(order = order) + self.play(Transform( + fractal, new_fractal, + path_arc = self.path_arc, + submobject_mode = self.submobject_mode, + run_time = 2 + )) + self.dither() + self.dither() + ################################### @@ -122,7 +158,7 @@ class ZoomInOnFractal(PiCreatureScene): class WhatAreFractals(TeacherStudentsScene): def construct(self): self.student_says( - "But what \\emph{is} a fractal", + "But what \\emph{is} a fractal?", student_index = 2, width = 6 ) @@ -150,13 +186,186 @@ class WhatAreFractals(TeacherStudentsScene): self.play(self.get_teacher().change_mode, "happy") self.dither(2) - - - - - - - +class IntroduceVonKochCurve(Scene): + CONFIG = { + "order" : 5, + "stroke_width" : 3, + } + def construct(self): + snowflake = self.get_snowflake() + name = TextMobject("Von Koch Snowflake") + name.to_edge(UP) + + self.play(ShowCreation(snowflake, run_time = 3)) + self.play(Write(name, run_time = 2)) + curve = self.isolate_one_curve(snowflake) + self.dither() + self.zoom_in_on(curve) + + def get_snowflake(self): + triangle = RegularPolygon(n = 3, start_angle = np.pi/2) + triangle.scale_to_fit_height(4) + curves = VGroup(*[ + KochCurve( + order = self.order, + stroke_width = self.stroke_width + ) + for x in range(3) + ]) + for index, curve in enumerate(curves): + width = curve.get_width() + curve.move_to( + (np.sqrt(3)/6)*width*UP, DOWN + ) + curve.rotate(-index*2*np.pi/3) + curves.gradient_highlight(BLUE, WHITE, BLUE) + + return curves + + def isolate_one_curve(self, snowflake): + self.play(*[ + ApplyMethod(curve.shift, curve.get_center()/2) + for curve in snowflake + ]) + self.dither() + self.play( + snowflake.scale, 2.1, + snowflake.next_to, UP, DOWN + ) + self.remove(*snowflake[1:]) + return snowflake[0] + + def zoom_in_on(self, curve): + larger_curve = KochCurve(order = self.order+1) + larger_curve.replace(curve) + larger_curve.scale(3, about_point = curve.get_corner(DOWN+LEFT)) + larger_curve.gradient_highlight( + curve[0].get_color(), + curve[-1].get_color(), + ) + + self.play(Transform(curve, larger_curve, run_time = 2)) + n_parts = len(curve.split()) + sub_portion = VGroup(*curve[:n_parts/4]) + self.play( + sub_portion.highlight, YELLOW, + rate_func = there_and_back + ) + self.dither() + +class IntroduceSierpinskiTriangle(PiCreatureScene): + CONFIG = { + "order" : 7, + } + def construct(self): + sierp = Sierpinski(order = self.order) + sierp.save_state() + + self.play(FadeIn( + sierp, + run_time = 2, + submobject_mode = "lagged_start" + )) + self.dither() + self.play( + self.pi_creature.change_mode, "pondering", + *[ + ApplyMethod(submob.shift, submob.get_center()) + for submob in sierp + ] + ) + self.dither() + for submob in sierp: + self.play(sierp.shift, -submob.get_center()) + self.dither() + self.play(sierp.restore) + self.change_mode("happy") + self.dither() + +class SelfSimilarFractalsAsSubset(Scene): + CONFIG = { + "fractal_width" : 1.5 + } + def construct(self): + self.add_self_similar_fractals() + self.add_general_fractals() + + def add_self_similar_fractals(self): + fractals = VGroup( + DiamondFractal(order = 5), + KochSnowFlake(order = 3), + Sierpinski(order = 5), + ) + for submob in fractals: + submob.scale_to_fit_width(self.fractal_width) + fractals.arrange_submobjects(RIGHT) + fractals[-1].next_to(VGroup(*fractals[:-1]), DOWN) + + title = TextMobject("Self-similar fractals") + title.next_to(fractals, UP) + + small_rect = Rectangle() + small_rect.replace(VGroup(fractals, title), stretch = True) + small_rect.scale_in_place(1.2) + self.small_rect = small_rect + + group = VGroup(fractals, title, small_rect) + group.to_corner(UP+LEFT, buff = 2*MED_BUFF) + + self.play( + Write(title), + ShowCreation(fractals), + run_time = 3 + ) + self.play(ShowCreation(small_rect)) + self.dither() + + def add_general_fractals(self): + big_rectangle = Rectangle( + width = 2*SPACE_WIDTH - 2*MED_BUFF, + height = 2*SPACE_HEIGHT - 2*MED_BUFF, + ) + title = TextMobject("Fractals") + title.scale(1.5) + title.next_to(ORIGIN, RIGHT, buff = LARGE_BUFF) + title.to_edge(UP, buff = 2*MED_BUFF) + + britain = Britain() + britain.next_to(self.small_rect, RIGHT) + britain.shift(2*DOWN) + + randy = Randolph().flip().scale(1.4) + randy.next_to(britain, buff = SMALL_BUFF) + randy.generate_target() + randy.target.change_mode("pleading") + fractalify(randy.target, order = 2) + + self.play( + ShowCreation(big_rectangle), + Write(title), + ) + self.play(ShowCreation(britain), run_time = 5) + self.play( + britain.set_stroke, BLACK, 0, + britain.set_fill, BLUE, 1, + ) + self.play(FadeIn(randy)) + self.play(MoveToTarget(randy, run_time = 2)) + self.dither(2) + +class ConstrastSmoothAndFractal(Scene): + def construct(self): + v_line = Line(UP, DOWN).scale(SPACE_HEIGHT) + smooth = TextMobject("Smooth") + smooth.shift(SPACE_WIDTH*LEFT/2) + fractal = TextMobject("Fractal") + fractal.shift(SPACE_WIDTH*RIGHT/2) + VGroup(smooth, fractal).to_edge(UP) + self.add(v_line, smooth, fractal) + + britain = Britain() + anchors = britain.get_anchors() + smooth_britain = diff --git a/mobject/svg_mobject.py b/mobject/svg_mobject.py index 0998568f..25e8f71c 100644 --- a/mobject/svg_mobject.py +++ b/mobject/svg_mobject.py @@ -62,6 +62,8 @@ class SVGMobject(VMobject): result.append(self.rect_to_mobject(element)) elif element.tagName == 'circle': result.append(self.circle_to_mobject(element)) + elif element.tagName == 'polygon': + result.append(self.polygon_to_mobject(element)) else: warnings.warn("Unknown element type: " + element.tagName) result = filter(lambda m : m is not None, result) @@ -86,6 +88,14 @@ class SVGMobject(VMobject): self.ref_to_element[ref] ) + def polygon_to_mobject(self, polygon_element): + #TODO, This seems hacky... + path_string = polygon_element.getAttribute("points") + for digit in string.digits: + path_string = path_string.replace(" " + digit, " L" + digit) + path_string = "M" + path_string + return self.path_string_to_mobject(path_string) + # def circle_to_mobject(self, circle_element): diff --git a/topics/fractals.py b/topics/fractals.py index d42f1d99..89b5c113 100644 --- a/topics/fractals.py +++ b/topics/fractals.py @@ -14,6 +14,50 @@ def rotate(points, angle = np.pi, axis = OUT): points = np.dot(points, np.transpose(matrix)) return points +def fractalify(vmobject, order = 3, *args, **kwargs): + for x in range(order): + fractalification_iteration(vmobject) + return vmobject + +def fractalification_iteration(vmobject, + dimension = 1.05, + num_inserted_anchors_range = range(1, 4) + ): + 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, 0.99, num_points) + ] + 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 + class SelfSimilarFractal(VMobject): CONFIG = { @@ -87,15 +131,31 @@ class DiamondFractal(SelfSimilarFractal): VGroup(*subparts).rotate(np.pi/4) +class PentagonalFractal(SelfSimilarFractal): + CONFIG = { + "num_subparts" : 5, + "colors" : [MAROON_B, YELLOW, RED] + } + def get_seed_shape(self): + return RegularPolygon(n = 5, start_angle = np.pi/2) + + def arrange_subparts(self, *subparts): + phi = (1 + np.sqrt(5))/2 + for x, part in enumerate(subparts): + part.shift(0.95*part.get_height()*UP) + part.rotate(2*np.pi*x/5) + + ######## Space filling curves ############ -class SpaceFillingCurve(VMobject): +class FractalCurve(VMobject): CONFIG = { "radius" : 3, "order" : 5, "colors" : [RED, GREEN], "monochromatic" : False, - "stroke_width" : 2, + "stroke_width" : 3, + "propogate_style_to_family" : True, } def generate_points(self): @@ -107,13 +167,15 @@ class SpaceFillingCurve(VMobject): corner = VMobject() corner.set_points_as_corners(triplet) self.add(corner) + + def init_colors(self): self.gradient_highlight(*self.colors) def get_anchor_points(self): raise Exception("Not implemented") -class LindenmayerCurve(SpaceFillingCurve): +class LindenmayerCurve(FractalCurve): CONFIG = { "axiom" : "A", "rule" : {}, @@ -154,7 +216,7 @@ class LindenmayerCurve(SpaceFillingCurve): return np.array(result) - center_of_mass(result) -class SelfSimilarSpaceFillingCurve(SpaceFillingCurve): +class SelfSimilarSpaceFillingCurve(FractalCurve): CONFIG = { "offsets" : [], #keys must awkwardly be in string form... @@ -339,7 +401,7 @@ class SierpinskiCurve(LindenmayerCurve): class KochSnowFlake(LindenmayerCurve): CONFIG = { - "colors" : [BLUE_D, WHITE], + "colors" : [BLUE_D, WHITE, BLUE_D], "axiom" : "A--A--A--", "rule" : { "A" : "A+A--A+A" @@ -374,7 +436,7 @@ class StellarCurve(LindenmayerCurve): "angle" : 2*np.pi/5, } -class SnakeCurve(SpaceFillingCurve): +class SnakeCurve(FractalCurve): CONFIG = { "start_color" : BLUE, "end_color" : YELLOW, @@ -409,7 +471,7 @@ class SpaceFillingCurveScene(Scene): curve_class_name, order_str = arg_str.split() space_filling_curves = dict([ (Class.__name__, Class) - for Class in get_all_descendent_classes(SpaceFillingCurve) + for Class in get_all_descendent_classes(FractalCurve) ]) if curve_class_name not in space_filling_curves: raise Exception(