From 408da1871df5969d829538e22989b151a30ab6d0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 6 Apr 2019 11:52:56 -0700 Subject: [PATCH] More Fourier + Heat animations --- active_projects/ode/all_part2_scenes.py | 8 + active_projects/ode/part2/fourier_series.py | 175 ++++++++++++++---- active_projects/ode/part2/heat_equation.py | 152 ++++++++++++++- .../ode/part2/shared_constructs.py | 35 ++++ active_projects/ode/part2/staging.py | 7 +- 5 files changed, 339 insertions(+), 38 deletions(-) create mode 100644 active_projects/ode/part2/shared_constructs.py diff --git a/active_projects/ode/all_part2_scenes.py b/active_projects/ode/all_part2_scenes.py index 3ce14270..50671114 100644 --- a/active_projects/ode/all_part2_scenes.py +++ b/active_projects/ode/all_part2_scenes.py @@ -10,6 +10,11 @@ ALL_SCENE_CLASSES = [ FourierOfTrebleClef, FourierOfEighthNote, FourierOfN, + FourierNailAndGear, + FourierNDQ, + FourierBatman, + FourierGoogleG, + FourierHeart, # CirclesDrawingWave, # Scenes for video ExplainCircleAnimations, @@ -19,4 +24,7 @@ ALL_SCENE_CLASSES = [ FourierSeriesIntroBackground20, FourierSeriesIntro, PartTwoOfTour, + TwoDBodyWithManyTemperatures, + TwoDBodyWithManyTemperaturesGraph, + TwoDBodyWithManyTemperaturesContour, ] diff --git a/active_projects/ode/part2/fourier_series.py b/active_projects/ode/part2/fourier_series.py index 6fa53bfc..c3e0a3be 100644 --- a/active_projects/ode/part2/fourier_series.py +++ b/active_projects/ode/part2/fourier_series.py @@ -15,6 +15,13 @@ class FourierCirclesScene(Scene): "circle_style": { "stroke_width": 2, }, + "arrow_config": { + "buff": 0, + "max_tip_length_to_length_ratio": 0.35, + "tip_length": 0.15, + "max_stroke_width_to_length_ratio": 10, + "stroke_width": 2, + }, "use_vectors": True, "base_frequency": 1, "slow_factor": 0.25, @@ -77,18 +84,21 @@ class FourierCirclesScene(Scene): color=color, **self.circle_style, ) - if self.use_vectors: - LineClass = Arrow - else: - LineClass = Line - circle.radial_line = LineClass( + line_points = ( circle.get_center(), circle.get_start(), - color=WHITE, - buff=0, - max_tip_length_to_length_ratio=0.1, - **self.circle_style, ) + if self.use_vectors: + circle.radial_line = Arrow( + *line_points, + **self.arrow_config, + ) + else: + circle.radial_line = Line( + *line_points, + color=WHITE, + **self.circle_style, + ) circle.add(circle.radial_line) circle.freq = freq circle.phase = phase @@ -154,10 +164,11 @@ class FourierCirclesScene(Scene): width = 0 else: width = stroke_width * (1 - (b % 1)) - sp.set_stroke(YELLOW, width=width) + sp.set_stroke(width=width) path.curr_time += dt return path + broken_path.set_color(YELLOW) broken_path.add_updater(update_path) return broken_path @@ -365,13 +376,121 @@ class FourierOfEighthNote(FourierOfTrebleClef): class FourierOfN(FourierOfTrebleClef): CONFIG = { "height": 6, - "n_circles": 200, + "n_circles": 1000, } def get_shape(self): return TexMobject("N") +class FourierNailAndGear(FourierOfTrebleClef): + CONFIG = { + "height": 6, + "n_circles": 200, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + shape = SVGMobject("Nail_And_Gear")[1] + return shape + + +class FourierBatman(FourierOfTrebleClef): + CONFIG = { + "height": 4, + "n_circles": 100, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + shape = SVGMobject("BatmanLogo")[1] + return shape + + +class FourierHeart(FourierOfTrebleClef): + CONFIG = { + "height": 4, + "n_circles": 100, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + shape = SuitSymbol("hearts") + return shape + + def get_drawn_path(self, *args, **kwargs): + kwargs["stroke_width"] = 5 + path = super().get_drawn_path(*args, **kwargs) + path.set_color(PINK) + return path + + +class FourierNDQ(FourierOfTrebleClef): + CONFIG = { + "height": 4, + "n_circles": 1000, + "run_time": 10, + "arrow_config": { + "tip_length": 0.1, + "stroke_width": 2, + } + } + + def get_shape(self): + path = VMobject() + shape = TexMobject("Hayley") + for sp in shape.family_members_with_points(): + path.append_points(sp.points) + return path + + +class FourierGoogleG(FourierOfTrebleClef): + CONFIG = { + "n_circles": 10, + "height": 5, + "g_colors": [ + "#4285F4", + "#DB4437", + "#F4B400", + "#0F9D58", + ] + } + + def get_shape(self): + g = SVGMobject("google_logo")[5] + g.center() + self.add(g) + return g + + def get_drawn_path(self, *args, **kwargs): + kwargs["stroke_width"] = 7 + path = super().get_drawn_path(*args, **kwargs) + + blue, red, yellow, green = self.g_colors + + path[:250].set_color(blue) + path[250:333].set_color(green) + path[333:370].set_color(yellow) + path[370:755].set_color(red) + path[755:780].set_color(yellow) + path[780:860].set_color(green) + path[860:].set_color(blue) + + return path + + class ExplainCircleAnimations(FourierCirclesScene): CONFIG = { "n_circles": 100, @@ -521,26 +640,25 @@ class ExplainCircleAnimations(FourierCirclesScene): def show_as_vectors(self): top_circles = self.top_circles top_vectors = self.get_rotating_vectors(top_circles) - top_vectors.set_color(RED) - lines = VGroup(*filter( - lambda sm: isinstance(sm, Line), - top_circles.family_members_with_points() - )) + top_vectors.set_color(WHITE) - self.add(lines, top_circles) + original_circles = top_circles.copy() self.play( FadeIn(top_vectors), - lines.fade, 1, + top_circles.set_opacity, 0, ) self.wait(3) + self.play( + top_circles.match_style, original_circles + ) + self.remove(top_vectors) self.top_vectors = top_vectors def show_vector_sum(self): - # trackers = self.center_trackers.deepcopy() trackers = self.center_trackers.copy() trackers.sort( - submob_func=lambda t: abs(t.circle.freq) + submob_func=lambda t: abs(t.circle.freq - 0.1) ) plane = self.plane = NumberPlane( x_min=-3, @@ -556,7 +674,6 @@ class ExplainCircleAnimations(FourierCirclesScene): plane.move_to(self.center_point) self.play( - # FadeOut(self.path), FadeOut(self.drawn_path), FadeOut(self.circles), self.slow_factor_tracker.set_value, 0.05, @@ -565,7 +682,6 @@ class ExplainCircleAnimations(FourierCirclesScene): self.play(FadeIn(plane)) new_circles = VGroup() - new_vectors = VGroup() last_tracker = None for tracker in trackers: if last_tracker: @@ -574,20 +690,11 @@ class ExplainCircleAnimations(FourierCirclesScene): tracker.new_location_func = lambda: self.center_point original_circle = tracker.circle - original_vector = tracker.circle.vector tracker.circle = original_circle.copy() tracker.circle.center_func = tracker.get_location - tracker.vector = original_vector.copy() - tracker.vector.clear_updaters() - tracker.vector.circle = tracker.circle - tracker.vector.add_updater(lambda v: v.put_start_and_end_on( - v.circle.get_center(), - v.circle.get_start(), - )) new_circles.add(tracker.circle) - new_vectors.add(tracker.vector) - self.add(tracker, tracker.circle, tracker.vector) + self.add(tracker, tracker.circle) start_point = tracker.get_location() self.play( UpdateFromAlphaFunc( @@ -613,7 +720,6 @@ class ExplainCircleAnimations(FourierCirclesScene): self.slow_factor_tracker.set_value(0.1) self.add( self.top_circles, - self.top_vectors, self.freq_numbers, self.path, ) @@ -629,7 +735,6 @@ class ExplainCircleAnimations(FourierCirclesScene): def tweak_starting_vectors(self): top_circles = self.top_circles - top_vectors = self.top_vectors circles = self.circles path = self.path drawn_path = self.drawn_path @@ -658,9 +763,7 @@ class ExplainCircleAnimations(FourierCirclesScene): self.wait() self.play( ReplacementTransform(top_circles, new_top_circles), - ReplacementTransform(top_vectors, new_top_vectors), ReplacementTransform(circles, new_circles), - # ReplacementTransform(path, new_path), FadeOut(path), run_time=3, ) diff --git a/active_projects/ode/part2/heat_equation.py b/active_projects/ode/part2/heat_equation.py index 2b8b4e67..c1fd1c49 100644 --- a/active_projects/ode/part2/heat_equation.py +++ b/active_projects/ode/part2/heat_equation.py @@ -1,6 +1,156 @@ from big_ol_pile_of_manim_imports import * +from active_projects.ode.part2.shared_constructs import * -class NewSceneName(Scene): +class TwoDBodyWithManyTemperatures(ThreeDScene): + CONFIG = { + "cells_per_side": 20, + "body_height": 6, + } + + def construct(self): + self.introduce_body() + self.show_temperature_at_all_points() + + def introduce_body(self): + height = self.body_height + buff = 0.025 + rows = VGroup(*[ + VGroup(*[ + Dot( + # stroke_width=0.5, + stroke_width=0, + fill_opacity=1, + ) + for x in range(self.cells_per_side) + ]).arrange(RIGHT, buff=buff) + for y in range(self.cells_per_side) + ]).arrange(DOWN, buff=buff) + for row in rows[1::2]: + row.submobjects.reverse() + + body = self.body = VGroup(*it.chain(*rows)) + body.set_height(height) + body.center() + body.to_edge(LEFT) + + axes = self.axes = Axes( + x_min=-5, x_max=5, + y_min=-5, y_max=5, + ) + axes.match_height(body) + axes.move_to(body) + + for cell in body: + self.color_cell(cell) + # body.set_stroke(WHITE, 0.5) # Do this? + + plate = Square( + stroke_width=0, + fill_color=DARK_GREY, + sheen_direction=UL, + sheen_factor=1, + fill_opacity=1, + ) + plate.replace(body) + + plate_words = TextMobject("Piece of \\\\ metal") + plate_words.scale(2) + plate_words.set_stroke(BLACK, 2, background=True) + plate_words.set_color(BLACK) + plate_words.move_to(plate) + + self.play( + DrawBorderThenFill(plate), + Write( + plate_words, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1) + ) + ) + self.wait() + + self.remove(plate_words) + + def show_temperature_at_all_points(self): + body = self.body + start_corner = body[0].get_center() + + dot = Dot(radius=0.01, color=WHITE) + dot.move_to(start_corner) + + get_point = dot.get_center + + lhs = TexMobject("T = ") + lhs.next_to(body, RIGHT, LARGE_BUFF) + + decimal = DecimalNumber( + num_decimal_places=1, + unit="^\\circ" + ) + decimal.next_to(lhs, RIGHT, MED_SMALL_BUFF, DOWN) + decimal.add_updater( + lambda d: d.set_value( + 40 + 50 * self.point_to_temp(get_point()) + ) + ) + + arrow = Arrow(color=YELLOW) + arrow.set_stroke(BLACK, 8, background=True) + arrow.tip.set_stroke(BLACK, 2, background=True) + # arrow.add_to_back(arrow.copy().set_stroke(BLACK, 5)) + arrow.add_updater(lambda a: a.put_start_and_end_on( + lhs.get_left() + MED_SMALL_BUFF * LEFT, + get_point(), + )) + + dot.add_updater(lambda p: p.move_to( + body[-1] if (1 < len(body)) else start_corner + )) + self.add(body, dot, lhs, decimal, arrow) + self.play( + ShowIncreasingSubsets( + body, + run_time=10, + rate_func=linear, + ) + ) + self.wait() + self.remove(dot) + self.play( + FadeOut(arrow), + FadeOut(lhs), + FadeOut(decimal), + ) + + # + def point_to_temp(self, point, time=0): + x, y = self.axes.point_to_coords(point) + return two_d_temp_func( + 0.3 * x, 0.3 * y, t=time + ) + + def color_cell(self, cell, vect=RIGHT): + p0 = cell.get_corner(-vect) + p1 = cell.get_corner(vect) + colors = [] + for point in p0, p1: + temp = self.point_to_temp(point) + color = temperature_to_color(temp) + colors.append(color) + cell.set_color(color=colors) + cell.set_sheen_direction(vect) + return cell + + +class TwoDBodyWithManyTemperaturesGraph(ExternallyAnimatedScene): + pass + + +class TwoDBodyWithManyTemperaturesContour(ExternallyAnimatedScene): + pass + + +class BringTwoRodsTogether(Scene): def construct(self): pass diff --git a/active_projects/ode/part2/shared_constructs.py b/active_projects/ode/part2/shared_constructs.py new file mode 100644 index 00000000..baa02956 --- /dev/null +++ b/active_projects/ode/part2/shared_constructs.py @@ -0,0 +1,35 @@ +from big_ol_pile_of_manim_imports import * + +TIME_COLOR = YELLOW +X_COLOR = GREEN + + +def get_heat_equation(): + pass + + +def temperature_to_color(temp, min_temp=-1, max_temp=1): + colors = [BLUE, TEAL, GREEN, YELLOW, "#ff0000"] + + alpha = inverse_interpolate(min_temp, max_temp, temp) + index, sub_alpha = integer_interpolate( + 0, len(colors) - 1, alpha + ) + return interpolate_color( + colors[index], colors[index + 1], sub_alpha + ) + + +def two_d_temp_func(x, y, t): + return np.sum([ + c * np.sin(f * var) * np.exp(-(f**2) * t) + for c, f, var in [ + (0.2, 1, x), + (0.3, 3, x), + (0.02, 5, x), + (0.01, 7, x), + (0.5, 2, y), + (0.1, 10, y), + (0.01, 20, y), + ] + ]) diff --git a/active_projects/ode/part2/staging.py b/active_projects/ode/part2/staging.py index bbaac905..52675785 100644 --- a/active_projects/ode/part2/staging.py +++ b/active_projects/ode/part2/staging.py @@ -56,7 +56,7 @@ class FourierSeriesIntro(Scene): TransformFromCopy( title.get_part_by_tex("Fourier"), name.get_part_by_tex("Fourier"), - path_arc=-45 * DEGREES, + path_arc=90 * DEGREES, ), FadeIn(name.get_part_by_tex("Joseph")), ) @@ -81,3 +81,8 @@ class PartTwoOfTour(TourOfDifferentialEquations): CONFIG = { "zoomed_thumbnail_index": 1, } + + +class CompareODEToPDE(Scene): + def construct(self): + pass