├── colab ├── RELEASE ├── manim.py └── colab.py ├── plugins ├── RELEASE ├── plugins.py ├── fractal.py ├── extras.py └── roulette.py ├── LICENSE ├── README.md └── source ├── koch_curve.py ├── fibonacci_spiral.py ├── scenes ├── mscene.ipynb ├── cycloids.ipynb ├── koch_curve.ipynb └── fibonacci_spiral.ipynb ├── clips └── flash_fade.ipynb └── cycloids.py /colab/RELEASE: -------------------------------------------------------------------------------- 1 | colab 2541 2 | manim 2541 -------------------------------------------------------------------------------- /plugins/RELEASE: -------------------------------------------------------------------------------- 1 | plugins 2526 2 | extras 2526 3 | fractal 2511 4 | roulette 2503 5 | -------------------------------------------------------------------------------- /plugins/plugins.py: -------------------------------------------------------------------------------- 1 | # mscene plugins 2 | from .roulette import * 3 | from .fractal import * 4 | from .extras import * 5 | -------------------------------------------------------------------------------- /colab/manim.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | with warnings.catch_warnings(): 4 | warnings.filterwarnings("ignore") 5 | from manim import * 6 | 7 | config.disable_caching = True 8 | config.verbosity = "WARNING" 9 | config.media_width = "50%" 10 | config.media_embed = True 11 | 12 | Text.set_default(font="STIX Two Text") 13 | 14 | 15 | class ManimScene(Scene): 16 | def construct(self): 17 | banner = ManimBanner() 18 | self.play(banner.create()) 19 | self.play(banner.expand()) 20 | self.wait(2) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 CuriousWalk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Mscene](https://mscene.curiouswalk.com/assets/banner.png)](https://mscene.curiouswalk.com) 2 | 3 | # Mscene 4 | 5 | A Python library for creating science animation with Manim in Google Colab. ✨ [mscene.curiouswalk.com](https://mscene.curiouswalk.com) 6 | 7 | >[Manim](https://www.manim.community) is an animation engine designed to program precise animations for science videos. Google [Colab](https://colab.google) is a hosted Jupyter Notebook service that requires no setup and provides free access to computing resources, including GPUs and TPUs. 8 | 9 | ## Quickstart 10 | 11 | ### Manim in Colab 12 | 13 | Visit [*colab.new*](https://colab.new) to create a new Colab notebook. 14 | 15 | **Installation** 16 | ```python 17 | %pip install -q mscene 18 | import mscene 19 | %mscene -l manim 20 | ``` 21 | ```python 22 | from mscene.manim import * 23 | ``` 24 | **Example Scene** 25 | ```python 26 | %%manim -qm ManimScene 27 | 28 | class ManimScene(Scene): 29 | def construct(self): 30 | banner = ManimBanner() 31 | self.play(banner.create()) 32 | self.play(banner.expand()) 33 | self.wait(1.5) 34 | ``` 35 | 36 | ### Mscene Plugins 37 | 38 | Plugins extend Manim with additional features. 39 | 40 | **Adding Plugins** 41 | ```python 42 | import mscene 43 | %mscene plugins 44 | ``` 45 | ```python 46 | from mscene.plugins import * 47 | ``` 48 | **Example Scene** 49 | ```python 50 | %%manim -qm FractalScene 51 | 52 | class FractalScene(Scene): 53 | def construct(self): 54 | ks = KochSnowflake(level=2) 55 | self.add(ks) 56 | self.play(ks.animate.next_level()) 57 | self.wait(1.5) 58 | self.play(ks.animate.prev_level()) 59 | self.wait(1.5) 60 | ``` 61 | --- 62 | -------------------------------------------------------------------------------- /colab/colab.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | from importlib.util import find_spec 3 | from pathlib import Path 4 | import subprocess 5 | import requests 6 | 7 | try: 8 | from IPython.display import display, HTML, clear_output 9 | from IPython import get_ipython 10 | 11 | except ImportError: 12 | ipychk = False 13 | 14 | else: 15 | ipy = get_ipython() 16 | ipychk = ipy is not None 17 | 18 | 19 | def display_progress(value): 20 | html = f""" 21 |
22 |

Installing Manim

23 | 25 |
26 | 27 | 32 | """ 33 | display(HTML(html)) 34 | 35 | 36 | def add_file(url, filename): 37 | path = Path(filename) 38 | if not path.exists() and (response := requests.get(url)).ok: 39 | path.parent.mkdir(parents=True, exist_ok=True) 40 | path.write_bytes(response.content) 41 | 42 | 43 | def check_package(name): 44 | if name == "manim": 45 | status = find_spec(name) is None 46 | else: 47 | cmd = ("dpkg", "-s", name) 48 | stdout = subprocess.run(cmd, capture_output=True) 49 | status = stdout.returncode != 0 50 | return status 51 | 52 | 53 | def print_manim(): 54 | if check_package("manim"): 55 | info = "Manim – Mathematical Animation Framework" 56 | else: 57 | info = f"Manim – Mathematical Animation Framework (Version {version('manim')})" 58 | print(info) 59 | 60 | 61 | def setup(name, lite=False): 62 | cmd = [] 63 | pkg = [] 64 | 65 | if not lite and check_package("texlive"): 66 | cmd.append(("apt-get", "-qq", "update")) 67 | pkg.extend( 68 | ("texlive", "texlive-latex-extra", "texlive-science", "texlive-fonts-extra") 69 | ) 70 | 71 | if check_package("libpango1.0-dev"): 72 | pkg.append("libpango1.0-dev") 73 | 74 | if pkg: 75 | cmd.append(("apt-get", "-qq", "install", "-y", *pkg)) 76 | 77 | if check_package("manim"): 78 | cmd.append(("uv", "pip", "install", "-q", name)) 79 | 80 | if cmd: 81 | if ipychk: 82 | display_progress(30) if lite else display_progress(240) 83 | 84 | # add STIX font (stixfonts.org) 85 | add_file( 86 | "https://raw.githubusercontent.com/stipub/stixfonts/master/fonts/static_ttf/STIXTwoText-Regular.ttf", 87 | "/usr/share/fonts/truetype/stixfonts/STIXTwoText-Regular.ttf", 88 | ) 89 | 90 | for c in cmd: 91 | result = subprocess.run( 92 | c, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 93 | ) 94 | if result.returncode != 0 and c[0] == "uv": 95 | subprocess.run( 96 | c[1:], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 97 | ) 98 | 99 | if ipychk: 100 | clear_output() 101 | print_manim() 102 | stdout = ipy.kernel.do_shutdown(restart=True) 103 | 104 | else: 105 | print_manim() 106 | -------------------------------------------------------------------------------- /plugins/fractal.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | 3 | 4 | class KochCurve(VMobject): 5 | def __init__( 6 | self, 7 | level=0, 8 | length=config.frame_width * 3 / 4, 9 | point=ORIGIN, 10 | group=True, 11 | **kwargs 12 | ): 13 | super().__init__(**kwargs) 14 | 15 | self.level = 0 if level < 0 else level 16 | self.length = length 17 | self.kwargs = kwargs 18 | self._koch_curve(self.level, self.length, point, group=group) 19 | 20 | def _koch_curve(self, level, length, point, group=True): 21 | l = length / (3**level) 22 | points = [LEFT * l / 2, RIGHT * l / 2] 23 | self.set_points_as_corners(points) 24 | vmob = self.copy() 25 | for _ in range(level): 26 | new_vmob = VGroup() 27 | for i in (0, PI / 3, -PI / 3, 0): 28 | new_vmob.add(vmob.copy().rotate(i)) 29 | new_vmob.arrange(RIGHT, buff=0, aligned_edge=DOWN) 30 | vmob.become(new_vmob) 31 | vmob.move_to(point) 32 | if group: 33 | points = vmob.get_all_points() 34 | self.set_points(points) 35 | else: 36 | self.become(vmob) 37 | 38 | def new_level(self, level=None, length=None, point=None, **kwargs): 39 | level = self.level if level is None else level 40 | length = self.length if length is None else length 41 | point = self.get_center() if point is None else point 42 | self.kwargs.update(kwargs) 43 | kwargs = self.kwargs 44 | self.__init__(level, length, point, **kwargs) 45 | 46 | def next_level(self, **kwargs): 47 | level = self.level + 1 48 | self.new_level(level, **kwargs) 49 | 50 | def prev_level(self, **kwargs): 51 | level = self.level - 1 52 | self.new_level(level, **kwargs) 53 | 54 | 55 | class KochSnowflake(KochCurve): 56 | def __init__( 57 | self, 58 | level=0, 59 | length=config.frame_width / 3, 60 | point=ORIGIN, 61 | invert=False, 62 | fill_opacity=1, 63 | stroke_width=0, 64 | color=BLUE, 65 | **kwargs 66 | ): 67 | kwargs.update( 68 | { 69 | "fill_opacity": fill_opacity, 70 | "stroke_width": stroke_width, 71 | "color": color, 72 | } 73 | ) 74 | 75 | super().__init__(**kwargs) 76 | 77 | self.level = 0 if level < 0 else level 78 | self.length = length 79 | kwargs.update({"invert": invert}) 80 | self.kwargs = kwargs 81 | self._koch_snowflake(self.level, self.length, point, invert) 82 | 83 | def _koch_snowflake(self, level, length, point, invert): 84 | 85 | self._koch_curve(level, length, point, group=True) 86 | 87 | if invert: 88 | self.rotate(PI).reverse_direction() 89 | kc2 = self.copy().rotate(PI, about_point=self.get_top()) 90 | else: 91 | kc2 = self.copy().rotate(PI, about_point=self.get_bottom()) 92 | 93 | kc1 = self.copy().rotate(-PI / 3, about_point=self.get_end()) 94 | kc3 = self.copy().rotate(PI / 3, about_point=self.get_start()) 95 | ks = VGroup(kc1, kc2, kc3).move_to(point) 96 | 97 | points = ks.get_all_points() 98 | 99 | self.sides = ks.set_style(fill_opacity=0, stroke_width=4) 100 | 101 | self.set_points_as_corners(points) 102 | -------------------------------------------------------------------------------- /source/koch_curve.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mscene: Koch Curve 3 | 4 | https://mscene.curiouswalk.com/scenes/koch-curve 5 | """ 6 | 7 | import sys 8 | 9 | try: 10 | from mscene.fractal import * 11 | except ImportError: 12 | print( 13 | "Error: 'mscene.fractal' not found.\nInstall: 'pip install mscene && mscene plugins'", 14 | file=sys.stderr, 15 | ) 16 | sys.exit(1) 17 | 18 | from manim import * 19 | 20 | 21 | class ExampleOne(Scene): 22 | def construct(self): 23 | kc = KochCurve(2) 24 | self.add(kc) 25 | 26 | self.play(kc.animate.next_level()) 27 | self.wait() 28 | 29 | self.play(kc.animate.prev_level()) 30 | self.wait() 31 | 32 | 33 | class ExampleTwo(Scene): 34 | def construct(self): 35 | kc1 = KochCurve(level=1, stroke_width=6) 36 | kc2 = KochCurve(level=2, stroke_width=6, group=False) 37 | kc3 = KochCurve(level=3, stroke_width=6, group=False) 38 | colors = color_gradient((PURE_RED, PURE_BLUE), 4) 39 | 40 | for i, j, c in zip(kc2, kc3, colors): 41 | i.set_color(c) 42 | j.set_color(c) 43 | 44 | self.add(kc1) 45 | self.play(kc1.animate.next_level()) 46 | self.wait() 47 | 48 | self.play(FadeTransform(kc1, kc2)) 49 | self.wait() 50 | 51 | self.play(Transform(kc2, kc3, rate_func=there_and_back_with_pause, run_time=3)) 52 | self.wait() 53 | 54 | self.play(FadeTransform(kc2, kc1)) 55 | self.wait() 56 | 57 | self.play(kc1.animate.prev_level()) 58 | self.wait() 59 | 60 | 61 | class KochCurveScene(Scene): 62 | def construct(self): 63 | color = [ManimColor(hex) for hex in ("#0A68EF", "#0ADBEF", "#0A68EF")] 64 | kc = KochCurve(level=1, stroke_width=8, stroke_color=color) 65 | title = Text("Koch Curve\nLevel 1").to_corner(UL, buff=0.75) 66 | 67 | self.add(title, kc) 68 | self.wait() 69 | 70 | for level in (2, 3, 2, 1): 71 | self.play( 72 | kc.animate.new_level(level, stroke_width=8 - level), 73 | Transform(title[-1], Text(str(level)).move_to(title[-1])), 74 | ) 75 | self.wait() 76 | 77 | 78 | class Snowflake(Scene): 79 | def construct(self): 80 | color = [ManimColor(hex) for hex in ("#0ADBEF", "#0A68EF", "#1F0AEF")] 81 | ks = KochSnowflake(level=1, fill_color=color) 82 | title = Text("Koch Snowflake\nLevel 1").to_corner(UL, buff=0.75) 83 | 84 | self.add(title, ks) 85 | self.wait() 86 | 87 | for level in (2, 3, 2, 1): 88 | self.play( 89 | ks.animate.new_level(level), 90 | Transform(title[-1], Text(str(level)).move_to(title[-1])), 91 | ) 92 | self.wait() 93 | 94 | 95 | class Antisnowflake(Scene): 96 | def construct(self): 97 | color = [ManimColor(hex) for hex in ("#1F0AEF", "#0A68EF", "#0ADBEF")] 98 | ks = KochSnowflake(level=1, invert=True, fill_color=color) 99 | title = Text("Koch Anti-\nsnowflake\nLevel 1").to_corner(UL, buff=0.75) 100 | 101 | self.add(title, ks) 102 | self.wait() 103 | 104 | for level in (2, 3, 2, 1): 105 | self.play( 106 | ks.animate.new_level(level), 107 | Transform(title[-1], Text(str(level)).move_to(title[-1])), 108 | ) 109 | self.wait() 110 | 111 | 112 | class DualFlakes(Scene): 113 | def construct(self): 114 | snowflake = KochSnowflake(level=1, fill_color=ManimColor("#5d06e9")) 115 | anti_snowflake = KochSnowflake( 116 | level=1, invert=True, fill_color=ManimColor("#9e0168") 117 | ).align_to(snowflake, UP) 118 | 119 | self.add(snowflake, anti_snowflake) 120 | self.wait() 121 | 122 | for level in (2, 3, 2, 1): 123 | self.play( 124 | snowflake.animate.new_level(level), 125 | anti_snowflake.animate.new_level(level), 126 | run_time=1.5, 127 | ) 128 | self.wait(1.5) 129 | -------------------------------------------------------------------------------- /source/fibonacci_spiral.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mscene: Fibonacci Spiral 3 | 4 | https://mscene.curiouswalk.com/scenes/fibonacci-spiral 5 | """ 6 | 7 | from manim import * 8 | 9 | 10 | def fseq(n, a=0, b=1): 11 | """Return the first n Fibonacci numbers starting with a and b.""" 12 | seq = [] 13 | for _ in range(n): 14 | seq.append(a) 15 | a, b = b, a + b 16 | return seq 17 | 18 | 19 | def fsmob(n, width=None, height=None): 20 | """VGroup of Mobjects for the Fibonacci spiral. 21 | 22 | Args: 23 | n (int): Number of first Fibonacci terms. 24 | width (float | None): Width of the object (default: None). 25 | height (float | None): Height of the object (default: None). 26 | 27 | Returns: 28 | VGroup of Square, Text, ArcBetweenPoints and Dot. 29 | """ 30 | sqr_color = ManimColor("#214761") 31 | txt_color = ManimColor("#516572") 32 | arc_color = ManimColor("#04d9ff") 33 | dot_color = ManimColor("#cfff04") 34 | 35 | seq = fseq(n, 1) 36 | 37 | if width and not height: 38 | scale = width / sum(seq[-2:]) 39 | elif height and not width: 40 | scale = height / seq[-1] 41 | else: 42 | scale = 1 43 | 44 | mobjects = VGroup() 45 | squares = VGroup() 46 | 47 | if len(seq) % 2: 48 | angle = PI / 2 49 | direction = (RIGHT, UP, LEFT, DOWN) 50 | dot_index = (0, -1, -1, 0) 51 | else: 52 | angle = -PI / 2 53 | direction = (UP, RIGHT, DOWN, LEFT) 54 | dot_index = (0, 0, -1, -1) 55 | 56 | corner = (DL, UL) 57 | 58 | for i, t in enumerate(seq): 59 | square = Square(t * scale, stroke_width=6, color=sqr_color).next_to( 60 | squares, 61 | direction[i % 4], 62 | buff=0, 63 | ) 64 | 65 | dots = VGroup( 66 | Dot(square.get_corner(corner[i % 2]), color=dot_color), 67 | Dot(square.get_corner(-corner[i % 2]), color=dot_color), 68 | ) 69 | 70 | arc = ArcBetweenPoints( 71 | dots[dot_index[i % 4]].get_center(), 72 | dots[dot_index[i % 4] + 1].get_center(), 73 | angle=angle, 74 | color=arc_color, 75 | stroke_width=6, 76 | ) 77 | 78 | text = ( 79 | Text(f"{t}×{t}", color=txt_color) 80 | .scale_to_fit_width(square.width * 0.5) 81 | .move_to(square) 82 | ) 83 | 84 | vgrp = VGroup(square, text, arc, dots) 85 | vgrp[2:].set_z_index(1) 86 | squares.add(square) 87 | mobjects.add(vgrp) 88 | 89 | mobjects.center() 90 | 91 | return mobjects 92 | 93 | 94 | def fsmob_anim(mob, mode="IN", lag_ratio=0.125, **kwargs): 95 | """Return an AnimationGroup for the Fibonacci spiral.""" 96 | 97 | if mode == "IN": 98 | 99 | anim = [(FadeIn(i[0]), Write(i[1]), Create(i[2]), FadeIn(i[3])) for i in mob] 100 | elif mode == "OUT": 101 | anim = [ 102 | (FadeOut(i[0]), Unwrite(i[1]), Uncreate(i[2]), FadeOut(i[3])) 103 | for i in mob[::-1] 104 | ] 105 | else: 106 | raise ValueError("mode must be 'IN' or 'OUT'") 107 | 108 | return AnimationGroup(*anim, lag_ratio=lag_ratio, **kwargs) 109 | 110 | 111 | class SceneOne(Scene): 112 | def construct(self): 113 | seq = fseq(25) 114 | width = config.frame_width * 3 / 4 115 | seq_str = ", ".join(map(str, seq)) + ", ..." 116 | 117 | title = Text("Fibonacci Sequence").scale_to_fit_width(width * 3 / 4) 118 | text = MarkupText(seq_str, width=width, justify=True, font_size=130) 119 | VGroup(title, text).arrange(DOWN, buff=3 / 4) 120 | 121 | self.play(Write(title), Write(text, run_time=3)) 122 | self.wait(3) 123 | 124 | 125 | class SceneTwo(Scene): 126 | def construct(self): 127 | width = config.frame_width * 3 / 4 128 | mob = fsmob(6, width=width) 129 | 130 | self.play(fsmob_anim(mob)) 131 | self.wait(2.5) 132 | 133 | self.play(fsmob_anim(mob, mode="OUT")) 134 | self.wait(0.5) 135 | 136 | 137 | class SceneThree(Scene): 138 | def construct(self): 139 | width = config.frame_width * 3 / 4 140 | terms = [4, 8, 12] 141 | term = None 142 | mob = None 143 | 144 | for n in terms: 145 | _mob = fsmob(n, width) 146 | 147 | if mob is None: 148 | self.play(fsmob_anim(_mob)) 149 | else: 150 | i = term if term < n else -1 151 | self.play(ReplacementTransform(mob, _mob[:i])) 152 | self.wait(0.5) 153 | self.play(fsmob_anim(_mob[i:])) 154 | 155 | mob = _mob 156 | term = n 157 | self.wait(1.5) 158 | 159 | self.play(fsmob_anim(mob, mode="OUT", lag_ratio=0)) 160 | self.wait(0.5) 161 | 162 | 163 | class SceneFour(Scene): 164 | def construct(self): 165 | n = 12 166 | width = config.frame_width * 3 / 4 167 | mob = fsmob(n, width) 168 | mob.save_state() 169 | 170 | width *= sum(fseq(n)[-2:]) * 3 / 4 171 | _mob = fsmob(n, width) 172 | _mob.shift(-_mob[0].get_center()) 173 | 174 | self.add(mob) 175 | self.wait(0.5) 176 | 177 | self.play(Transform(mob, _mob, run_time=6)) 178 | self.wait(2) 179 | 180 | self.play(Restore(mob, run_time=4)) 181 | self.wait(0.5) 182 | -------------------------------------------------------------------------------- /plugins/extras.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | 3 | 4 | class DashFlow(DashedVMobject): 5 | """A dashed VMobject with its dash offset continuously shifting for a flowing effect. 6 | 7 | Args: 8 | vmob (VMobject): The VMobject to animate. 9 | rate (float): The rate of shifting dash offset over time (default: 1.0). 10 | **kwargs: Additional keyword arguments passed to DashedVMobject. 11 | 12 | Methods: 13 | play(): 14 | Resume updater to play animation. 15 | pause(): 16 | Suspend updater to pause animation. 17 | stop(): 18 | Clear updater to stop animation. 19 | """ 20 | 21 | def __init__(self, vmob, rate=1.0, dash_offset=0, **kwargs): 22 | vmob = vmob.copy() 23 | super().__init__(vmob, dash_offset=dash_offset, **kwargs) 24 | 25 | self.rate = rate 26 | self.offset = dash_offset 27 | 28 | def _updater(mobj, dt): 29 | self.offset = (self.offset + dt * self.rate) % 1 30 | mobj.become(DashedVMobject(vmob, dash_offset=self.offset, **kwargs)) 31 | 32 | self.add_updater(_updater) 33 | 34 | def play(self): 35 | self.resume_updating() 36 | 37 | def pause(self): 38 | self.suspend_updating() 39 | 40 | def stop(self): 41 | self.clear_updaters() 42 | 43 | 44 | class DrawArc(Succession): 45 | def __init__(self, arc, reverse=False, run_time=2, **kwargs): 46 | quarter_time = run_time / 4 47 | half_time = run_time / 2 48 | 49 | if reverse: 50 | line = Line( 51 | arc.get_arc_center(), 52 | arc.get_end(), 53 | stroke_width=arc.get_stroke_width(), 54 | stroke_color=arc.get_stroke_color(), 55 | ) 56 | create_arc = Uncreate(arc, run_time=half_time) 57 | rotate_line = Rotate( 58 | line, -arc.angle, about_point=arc.get_arc_center(), run_time=half_time 59 | ) 60 | else: 61 | line = Line( 62 | arc.get_arc_center(), 63 | arc.get_start(), 64 | stroke_width=arc.get_stroke_width(), 65 | stroke_color=arc.get_stroke_color(), 66 | ) 67 | create_arc = Create(arc, run_time=half_time) 68 | rotate_line = Rotate( 69 | line, arc.angle, about_point=arc.get_arc_center(), run_time=half_time 70 | ) 71 | 72 | super().__init__( 73 | Create(line, run_time=quarter_time), 74 | AnimationGroup( 75 | create_arc, 76 | rotate_line, 77 | ), 78 | Uncreate(line, run_time=quarter_time), 79 | **kwargs, 80 | ) 81 | 82 | 83 | class FlashFade(AnimationGroup): 84 | """Animation for fading VMobjects with flashing outlines. 85 | 86 | Args: 87 | vmob (VMobject): The VMobject to animate. 88 | mode (str): "IN" to fade in, "OUT" to fade out, "AUTO" to fade in and out (default: "AUTO"). 89 | reverse (bool): If True, reverses the animation direction (default: False). 90 | color (ManimColor | None): The stroke color of the flash outline (default: None). 91 | width (float | None): The stroke width of the flash outline (default: None). 92 | opacity (float | None): The stroke opacity of the flash outline (default: None). 93 | time_width (float): The length of the sliver relative to the length of the stroke (default: 0.5). 94 | duration (float): The duration of each sub-animation (default: 1.0). 95 | lag_ratio (float): The lag ratio between sub-animations (default: 0.125). 96 | **kwargs: Additional keyword arguments for AnimationGroup. 97 | """ 98 | 99 | def __init__( 100 | self, 101 | vmob: VMobject, 102 | mode="AUTO", 103 | reverse=False, 104 | color=None, 105 | width=None, 106 | opacity=None, 107 | time_width=0.5, 108 | duration=1.0, 109 | lag_ratio=0.125, 110 | **kwargs, 111 | ): 112 | if reverse: 113 | vmob = vmob[::-1] 114 | vcopy = vmob.copy() 115 | vcopy.set_fill(opacity=0).set_stroke(color, width, opacity) 116 | for vm in vcopy: 117 | vm = vm.reverse_direction() 118 | else: 119 | vcopy = vmob.copy() 120 | vcopy.set_fill(opacity=0).set_stroke(color, width, opacity) 121 | 122 | mode = mode.upper() 123 | n = len(vmob) 124 | anim = [] 125 | 126 | if mode == "IN": 127 | for i in range(n): 128 | anim.append( 129 | AnimationGroup( 130 | FadeIn(vmob[i]), 131 | ShowPassingFlash(vcopy[i], time_width=time_width), 132 | run_time=duration, 133 | ) 134 | ) 135 | elif mode == "OUT": 136 | for i in range(n): 137 | anim.append( 138 | AnimationGroup( 139 | FadeOut(vmob[i]), 140 | ShowPassingFlash(vcopy[i], time_width=time_width), 141 | run_time=duration, 142 | ) 143 | ) 144 | else: 145 | for i in range(n): 146 | anim.append( 147 | AnimationGroup( 148 | FadeIn(vmob[i].copy(), rate_func=there_and_back_with_pause), 149 | ShowPassingFlash(vcopy[i], time_width=time_width), 150 | run_time=duration, 151 | ) 152 | ) 153 | 154 | super().__init__(*anim, lag_ratio=lag_ratio, **kwargs) 155 | -------------------------------------------------------------------------------- /source/scenes/mscene.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "provenance": [], 7 | "gpuType": "V28", 8 | "include_colab_link": true 9 | }, 10 | "kernelspec": { 11 | "name": "python3", 12 | "display_name": "Python 3" 13 | }, 14 | "language_info": { 15 | "name": "python" 16 | }, 17 | "accelerator": "TPU" 18 | }, 19 | "cells": [ 20 | { 21 | "cell_type": "markdown", 22 | "metadata": { 23 | "id": "view-in-github", 24 | "colab_type": "text" 25 | }, 26 | "source": [ 27 | "\"Open" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "source": [ 33 | "# Mscene\n", 34 | "\n", 35 | "A Python library for creating science animation with [Manim](https://www.manim.community) in Google [Colab](https://colab.google).
✨ [mscene.curiouswalk.com](https://mscene.curiouswalk.com)" 36 | ], 37 | "metadata": { 38 | "id": "yNXt9zhZfn6h" 39 | } 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "source": [ 44 | "## Setup" 45 | ], 46 | "metadata": { 47 | "id": "eULwb3Ini5Q1" 48 | } 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "source": [ 53 | "**Installation**" 54 | ], 55 | "metadata": { 56 | "id": "2g1ykPfzqoXm" 57 | } 58 | }, 59 | { 60 | "cell_type": "code", 61 | "source": [ 62 | "%pip install -q mscene\n", 63 | "import mscene\n", 64 | "%mscene -l manim" 65 | ], 66 | "metadata": { 67 | "id": "hL1OOSlHcGk1" 68 | }, 69 | "execution_count": null, 70 | "outputs": [] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "source": [ 75 | "**Import**" 76 | ], 77 | "metadata": { 78 | "id": "RwkhMGtCqxXG" 79 | } 80 | }, 81 | { 82 | "cell_type": "code", 83 | "source": [ 84 | "from mscene.manim import *" 85 | ], 86 | "metadata": { 87 | "id": "smJo8BfrKfvn" 88 | }, 89 | "execution_count": null, 90 | "outputs": [] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "source": [ 95 | "## Scenes" 96 | ], 97 | "metadata": { 98 | "id": "ChNWO0aP7prt" 99 | } 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "source": [ 104 | "**Example Scene**" 105 | ], 106 | "metadata": { 107 | "id": "wz6vt4gMuFxK" 108 | } 109 | }, 110 | { 111 | "cell_type": "code", 112 | "source": [ 113 | "%%manim -qm ManimScene\n", 114 | "\n", 115 | "class ManimScene(Scene):\n", 116 | " def construct(self):\n", 117 | " banner = ManimBanner()\n", 118 | " self.play(banner.create())\n", 119 | " self.play(banner.expand())\n", 120 | " self.wait(1.5)" 121 | ], 122 | "metadata": { 123 | "id": "Opt7RsK_8Fxc" 124 | }, 125 | "execution_count": null, 126 | "outputs": [] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "source": [ 131 | "**Adding Plugins**" 132 | ], 133 | "metadata": { 134 | "id": "RFj_bFXatY_E" 135 | } 136 | }, 137 | { 138 | "cell_type": "code", 139 | "source": [ 140 | "import mscene\n", 141 | "%mscene plugins\n", 142 | "from mscene.plugins import *" 143 | ], 144 | "metadata": { 145 | "id": "qFLa9HLZau_r" 146 | }, 147 | "execution_count": null, 148 | "outputs": [] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "source": [ 153 | "**Example Plugin**" 154 | ], 155 | "metadata": { 156 | "id": "vTmnOm5HtsaA" 157 | } 158 | }, 159 | { 160 | "cell_type": "code", 161 | "source": [ 162 | "%%manim -qm FractalScene\n", 163 | "\n", 164 | "class FractalScene(Scene):\n", 165 | " def construct(self):\n", 166 | " ks = KochSnowflake(level=2)\n", 167 | " self.add(ks)\n", 168 | " self.play(ks.animate.next_level())\n", 169 | " self.wait(1.5)\n", 170 | " self.play(ks.animate.prev_level())\n", 171 | " self.wait(1.5)" 172 | ], 173 | "metadata": { 174 | "id": "VjYweG39a8wi" 175 | }, 176 | "execution_count": null, 177 | "outputs": [] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "source": [ 182 | "**New Scene**" 183 | ], 184 | "metadata": { 185 | "id": "Kmcjj05JU3BQ" 186 | } 187 | }, 188 | { 189 | "cell_type": "code", 190 | "source": [ 191 | "%%manim -qm NewScene\n", 192 | "\n", 193 | "class NewScene(Scene):\n", 194 | " def construct(self):\n" 195 | ], 196 | "metadata": { 197 | "id": "bVcGeggWVSOY" 198 | }, 199 | "execution_count": null, 200 | "outputs": [] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "source": [ 205 | "## End Session\n", 206 | "\n", 207 | "Run the cell below to disconnect the runtime and terminate the session." 208 | ], 209 | "metadata": { 210 | "id": "Fn6lcBcm9vSl" 211 | } 212 | }, 213 | { 214 | "cell_type": "code", 215 | "source": [ 216 | "from google.colab import runtime\n", 217 | "runtime.unassign()" 218 | ], 219 | "metadata": { 220 | "id": "CnoWqKyO9xm_" 221 | }, 222 | "execution_count": null, 223 | "outputs": [] 224 | } 225 | ] 226 | } -------------------------------------------------------------------------------- /source/clips/flash_fade.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "provenance": [], 7 | "gpuType": "V28", 8 | "authorship_tag": "ABX9TyOAr0hextHh8vGuN73XTk6c", 9 | "include_colab_link": true 10 | }, 11 | "kernelspec": { 12 | "name": "python3", 13 | "display_name": "Python 3" 14 | }, 15 | "language_info": { 16 | "name": "python" 17 | }, 18 | "accelerator": "TPU" 19 | }, 20 | "cells": [ 21 | { 22 | "cell_type": "markdown", 23 | "metadata": { 24 | "id": "view-in-github", 25 | "colab_type": "text" 26 | }, 27 | "source": [ 28 | "\"Open" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "source": [ 34 | "# Flash Fade\n", 35 | "\n", 36 | "`FlashFade` is an animation for fading VMobjects with flashing outlines." 37 | ], 38 | "metadata": { 39 | "id": "Y_zEyJhMI89n" 40 | } 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "source": [ 45 | "## Setup" 46 | ], 47 | "metadata": { 48 | "id": "-56bTsZD8Wvk" 49 | } 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "source": [ 54 | "Installation" 55 | ], 56 | "metadata": { 57 | "id": "Nftp_no9RWWN" 58 | } 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": { 64 | "id": "0H01NHPM6l-8" 65 | }, 66 | "outputs": [], 67 | "source": [ 68 | "%pip install -q mscene\n", 69 | "import mscene\n", 70 | "%mscene -l manim plugins" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "source": [ 76 | "Imports" 77 | ], 78 | "metadata": { 79 | "id": "awqF3HPMRdAe" 80 | } 81 | }, 82 | { 83 | "cell_type": "code", 84 | "source": [ 85 | "from mscene.manim import *\n", 86 | "from mscene.plugins import FlashFade\n", 87 | "print(FlashFade.__doc__)" 88 | ], 89 | "metadata": { 90 | "id": "GeFqrCHT8cB2" 91 | }, 92 | "execution_count": null, 93 | "outputs": [] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "source": [ 98 | "# Scenes" 99 | ], 100 | "metadata": { 101 | "id": "Cjbv_GHx-hdQ" 102 | } 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "source": [ 107 | "Scene One" 108 | ], 109 | "metadata": { 110 | "id": "6uHpp6QLRqq-" 111 | } 112 | }, 113 | { 114 | "cell_type": "code", 115 | "source": [ 116 | "%%manim -qm SceneOne\n", 117 | "\n", 118 | "class SceneOne(Scene):\n", 119 | " def construct(self):\n", 120 | " text = Text(\"Hello World\", font_size=140)\n", 121 | " blues = [BLUE_A, BLUE_C, BLUE_E]\n", 122 | " kwargs = dict(color=blues, width=6, duration=1.5)\n", 123 | "\n", 124 | " self.play(FlashFade(text, mode=\"IN\", **kwargs))\n", 125 | " self.wait(1.5)\n", 126 | "\n", 127 | " self.play(FlashFade(text, mode=\"OUT\", reverse=True, **kwargs))\n", 128 | " self.wait()\n", 129 | "\n", 130 | " self.play(FlashFade(text, **kwargs))\n", 131 | " self.wait(0.5)" 132 | ], 133 | "metadata": { 134 | "id": "yowjL9pc-jhf" 135 | }, 136 | "execution_count": null, 137 | "outputs": [] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "source": [ 142 | "Scene Two" 143 | ], 144 | "metadata": { 145 | "id": "2PwiyPofRtPR" 146 | } 147 | }, 148 | { 149 | "cell_type": "code", 150 | "source": [ 151 | "%%manim -qm SceneTwo\n", 152 | "\n", 153 | "class SceneTwo(Scene):\n", 154 | " def construct(self):\n", 155 | " # Aibohphobia is a (humorous) term for the irrational fear of palindromes.\n", 156 | " text = Text(\"AIBOHPHOBIA\", font_size=100)\n", 157 | " tp1 = text[:5]\n", 158 | " tp2 = text[5:]\n", 159 | " reds = [RED_A, RED_C, RED_E]\n", 160 | " kwargs = dict(color=reds, width=5, duration=1.5, lag_ratio=0.25)\n", 161 | "\n", 162 | " self.play(\n", 163 | " FlashFade(tp1, mode=\"IN\", reverse=True, **kwargs),\n", 164 | " FlashFade(tp2, mode=\"IN\", **kwargs),\n", 165 | " )\n", 166 | " self.wait(1.5)\n", 167 | "\n", 168 | " self.play(\n", 169 | " FlashFade(tp1, mode=\"OUT\", **kwargs),\n", 170 | " FlashFade(tp2, mode=\"OUT\", reverse=True, **kwargs),\n", 171 | " )\n", 172 | " self.wait(0.5)" 173 | ], 174 | "metadata": { 175 | "id": "EJQGxdlD-kOt" 176 | }, 177 | "execution_count": null, 178 | "outputs": [] 179 | }, 180 | { 181 | "cell_type": "markdown", 182 | "source": [ 183 | "Scene Three" 184 | ], 185 | "metadata": { 186 | "id": "UV22ecE4RusP" 187 | } 188 | }, 189 | { 190 | "cell_type": "code", 191 | "source": [ 192 | "%%manim -qm SceneThree\n", 193 | "\n", 194 | "class SceneThree(Scene):\n", 195 | " def construct(self):\n", 196 | " shapes = ManimBanner()[0].fade().center()\n", 197 | " self.play(FlashFade(shapes, width=6, opacity=1, lag_ratio=0.5, duration=1.5))\n", 198 | " self.wait(0.5)" 199 | ], 200 | "metadata": { 201 | "id": "buG206GpH5HC" 202 | }, 203 | "execution_count": null, 204 | "outputs": [] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "source": [ 209 | "## End Session\n", 210 | "\n", 211 | "Run the cell below to disconnect the runtime and terminate the session." 212 | ], 213 | "metadata": { 214 | "id": "ltpqk5LuQUZP" 215 | } 216 | }, 217 | { 218 | "cell_type": "code", 219 | "source": [ 220 | "from google.colab import runtime\n", 221 | "runtime.unassign()" 222 | ], 223 | "metadata": { 224 | "id": "F3Nxfl5oMNQ6" 225 | }, 226 | "execution_count": null, 227 | "outputs": [] 228 | } 229 | ] 230 | } -------------------------------------------------------------------------------- /source/scenes/cycloids.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "provenance": [], 7 | "gpuType": "V28", 8 | "include_colab_link": true 9 | }, 10 | "kernelspec": { 11 | "name": "python3", 12 | "display_name": "Python 3" 13 | }, 14 | "language_info": { 15 | "name": "python" 16 | }, 17 | "accelerator": "TPU" 18 | }, 19 | "cells": [ 20 | { 21 | "cell_type": "markdown", 22 | "metadata": { 23 | "id": "view-in-github", 24 | "colab_type": "text" 25 | }, 26 | "source": [ 27 | "\"Open" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "source": [ 33 | "# Cycloids\n", 34 | "\n", 35 | "\n", 36 | "\n", 37 | "Learn how a circle rolls along a straight line, creating a cycloid, and explore its variations: epicycloid and hypocycloid, formed when they roll along the outside or inside edge of another circle. Roll circles in different configurations to draw various cycloidal curves and visualize the intriguing patterns. Simulate to study the applications of these fascinating curves in multiple fields, including mathematics, physics, and engineering. Perfect for anyone curious about the geometry of rolling circles. ✨ [mscene.curiouswalk.com/scenes/cycloids](https://mscene.curiouswalk.com/scenes/cycloids)" 38 | ], 39 | "metadata": { 40 | "id": "Vf3VcAXsKO6b" 41 | } 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "source": [ 46 | "## Setup" 47 | ], 48 | "metadata": { 49 | "id": "GqUiZVjOMz_r" 50 | } 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "source": [ 55 | "Installation" 56 | ], 57 | "metadata": { 58 | "id": "c34lKbph84az" 59 | } 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": { 65 | "id": "adwd99STI--b" 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "%pip install -q mscene\n", 70 | "import mscene\n", 71 | "%mscene -l manim plugins" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "source": [ 77 | "Imports" 78 | ], 79 | "metadata": { 80 | "id": "IBrxn3hZ85MR" 81 | } 82 | }, 83 | { 84 | "cell_type": "code", 85 | "source": [ 86 | "from mscene.manim import *\n", 87 | "from mscene.roulette import *" 88 | ], 89 | "metadata": { 90 | "id": "EkbTlUS-f2Gm" 91 | }, 92 | "execution_count": null, 93 | "outputs": [] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "source": [ 98 | "## Scenes\n", 99 | "\n", 100 | "* [**Cycloid**](https://mscene.curiouswalk.com/scenes/cycloids/cycloid)\n", 101 | "* [**Epicycloid**](https://mscene.curiouswalk.com/scenes/cycloids/epicycloid)\n", 102 | "* [**Hypocycloid**](https://mscene.curiouswalk.com/scenes/cycloids/hypocycloid)" 103 | ], 104 | "metadata": { 105 | "id": "iiS7LDtw0Rrq" 106 | } 107 | }, 108 | { 109 | "cell_type": "code", 110 | "source": [ 111 | "%%manim -qm Cycloids\n", 112 | "\n", 113 | "class Cycloids(Scene):\n", 114 | " def construct(self):\n", 115 | " radius = 0.8\n", 116 | " length = TAU * radius\n", 117 | "\n", 118 | " circle_one = Circle(radius=radius, color=ManimColor(\"#C5C9C7\")).rotate(PI / 2)\n", 119 | "\n", 120 | " circle_two = Circle(radius=3 * radius, color=ManimColor(\"#C5C9C7\")).rotate(\n", 121 | " PI / 2\n", 122 | " )\n", 123 | "\n", 124 | " line_end = circle_two.get_bottom()\n", 125 | " line_start = line_end + length * LEFT\n", 126 | "\n", 127 | " line = Line(line_start, line_end, color=ManimColor(\"#C5C9C7\"))\n", 128 | "\n", 129 | " wheel = Wheel(radius=radius, color=ManimColor(\"#04D9FF\"))\n", 130 | "\n", 131 | " self.play(GrowFromCenter(wheel))\n", 132 | " self.wait()\n", 133 | "\n", 134 | " self.play(Create(line), wheel.animate.move(line_start, UP))\n", 135 | " self.bring_to_front(wheel)\n", 136 | "\n", 137 | " marker = [(1, PI / 2, ManimColor(\"#6019E3\"))]\n", 138 | " self.play(wheel.draw_markers(marker))\n", 139 | "\n", 140 | " path = wheel.trace_paths(dissipating_time=2)\n", 141 | " self.add(path)\n", 142 | "\n", 143 | " # rolls along the line\n", 144 | " self.play(wheel.roll(length * RIGHT, run_time=2.5))\n", 145 | " self.wait()\n", 146 | "\n", 147 | " self.play(ReplacementTransform(line, circle_one))\n", 148 | " self.wait()\n", 149 | "\n", 150 | " # rolls along the outer edge of circle one\n", 151 | " self.play(wheel.roll(TAU, about=circle_one, run_time=2.5))\n", 152 | " self.wait()\n", 153 | "\n", 154 | " self.play(ReplacementTransform(circle_one, circle_two))\n", 155 | " self.wait()\n", 156 | "\n", 157 | " # rolls along the inner edge of circle two\n", 158 | " self.play(wheel.roll(TAU, about=circle_two, run_time=3))\n", 159 | " self.wait(2)\n", 160 | " path.clear_updaters()\n", 161 | " self.remove(path)\n", 162 | " self.play(wheel.undraw_markers())\n", 163 | "\n", 164 | " self.play(Uncreate(circle_two), wheel.animate.move(ORIGIN))\n", 165 | " self.wait()\n", 166 | "\n", 167 | " self.play(ShrinkToCenter(wheel))\n", 168 | " self.wait(0.5)" 169 | ], 170 | "metadata": { 171 | "id": "YPWP-Id8UAlZ" 172 | }, 173 | "execution_count": null, 174 | "outputs": [] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "source": [ 179 | "## Download Script\n", 180 | "\n", 181 | "Run the cell below to download the scene script." 182 | ], 183 | "metadata": { 184 | "id": "X4N5yMv49AsN" 185 | } 186 | }, 187 | { 188 | "cell_type": "code", 189 | "source": [ 190 | "import mscene\n", 191 | "%mscene cycloids.py" 192 | ], 193 | "metadata": { 194 | "id": "OBupcua-9PrD" 195 | }, 196 | "execution_count": null, 197 | "outputs": [] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "source": [ 202 | "## End Session\n", 203 | "\n", 204 | "Run the cell below to disconnect the runtime and terminate the session." 205 | ], 206 | "metadata": { 207 | "id": "4Vzp6XMkeoci" 208 | } 209 | }, 210 | { 211 | "cell_type": "code", 212 | "source": [ 213 | "from google.colab import runtime\n", 214 | "runtime.unassign()" 215 | ], 216 | "metadata": { 217 | "id": "aC2_yLZEepGW" 218 | }, 219 | "execution_count": null, 220 | "outputs": [] 221 | } 222 | ] 223 | } -------------------------------------------------------------------------------- /plugins/roulette.py: -------------------------------------------------------------------------------- 1 | from manim import * 2 | 3 | 4 | class Wheel(VGroup): 5 | """A class representing a rolling circle that rolls without sliding along a straight line or around another circle, with markers tracing cycloids as it moves. This allows for the simulation of rolling motion and the generation of cycloidal curves for various configurations.""" 6 | 7 | def __init__( 8 | self, 9 | radius: float = 1.0, 10 | color: ManimColor = BLUE, 11 | markers: list | None = None, 12 | point=ORIGIN, 13 | num_dashes=None, 14 | angle=None, 15 | **kwargs 16 | ): 17 | super().__init__(**kwargs) 18 | 19 | self.point = point 20 | 21 | self.radius = radius 22 | 23 | if num_dashes is None: 24 | num_dashes = int(14 * radius) 25 | 26 | self.circle = DashedVMobject( 27 | Circle( 28 | arc_center=self.point, radius=self.radius, stroke_width=5, color=color 29 | ), 30 | num_dashes=num_dashes, 31 | ) 32 | 33 | if angle is not None: 34 | self.circle.rotate(angle) 35 | 36 | self.dot = Dot(point=self.point, radius=0.08, color=color) 37 | 38 | self.markers = VGroup() 39 | 40 | if markers is not None: 41 | 42 | for marker in markers: 43 | new_marker = self._get_marker(*marker) 44 | self.markers.add(new_marker) 45 | 46 | self.add(self.circle, self.markers, self.dot) 47 | 48 | def _get_point(self, r, theta): 49 | """Gets polar to cartesian coordinates around the center point.""" 50 | 51 | x = r * np.cos(theta) 52 | y = r * np.sin(theta) 53 | self.point = self.dot.get_center() 54 | point = self.point + np.array([x, y, 0]) 55 | 56 | return point 57 | 58 | def _get_arc_angle(self, point1, point2): 59 | """Gets path_arc for Transform animation in transform_markers.""" 60 | 61 | a = (point1.angle - point2.angle) % TAU 62 | b = (point2.angle - point1.angle) % TAU 63 | arc_angle = -a if a < b else b 64 | 65 | return arc_angle 66 | 67 | def _get_marker(self, r, theta, color=RED, line=True): 68 | """Gets marker around the center point.""" 69 | 70 | marker = VGroup() 71 | 72 | marker.distance = r * self.radius 73 | 74 | marker.angle = theta 75 | 76 | point = self._get_point(marker.distance, marker.angle) 77 | 78 | marker.dot = Dot(point, radius=0.09, color=color) 79 | 80 | if line is not None: 81 | if isinstance(line, ManimColor): 82 | line_color = line 83 | else: 84 | line_color = average_color(self.dot.color, color) 85 | 86 | marker.line = Line( 87 | self.dot.get_center(), point, stroke_width=5, stroke_color=line_color 88 | ) 89 | 90 | marker.add(marker.line) 91 | else: 92 | marker.line = None 93 | 94 | marker.add(marker.dot) 95 | 96 | return marker 97 | 98 | def move(self, point, direction=ORIGIN): 99 | """Moves to the given point or Mobject.""" 100 | 101 | if isinstance(point, Mobject): 102 | point = point.get_center() 103 | 104 | self.point = self.dot.get_center() 105 | 106 | target = point - self.point + self.radius * direction 107 | 108 | self.point = point 109 | 110 | self.shift(target) 111 | 112 | return self 113 | 114 | def draw_markers(self, markers, **kwargs): 115 | """Grows markers from the center point.""" 116 | 117 | self.point = self.dot.get_center() 118 | 119 | new_markers = VGroup() 120 | 121 | for marker in markers: 122 | 123 | new_marker = self._get_marker(*marker) 124 | 125 | new_markers.add(new_marker) 126 | 127 | self.markers.add(new_marker) 128 | 129 | self.dot.set_z_index(1) 130 | 131 | anim = GrowFromPoint(new_markers, self.point, **kwargs) 132 | 133 | return anim 134 | 135 | def transform_markers(self, targets, idx=None, **kwargs): 136 | """Transforms markers into target markers.""" 137 | 138 | if idx is None: 139 | markers = VGroup(*self.markers) 140 | else: 141 | markers = VGroup(*[self.markers[i] for i in idx]) 142 | 143 | anim = [] 144 | 145 | for marker, target in zip(markers, targets): 146 | 147 | new_marker = self._get_marker(*target) 148 | path_arc = self._get_arc_angle(marker, new_marker) 149 | 150 | marker.distance = new_marker.distance 151 | marker.angle = new_marker.angle 152 | 153 | anim.append(Transform(marker, new_marker, path_arc=path_arc, **kwargs)) 154 | 155 | return anim 156 | 157 | def undraw_markers(self, idx=None, **kwargs): 158 | """Shrinks markers into the center point.""" 159 | 160 | self.point = self.dot.get_center() 161 | 162 | if idx is None: 163 | markers = VGroup(*self.markers) 164 | else: 165 | markers = VGroup(*[self.markers[i] for i in idx]) 166 | 167 | self.markers.remove(*markers) 168 | 169 | self.dot.set_z_index(1) 170 | 171 | anim = GrowFromPoint( 172 | markers, self.point, reverse_rate_function=True, remover=True, **kwargs 173 | ) 174 | 175 | return anim 176 | 177 | def trace_paths(self, idx=None, stroke_width=4, **kwargs): 178 | """Traces the path of markers.""" 179 | 180 | if idx is None: 181 | markers = VGroup(*self.markers) 182 | else: 183 | markers = VGroup(*[self.markers[i] for i in idx]) 184 | 185 | paths = VGroup() 186 | 187 | for marker in markers: 188 | paths.add( 189 | TracedPath( 190 | marker.dot.get_center, 191 | stroke_color=marker.dot.color, 192 | stroke_width=stroke_width, 193 | **kwargs 194 | ) 195 | ) 196 | 197 | return paths 198 | 199 | def roll( 200 | self, 201 | direction, 202 | about=None, 203 | reverse=False, 204 | rate_func=linear, 205 | run_time=2, 206 | **kwargs 207 | ): 208 | """Rolls without sliding along a straight line or around another circle in the same plane.""" 209 | 210 | self.point = self.dot.get_center() 211 | 212 | self.circle.angle = 0 213 | 214 | for marker in self.markers: 215 | marker.theta = marker.angle 216 | 217 | if about is None: 218 | 219 | distance = np.linalg.norm(direction) 220 | 221 | if any(direction > ORIGIN): 222 | distance *= -1 223 | 224 | else: 225 | 226 | if isinstance(about, Mobject): 227 | length = about.width / 2 228 | dis = self.point - about.get_center() 229 | else: 230 | length = 0 231 | dis = self.point - about 232 | 233 | radius = np.linalg.norm(dis) 234 | 235 | theta = angle_of_vector(dis) 236 | 237 | distance = radius * direction 238 | 239 | if radius < length: 240 | distance *= -1 241 | 242 | if reverse: 243 | distance *= -1 244 | 245 | def update_alpha(self, alpha): 246 | 247 | angle = (alpha * distance) / self.radius 248 | 249 | if about is None: 250 | 251 | point1 = self.point + alpha * direction 252 | 253 | else: 254 | 255 | point1_angle = alpha * direction + theta 256 | 257 | point1 = ORIGIN + ( 258 | np.cos(point1_angle) * radius, 259 | np.sin(point1_angle) * radius, 260 | 0.0, 261 | ) 262 | 263 | self.circle.rotate(angle - self.circle.angle).move_to(point1) 264 | 265 | self.circle.angle = angle 266 | 267 | self.dot.move_to(point1) 268 | 269 | for marker in self.markers: 270 | 271 | point2_angle = angle + marker.theta 272 | marker.angle = point2_angle 273 | 274 | point2 = point1 + ( 275 | np.cos(point2_angle) * marker.distance, 276 | np.sin(point2_angle) * marker.distance, 277 | 0.0, 278 | ) 279 | 280 | if marker.line: 281 | marker.line.set_points_by_ends(point1, point2) 282 | 283 | marker.dot.move_to(point2) 284 | 285 | anim = UpdateFromAlphaFunc( 286 | self, update_alpha, rate_func=rate_func, run_time=run_time, **kwargs 287 | ) 288 | 289 | return anim 290 | -------------------------------------------------------------------------------- /source/cycloids.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mscene: Cycloids 3 | 4 | https://mscene.curiouswalk.com/scenes/cycloids 5 | """ 6 | 7 | import sys 8 | 9 | try: 10 | from mscene.roulette import * 11 | except ImportError: 12 | print( 13 | "Error: 'mscene.roulette' not found.\nInstall: 'pip install mscene && mscene plugins'", 14 | file=sys.stderr, 15 | ) 16 | sys.exit(1) 17 | 18 | from manim import * 19 | 20 | 21 | class Cycloids(Scene): 22 | def construct(self): 23 | 24 | radius = 0.8 25 | length = TAU * radius 26 | 27 | circle_one = Circle(radius=radius, color=ManimColor("#C5C9C7")).rotate(PI / 2) 28 | 29 | circle_two = Circle(radius=3 * radius, color=ManimColor("#C5C9C7")).rotate( 30 | PI / 2 31 | ) 32 | 33 | line_end = circle_two.get_bottom() 34 | line_start = line_end + length * LEFT 35 | 36 | line = Line(line_start, line_end, color=ManimColor("#C5C9C7")) 37 | 38 | wheel = Wheel(radius=radius, color=ManimColor("#04D9FF")) 39 | 40 | self.play(GrowFromCenter(wheel)) 41 | self.wait() 42 | 43 | self.play(Create(line), wheel.animate.move(line_start, UP)) 44 | self.bring_to_front(wheel) 45 | 46 | marker = [(1, PI / 2, ManimColor("#6019E3"))] 47 | self.play(wheel.draw_markers(marker)) 48 | 49 | path = wheel.trace_paths(dissipating_time=2) 50 | self.add(path) 51 | 52 | # rolls along the line 53 | self.play(wheel.roll(length * RIGHT, run_time=2.5)) 54 | self.wait() 55 | 56 | self.play(ReplacementTransform(line, circle_one)) 57 | self.wait() 58 | 59 | # rolls along the outer edge of circle one 60 | self.play(wheel.roll(TAU, about=circle_one, run_time=2.5)) 61 | self.wait() 62 | 63 | self.play(ReplacementTransform(circle_one, circle_two)) 64 | self.wait() 65 | 66 | # rolls along the inner edge of circle two 67 | self.play(wheel.roll(TAU, about=circle_two, run_time=3)) 68 | self.wait(2) 69 | path.clear_updaters() 70 | self.remove(path) 71 | self.play(wheel.undraw_markers()) 72 | 73 | self.play(Uncreate(circle_two), wheel.animate.move(ORIGIN)) 74 | self.wait() 75 | 76 | self.play(ShrinkToCenter(wheel)) 77 | self.wait(0.5) 78 | 79 | 80 | def trochoid_scene(scene, title, markers): 81 | length = config.frame_width * 0.75 82 | radius = length / (2 * TAU) 83 | 84 | num_line = NumberLine( 85 | x_range=[-TAU, TAU, PI], length=length, color=ManimColor("#6F828A") 86 | ).shift(DOWN * radius) 87 | 88 | point = num_line.n2p(-TAU) + radius * UP 89 | 90 | wheel = Wheel(radius=radius, color=ManimColor("#04D9FF"), point=point) 91 | scene.play(GrowFromCenter(wheel), Create(num_line, lag_ratio=0)) 92 | scene.wait() 93 | 94 | scene.play(wheel.draw_markers(markers), Write(title)) 95 | scene.wait() 96 | 97 | path = wheel.trace_paths() 98 | scene.add(path) 99 | 100 | scene.play(wheel.roll(length * RIGHT, run_time=4)) 101 | scene.wait() 102 | 103 | path.clear_updaters() 104 | scene.play(path.animate.fade(2 / 3)) 105 | 106 | new_path = wheel.trace_paths(dissipating_time=2) 107 | scene.add(new_path) 108 | 109 | scene.play(wheel.roll(length * LEFT, run_time=4)) 110 | scene.wait() 111 | 112 | scene.play(Unwrite(title), FadeOut(path)) 113 | 114 | new_path.clear_updaters() 115 | scene.wait() 116 | 117 | scene.remove(new_path) 118 | scene.play(wheel.undraw_markers(), Unwrite(title)) 119 | scene.wait() 120 | 121 | scene.play(ShrinkToCenter(wheel), Uncreate(num_line, lag_ratio=0)) 122 | scene.wait(0.5) 123 | 124 | 125 | class Cycloid(Scene): 126 | def construct(self): 127 | title = Text("Cycloid").to_edge(DOWN, buff=1.25) 128 | marker = [(1, PI / 2, ManimColor("#6019E3"))] 129 | 130 | trochoid_scene(self, title, marker) 131 | 132 | 133 | class ProlateCycloid(Scene): 134 | def construct(self): 135 | title = Text("Prolate Cycloid").to_edge(DOWN, buff=1.25) 136 | marker = [(1.5, PI / 2, ManimColor("#E31937"))] 137 | 138 | trochoid_scene(self, title, marker) 139 | 140 | 141 | class CurtateCycloid(Scene): 142 | def construct(self): 143 | title = Text("Curtate Cycloid").to_edge(DOWN, buff=1.25) 144 | marker = [(0.5, PI / 2, ManimColor("#19E360"))] 145 | 146 | trochoid_scene(self, title, marker) 147 | 148 | 149 | class TwoMarkers(Scene): 150 | def construct(self): 151 | title = VMobject() 152 | marker = [ 153 | (1, PI / 2, ManimColor("#6019E3")), 154 | (0.5, -PI / 2, ManimColor("#E31937")), 155 | ] 156 | 157 | trochoid_scene(self, title, marker) 158 | 159 | 160 | class Trochoids(Scene): 161 | def construct(self): 162 | colors = [ManimColor(hex) for hex in ("#19E360", "#6019E3", "#E31937")] 163 | 164 | cycloids = [ 165 | ("Curtate Cycloid", (0.5, PI / 2, colors[0])), 166 | ("Cycloid", (1, PI / 2, colors[1])), 167 | ("Prolate Cycloid", (1.5, PI / 2, colors[2])), 168 | ] 169 | 170 | title = VGroup() 171 | markers = [] 172 | 173 | for i in cycloids: 174 | text = Text(i[0], font_size=40) 175 | dot = Dot(radius=0.125, color=i[1][2]).next_to(text[0], LEFT) 176 | title.add(VGroup(dot, text)) 177 | markers.append(i[1]) 178 | 179 | title.arrange(RIGHT, buff=0.5).to_edge(DOWN, buff=1.25) 180 | markers.reverse() 181 | 182 | trochoid_scene(self, title, markers) 183 | 184 | 185 | def epitrochoid_scene( 186 | scene, 187 | k, 188 | angle=-TAU, 189 | run_time=4, 190 | markers=[(1, PI / 2, ManimColor("#6019E3"))], 191 | scale=1, 192 | title=None, 193 | ): 194 | h = config.frame_height * scale * 3 / 8 195 | r = h / (2 + k) 196 | 197 | wheel = Wheel(radius=r, color=ManimColor("#04D9FF"), markers=markers) 198 | 199 | circle = Circle(radius=k * r, color=ManimColor("#6F828A")) 200 | 201 | txt = f"k = {k}" if title is None else f"{title}\nk = {k}" 202 | text = Text(txt).to_corner(UL, buff=2 / 3) 203 | 204 | wheel.move(circle.get_top(), UP) 205 | 206 | scene.play(FadeIn(text, circle, wheel)) 207 | scene.wait(0.5) 208 | 209 | path = wheel.trace_paths() 210 | scene.add(path) 211 | 212 | scene.play(wheel.roll(angle, about=circle, run_time=run_time)) 213 | 214 | path.clear_updaters() 215 | scene.wait(2) 216 | 217 | scene.play(FadeOut(path)) 218 | scene.wait() 219 | 220 | scene.play(FadeOut(text, circle, wheel)) 221 | scene.wait(0.5) 222 | 223 | 224 | class EpicycloidOne(Scene): 225 | def construct(self): 226 | 227 | epitrochoid_scene(self, k=3, angle=-TAU, title="Epicycloid") 228 | 229 | 230 | class EpicycloidTwo(Scene): 231 | def construct(self): 232 | k, angle, run_time = (1.5, -TAU * 2, 8) 233 | 234 | epitrochoid_scene(self, k, angle, run_time, title="Epicycloid") 235 | 236 | 237 | class Epitrochoids(Scene): 238 | def construct(self): 239 | k, angle, run_time = (3, -TAU, 5) 240 | markers = [ 241 | (2, PI / 2, ManimColor("#6019E3")), 242 | (0.5, -PI / 2, ManimColor("#E31937")), 243 | ] 244 | epitrochoid_scene( 245 | self, k, angle, run_time, markers, scale=0.875, title="Epitrochoids" 246 | ) 247 | 248 | 249 | class TwoRollingCircles(Scene): 250 | def construct(self): 251 | k, angle, run_time = (2.25, -4 * TAU, 16) 252 | h = config.frame_height * 3 / 8 253 | r = h / (2 + k) 254 | 255 | color_one = ManimColor("#6019E3") 256 | color_two = ManimColor("#E31937") 257 | 258 | wheel_one_markers = [(1, PI / 2, color_one)] 259 | wheel_two_markers = [(1, -PI / 2, color_two)] 260 | 261 | wheel_one = Wheel( 262 | radius=r, color=ManimColor("#04D9FF"), markers=wheel_one_markers 263 | ) 264 | wheel_two = Wheel( 265 | radius=r, color=ManimColor("#04D9FF"), markers=wheel_two_markers 266 | ) 267 | circle = Circle(radius=k * r, color=ManimColor("#6F828A")) 268 | 269 | wheel_one.move(circle.get_top(), UP) 270 | wheel_two.move(circle.get_top(), DOWN) 271 | 272 | self.play(FadeIn(circle, wheel_one, wheel_two)) 273 | self.wait(0.5) 274 | 275 | wheel_one_path = wheel_one.trace_paths() 276 | wheel_two_path = wheel_two.trace_paths() 277 | self.add(wheel_one_path, wheel_two_path) 278 | 279 | self.play( 280 | wheel_one.roll(angle, about=circle, run_time=run_time), 281 | wheel_two.roll(angle, about=circle, run_time=run_time), 282 | ) 283 | wheel_one_path.clear_updaters() 284 | wheel_two_path.clear_updaters() 285 | self.wait(2) 286 | 287 | self.play(FadeOut(wheel_one_path, wheel_two_path)) 288 | self.wait() 289 | 290 | self.play(FadeOut(circle, wheel_one, wheel_two)) 291 | self.wait(0.5) 292 | 293 | 294 | def hypotrochoid_scene( 295 | scene, 296 | k, 297 | angle=-TAU, 298 | run_time=4, 299 | markers=[(1, PI / 2, ManimColor("#6019E3"))], 300 | scale=1, 301 | title=None, 302 | ): 303 | h = config.frame_height * scale * 3 / 8 304 | r = h / k 305 | 306 | wheel = Wheel(radius=r, color=ManimColor("#04D9FF"), markers=markers) 307 | 308 | circle = Circle(radius=k * r, color=ManimColor("#6F828A")) 309 | 310 | txt = f"k = {k}" if title is None else f"{title}\nk = {k}" 311 | text = Text(txt).to_corner(UL, buff=2 / 3) 312 | 313 | wheel.move(circle.get_top(), DOWN) 314 | 315 | scene.play(FadeIn(text, circle, wheel)) 316 | scene.wait(0.5) 317 | 318 | path = wheel.trace_paths() 319 | scene.add(path) 320 | 321 | scene.play(wheel.roll(angle, about=circle, run_time=run_time)) 322 | 323 | path.clear_updaters() 324 | scene.wait(2) 325 | 326 | scene.play(FadeOut(path)) 327 | scene.wait() 328 | 329 | scene.play(FadeOut(text, circle, wheel)) 330 | scene.wait(0.5) 331 | 332 | 333 | class Hypocycloid(Scene): 334 | def construct(self): 335 | hypotrochoid_scene(self, k=3, angle=-TAU, title="Hypocycloid") 336 | 337 | 338 | class Hypotrochoids(Scene): 339 | def construct(self): 340 | k, angle, run_time = (3, -TAU, 5) 341 | markers = [ 342 | (2, PI / 2, ManimColor("#6019E3")), 343 | (0.5, -PI / 2, ManimColor("#E31937")), 344 | ] 345 | 346 | hypotrochoid_scene(self, k, angle, run_time, markers, scale=0.875) 347 | 348 | 349 | class StraightLines(Scene): 350 | def construct(self): 351 | k, angle, run_time = (2, -2 * TAU, 8) 352 | markers = [ 353 | (1, 0, ManimColor("#6019E3")), 354 | (1, PI / 2, ManimColor("#8C19AA")), 355 | (1, PI, ManimColor("#E31937")), 356 | (1, -PI / 2, ManimColor("#B71970")), 357 | ] 358 | 359 | hypotrochoid_scene(self, k, angle, run_time, markers) 360 | 361 | 362 | class Ellipses(Scene): 363 | def construct(self): 364 | k, angle, run_time = (2, -2 * TAU, 8) 365 | markers = [ 366 | (2, 0, ManimColor("#6019E3")), 367 | (0.5, PI, ManimColor("#E31937")), 368 | ] 369 | 370 | hypotrochoid_scene(self, k, angle, run_time, markers, scale=0.9) 371 | -------------------------------------------------------------------------------- /source/scenes/koch_curve.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "provenance": [], 7 | "gpuType": "V28", 8 | "include_colab_link": true 9 | }, 10 | "kernelspec": { 11 | "name": "python3", 12 | "display_name": "Python 3" 13 | }, 14 | "language_info": { 15 | "name": "python" 16 | }, 17 | "accelerator": "TPU" 18 | }, 19 | "cells": [ 20 | { 21 | "cell_type": "markdown", 22 | "metadata": { 23 | "id": "view-in-github", 24 | "colab_type": "text" 25 | }, 26 | "source": [ 27 | "\"Open" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "source": [ 33 | "# Koch Curve\n", 34 | "\n", 35 | "\n", 36 | "\n", 37 | "In mathematics, the intriguing concept of self-similarity emerges, wherein an object bears resemblance, either entirety or partially, to a smaller iteration of itself. A remarkable illustration of this phenomenon is the Koch Curve, showcasing the beauty of complexity inherent in self-similar geometric patterns. A fractal formed from the Koch Curve is the Koch Snowflake. It is created by repeatedly dividing each side of an equilateral triangle into three segments and replacing the middle segment with a smaller equilateral triangle. This process leads to a shape with an infinite perimeter enclosing a finite area. ✨ [mscene.curiouswalk.com/scenes/koch-curve](https://mscene.curiouswalk.com/scenes/koch-curve)" 38 | ], 39 | "metadata": { 40 | "id": "Vf3VcAXsKO6b" 41 | } 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "source": [ 46 | "## Setup" 47 | ], 48 | "metadata": { 49 | "id": "GqUiZVjOMz_r" 50 | } 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "source": [ 55 | "Installation" 56 | ], 57 | "metadata": { 58 | "id": "c34lKbph84az" 59 | } 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": { 65 | "id": "adwd99STI--b" 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "%pip install -q mscene\n", 70 | "import mscene\n", 71 | "%mscene -l manim plugins" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "source": [ 77 | "Imports" 78 | ], 79 | "metadata": { 80 | "id": "IBrxn3hZ85MR" 81 | } 82 | }, 83 | { 84 | "cell_type": "code", 85 | "source": [ 86 | "from mscene.manim import *\n", 87 | "from mscene.fractal import *" 88 | ], 89 | "metadata": { 90 | "id": "EkbTlUS-f2Gm" 91 | }, 92 | "execution_count": null, 93 | "outputs": [] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "source": [ 98 | "## Scenes" 99 | ], 100 | "metadata": { 101 | "id": "iiS7LDtw0Rrq" 102 | } 103 | }, 104 | { 105 | "cell_type": "code", 106 | "source": [ 107 | "%%manim -sqh -o image FractalImage\n", 108 | "# @title Select the shape, enter a level (0–4), and run this cell to generate the fractal image.\n", 109 | "\n", 110 | "class FractalImage(Scene):\n", 111 | " def construct(self):\n", 112 | "\n", 113 | " shape = \"Koch Curve\" # @param [\"Koch Curve\",\"Koch Snowflake\",\"Koch Antisnowflake\"]\n", 114 | " level = 3 # @param {\"type\":\"integer\"}\n", 115 | " title = True # @param {\"type\":\"boolean\"}\n", 116 | "\n", 117 | " n = 0 if level < 0 else 4 if level > 4 else level\n", 118 | "\n", 119 | " if shape == \"Koch Snowflake\":\n", 120 | " color = [ManimColor(hex) for hex in (\"#0ADBEF\", \"#0A68EF\", \"#1F0AEF\")]\n", 121 | " vmob = KochSnowflake(n, fill_color=color)\n", 122 | " elif shape == \"Koch Antisnowflake\":\n", 123 | " color = [ManimColor(hex) for hex in (\"#1F0AEF\", \"#0A68EF\", \"#0ADBEF\")]\n", 124 | " vmob = KochSnowflake(n, invert=True, fill_color=color)\n", 125 | " else:\n", 126 | " color = [ManimColor(hex) for hex in (\"#0A68EF\", \"#0ADBEF\", \"#0A68EF\")]\n", 127 | " vmob = KochCurve(n, stroke_width=8 - n, stroke_color=color)\n", 128 | "\n", 129 | " if title:\n", 130 | " text = Text(f\"{shape}\\nLevel {n}\", font_size=36).to_corner(UL, buff=0.75)\n", 131 | " self.add(text, vmob)\n", 132 | " else:\n", 133 | " self.add(vmob)\n" 134 | ], 135 | "metadata": { 136 | "cellView": "form", 137 | "id": "Uq6bXxYjQdJO" 138 | }, 139 | "execution_count": null, 140 | "outputs": [] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "source": [ 145 | "### Example Scenes" 146 | ], 147 | "metadata": { 148 | "id": "CMbpFpdIT9vG" 149 | } 150 | }, 151 | { 152 | "cell_type": "code", 153 | "source": [ 154 | "%%manim -qm ExampleOne\n", 155 | "\n", 156 | "class ExampleOne(Scene):\n", 157 | " def construct(self):\n", 158 | " kc = KochCurve(2)\n", 159 | " self.add(kc)\n", 160 | "\n", 161 | " self.play(kc.animate.next_level())\n", 162 | " self.wait()\n", 163 | "\n", 164 | " self.play(kc.animate.prev_level())\n", 165 | " self.wait()" 166 | ], 167 | "metadata": { 168 | "id": "YPWP-Id8UAlZ" 169 | }, 170 | "execution_count": null, 171 | "outputs": [] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "source": [ 176 | "%%manim -qm ExampleTwo\n", 177 | "\n", 178 | "class ExampleTwo(Scene):\n", 179 | " def construct(self):\n", 180 | " kc1 = KochCurve(level=1, stroke_width=6)\n", 181 | " kc2 = KochCurve(level=2, stroke_width=6, group=False)\n", 182 | " kc3 = KochCurve(level=3, stroke_width=6, group=False)\n", 183 | " colors = color_gradient((PURE_RED, PURE_BLUE), 4)\n", 184 | "\n", 185 | " for i, j, c in zip(kc2, kc3, colors):\n", 186 | " i.set_color(c)\n", 187 | " j.set_color(c)\n", 188 | "\n", 189 | " self.add(kc1)\n", 190 | " self.play(kc1.animate.next_level())\n", 191 | " self.wait()\n", 192 | "\n", 193 | " self.play(FadeTransform(kc1, kc2))\n", 194 | " self.wait()\n", 195 | "\n", 196 | " self.play(Transform(kc2, kc3, rate_func=there_and_back_with_pause, run_time=3))\n", 197 | " self.wait()\n", 198 | "\n", 199 | " self.play(FadeTransform(kc2, kc1))\n", 200 | " self.wait()\n", 201 | "\n", 202 | " self.play(kc1.animate.prev_level())\n", 203 | " self.wait()" 204 | ], 205 | "metadata": { 206 | "id": "CJMnjVtI9sl0" 207 | }, 208 | "execution_count": null, 209 | "outputs": [] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "source": [ 214 | "### Koch Curve" 215 | ], 216 | "metadata": { 217 | "id": "2cM03c0EVqmV" 218 | } 219 | }, 220 | { 221 | "cell_type": "code", 222 | "source": [ 223 | "%%manim -qm KochCurveScene\n", 224 | "\n", 225 | "class KochCurveScene(Scene):\n", 226 | " def construct(self):\n", 227 | " color = [ManimColor(hex) for hex in (\"#0A68EF\", \"#0ADBEF\", \"#0A68EF\")]\n", 228 | " kc = KochCurve(level=1, stroke_width=8, stroke_color=color)\n", 229 | " title = Text(\"Koch Curve\\nLevel 1\").to_corner(UL, buff=0.75)\n", 230 | "\n", 231 | " self.add(title, kc)\n", 232 | " self.wait()\n", 233 | "\n", 234 | " for level in (2, 3, 2, 1):\n", 235 | " self.play(\n", 236 | " kc.animate.new_level(level, stroke_width=8 - level),\n", 237 | " Transform(title[-1], Text(str(level)).move_to(title[-1])),\n", 238 | " )\n", 239 | " self.wait()" 240 | ], 241 | "metadata": { 242 | "id": "EVcPR-qeVrCJ" 243 | }, 244 | "execution_count": null, 245 | "outputs": [] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "source": [ 250 | "### Koch Snowflake" 251 | ], 252 | "metadata": { 253 | "id": "gRBEacNqWLxd" 254 | } 255 | }, 256 | { 257 | "cell_type": "code", 258 | "source": [ 259 | "%%manim -qm Snowflake\n", 260 | "\n", 261 | "class Snowflake(Scene):\n", 262 | " def construct(self):\n", 263 | " color = [ManimColor(hex) for hex in (\"#0ADBEF\", \"#0A68EF\", \"#1F0AEF\")]\n", 264 | " ks = KochSnowflake(level=1, fill_color=color)\n", 265 | " title = Text(\"Koch Snowflake\\nLevel 1\").to_corner(UL, buff=0.75)\n", 266 | "\n", 267 | " self.add(title, ks)\n", 268 | " self.wait()\n", 269 | "\n", 270 | " for level in (2, 3, 2, 1):\n", 271 | " self.play(\n", 272 | " ks.animate.new_level(level),\n", 273 | " Transform(title[-1], Text(str(level)).move_to(title[-1])),\n", 274 | " )\n", 275 | " self.wait()" 276 | ], 277 | "metadata": { 278 | "id": "JiGhvUHdWMDB" 279 | }, 280 | "execution_count": null, 281 | "outputs": [] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "source": [ 286 | "### Koch Antisnowflake" 287 | ], 288 | "metadata": { 289 | "id": "MhPlZF5AXb71" 290 | } 291 | }, 292 | { 293 | "cell_type": "code", 294 | "source": [ 295 | "%%manim -qm Antisnowflake\n", 296 | "\n", 297 | "class Antisnowflake(Scene):\n", 298 | " def construct(self):\n", 299 | " color = [ManimColor(hex) for hex in (\"#1F0AEF\", \"#0A68EF\", \"#0ADBEF\")]\n", 300 | " ks = KochSnowflake(level=1, invert=True, fill_color=color)\n", 301 | " title = Text(\"Koch Anti-\\nsnowflake\\nLevel 1\").to_corner(UL, buff=0.75)\n", 302 | "\n", 303 | " self.add(title, ks)\n", 304 | " self.wait()\n", 305 | "\n", 306 | " for level in (2, 3, 2, 1):\n", 307 | " self.play(\n", 308 | " ks.animate.new_level(level),\n", 309 | " Transform(title[-1], Text(str(level)).move_to(title[-1])),\n", 310 | " )\n", 311 | " self.wait()" 312 | ], 313 | "metadata": { 314 | "id": "NN3zOXn4XcRn" 315 | }, 316 | "execution_count": null, 317 | "outputs": [] 318 | }, 319 | { 320 | "cell_type": "markdown", 321 | "source": [ 322 | "### Dual Flakes" 323 | ], 324 | "metadata": { 325 | "id": "OrxAt_e3ehjp" 326 | } 327 | }, 328 | { 329 | "cell_type": "code", 330 | "source": [ 331 | "%%manim -qm DualFlakes\n", 332 | "\n", 333 | "class DualFlakes(Scene):\n", 334 | " def construct(self):\n", 335 | " snowflake = KochSnowflake(level=1, fill_color=ManimColor(\"#5d06e9\"))\n", 336 | " anti_snowflake = KochSnowflake(\n", 337 | " level=1, invert=True, fill_color=ManimColor(\"#9e0168\")\n", 338 | " ).align_to(snowflake, UP)\n", 339 | "\n", 340 | " self.add(snowflake, anti_snowflake)\n", 341 | " self.wait()\n", 342 | "\n", 343 | " for level in (2, 3, 2, 1):\n", 344 | " self.play(\n", 345 | " snowflake.animate.new_level(level),\n", 346 | " anti_snowflake.animate.new_level(level),\n", 347 | " run_time=1.5,\n", 348 | " )\n", 349 | " self.wait(1.5)" 350 | ], 351 | "metadata": { 352 | "id": "yPtMgub4eh-7" 353 | }, 354 | "execution_count": null, 355 | "outputs": [] 356 | }, 357 | { 358 | "cell_type": "markdown", 359 | "source": [ 360 | "## Download Script\n", 361 | "\n", 362 | "Run the cell below to download the scene script." 363 | ], 364 | "metadata": { 365 | "id": "X4N5yMv49AsN" 366 | } 367 | }, 368 | { 369 | "cell_type": "code", 370 | "source": [ 371 | "import mscene\n", 372 | "%mscene koch_curve.py" 373 | ], 374 | "metadata": { 375 | "id": "OBupcua-9PrD" 376 | }, 377 | "execution_count": null, 378 | "outputs": [] 379 | }, 380 | { 381 | "cell_type": "markdown", 382 | "source": [ 383 | "## End Session\n", 384 | "\n", 385 | "Run the cell below to disconnect the runtime and terminate the session." 386 | ], 387 | "metadata": { 388 | "id": "4Vzp6XMkeoci" 389 | } 390 | }, 391 | { 392 | "cell_type": "code", 393 | "source": [ 394 | "from google.colab import runtime\n", 395 | "runtime.unassign()" 396 | ], 397 | "metadata": { 398 | "id": "aC2_yLZEepGW" 399 | }, 400 | "execution_count": null, 401 | "outputs": [] 402 | } 403 | ] 404 | } -------------------------------------------------------------------------------- /source/scenes/fibonacci_spiral.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "nbformat": 4, 3 | "nbformat_minor": 0, 4 | "metadata": { 5 | "colab": { 6 | "provenance": [], 7 | "gpuType": "V28", 8 | "include_colab_link": true 9 | }, 10 | "kernelspec": { 11 | "name": "python3", 12 | "display_name": "Python 3" 13 | }, 14 | "language_info": { 15 | "name": "python" 16 | }, 17 | "accelerator": "TPU" 18 | }, 19 | "cells": [ 20 | { 21 | "cell_type": "markdown", 22 | "metadata": { 23 | "id": "view-in-github", 24 | "colab_type": "text" 25 | }, 26 | "source": [ 27 | "\"Open" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "source": [ 33 | "# Fibonacci Spiral\n", 34 | "\n", 35 | "\n", 36 | "\n", 37 | "There is a fascinating relation between the Fibonacci sequence and natural patterns. A remarkable example is the Fibonacci spiral, which is constructed by adding arcs of quarter circles with radii corresponding to Fibonacci numbers, making a beautiful mathematical pattern often observed throughout nature. ✨ [mscene.curiouswalk.com/scenes/fibonacci-spiral](https://mscene.curiouswalk.com/scenes/fibonacci-spiral)" 38 | ], 39 | "metadata": { 40 | "id": "dvWWLMVHCb-T" 41 | } 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "source": [ 46 | "## Setup" 47 | ], 48 | "metadata": { 49 | "id": "vfZ93w_MEEY3" 50 | } 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "source": [ 55 | "Installation" 56 | ], 57 | "metadata": { 58 | "id": "Qe4RtUGrEGtk" 59 | } 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": { 65 | "id": "JixaeT6PCD-u" 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "%pip install -q mscene\n", 70 | "import mscene\n", 71 | "%mscene -l manim" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "source": [ 77 | "Import" 78 | ], 79 | "metadata": { 80 | "id": "iDEC75jgEJCh" 81 | } 82 | }, 83 | { 84 | "cell_type": "code", 85 | "source": [ 86 | "from mscene.manim import *" 87 | ], 88 | "metadata": { 89 | "id": "gAhb9ICXCLiv" 90 | }, 91 | "execution_count": null, 92 | "outputs": [] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "source": [ 97 | "## Objects\n", 98 | "\n", 99 | "Functions to create objects for animation scenes." 100 | ], 101 | "metadata": { 102 | "id": "qwNARuh9IKwi" 103 | } 104 | }, 105 | { 106 | "cell_type": "code", 107 | "source": [ 108 | "def fseq(n, a=0, b=1):\n", 109 | " \"\"\"Return the first n Fibonacci numbers starting with a and b.\"\"\"\n", 110 | " seq = []\n", 111 | " for _ in range(n):\n", 112 | " seq.append(a)\n", 113 | " a, b = b, a + b\n", 114 | " return seq\n", 115 | "\n", 116 | "\n", 117 | "def fsmob(n, width=None, height=None):\n", 118 | " \"\"\"VGroup of Mobjects for the Fibonacci spiral.\n", 119 | "\n", 120 | " Args:\n", 121 | " n (int): Number of first Fibonacci terms.\n", 122 | " width (float | None): Width of the object (default: None).\n", 123 | " height (float | None): Height of the object (default: None).\n", 124 | "\n", 125 | " Returns:\n", 126 | " VGroup of Square, Text, ArcBetweenPoints and Dot.\n", 127 | " \"\"\"\n", 128 | " sqr_color = ManimColor(\"#214761\")\n", 129 | " txt_color = ManimColor(\"#516572\")\n", 130 | " arc_color = ManimColor(\"#04d9ff\")\n", 131 | " dot_color = ManimColor(\"#cfff04\")\n", 132 | "\n", 133 | " seq = fseq(n, 1)\n", 134 | "\n", 135 | " if width and not height:\n", 136 | " scale = width / sum(seq[-2:])\n", 137 | " elif height and not width:\n", 138 | " scale = height / seq[-1]\n", 139 | " else:\n", 140 | " scale = 1\n", 141 | "\n", 142 | " mobjects = VGroup()\n", 143 | " squares = VGroup()\n", 144 | "\n", 145 | " if len(seq) % 2:\n", 146 | " angle = PI / 2\n", 147 | " direction = (RIGHT, UP, LEFT, DOWN)\n", 148 | " dot_index = (0, -1, -1, 0)\n", 149 | " else:\n", 150 | " angle = -PI / 2\n", 151 | " direction = (UP, RIGHT, DOWN, LEFT)\n", 152 | " dot_index = (0, 0, -1, -1)\n", 153 | "\n", 154 | " corner = (DL, UL)\n", 155 | "\n", 156 | " for i, t in enumerate(seq):\n", 157 | " square = Square(t * scale, stroke_width=6, color=sqr_color).next_to(\n", 158 | " squares,\n", 159 | " direction[i % 4],\n", 160 | " buff=0,\n", 161 | " )\n", 162 | "\n", 163 | " dots = VGroup(\n", 164 | " Dot(square.get_corner(corner[i % 2]), color=dot_color),\n", 165 | " Dot(square.get_corner(-corner[i % 2]), color=dot_color),\n", 166 | " )\n", 167 | "\n", 168 | " arc = ArcBetweenPoints(\n", 169 | " dots[dot_index[i % 4]].get_center(),\n", 170 | " dots[dot_index[i % 4] + 1].get_center(),\n", 171 | " angle=angle,\n", 172 | " color=arc_color,\n", 173 | " stroke_width=6,\n", 174 | " )\n", 175 | "\n", 176 | " text = (\n", 177 | " Text(f\"{t}×{t}\", color=txt_color)\n", 178 | " .scale_to_fit_width(square.width * 0.5)\n", 179 | " .move_to(square)\n", 180 | " )\n", 181 | "\n", 182 | " vgrp = VGroup(square, text, arc, dots)\n", 183 | " vgrp[2:].set_z_index(1)\n", 184 | " squares.add(square)\n", 185 | " mobjects.add(vgrp)\n", 186 | "\n", 187 | " mobjects.center()\n", 188 | "\n", 189 | " return mobjects\n", 190 | "\n", 191 | "\n", 192 | "def fsmob_anim(mob, mode=\"IN\", lag_ratio=0.125, **kwargs):\n", 193 | " \"\"\"Return an AnimationGroup for the Fibonacci spiral.\"\"\"\n", 194 | "\n", 195 | " if mode == \"IN\":\n", 196 | "\n", 197 | " anim = [(FadeIn(i[0]), Write(i[1]), Create(i[2]), FadeIn(i[3])) for i in mob]\n", 198 | " elif mode == \"OUT\":\n", 199 | " anim = [\n", 200 | " (FadeOut(i[0]), Unwrite(i[1]), Uncreate(i[2]), FadeOut(i[3]))\n", 201 | " for i in mob[::-1]\n", 202 | " ]\n", 203 | " else:\n", 204 | " raise ValueError(\"mode must be 'IN' or 'OUT'\")\n", 205 | "\n", 206 | " return AnimationGroup(*anim, lag_ratio=lag_ratio, **kwargs)\n" 207 | ], 208 | "metadata": { 209 | "id": "CsASQ3YdILDm" 210 | }, 211 | "execution_count": null, 212 | "outputs": [] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "source": [ 217 | "n = 25\n", 218 | "print(f\"First {n} terms of the Fibonacci sequence:\", end=\" \")\n", 219 | "print(*fseq(n), sep=\", \")" 220 | ], 221 | "metadata": { 222 | "id": "FsFXGC5RZERZ" 223 | }, 224 | "execution_count": null, 225 | "outputs": [] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "source": [ 230 | "## Scenes" 231 | ], 232 | "metadata": { 233 | "id": "994HuJVIajqz" 234 | } 235 | }, 236 | { 237 | "cell_type": "code", 238 | "source": [ 239 | "%%manim -sqh FibonacciSpiral\n", 240 | "# @title Fibonacci Spiral\n", 241 | "\n", 242 | "class FibonacciSpiral(Scene):\n", 243 | " def construct(self):\n", 244 | " n = 6 # @param {\"type\":\"number\",\"placeholder\":\"Enter the number of Fibonacci terms.\"}\n", 245 | " n = 1 if n < 1 else n\n", 246 | " sf = 1 / 2 if n < 3 else 3 / 4\n", 247 | " height = config.frame_height * sf\n", 248 | " mob = fsmob(n, height=height)\n", 249 | " print(f\"First {n} terms of the Fibonacci sequence:\", end=\" \")\n", 250 | " print(*fseq(n, 1), sep=\", \", end=\"\\n\\n\")\n", 251 | " self.add(mob)" 252 | ], 253 | "metadata": { 254 | "id": "bZda84v-2dkh", 255 | "cellView": "form" 256 | }, 257 | "execution_count": null, 258 | "outputs": [] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "source": [ 263 | "### Scene One" 264 | ], 265 | "metadata": { 266 | "id": "BzPYhzIYae40" 267 | } 268 | }, 269 | { 270 | "cell_type": "code", 271 | "source": [ 272 | "%%manim -qm SceneOne\n", 273 | "\n", 274 | "class SceneOne(Scene):\n", 275 | " def construct(self):\n", 276 | " seq = fseq(25)\n", 277 | " width = config.frame_width * 3 / 4\n", 278 | " seq_str = \", \".join(map(str, seq)) + \", ...\"\n", 279 | "\n", 280 | " title = Text(\"Fibonacci Sequence\").scale_to_fit_width(width * 3 / 4)\n", 281 | " text = MarkupText(seq_str, width=width, justify=True, font_size=130)\n", 282 | " VGroup(title, text).arrange(DOWN, buff=3 / 4)\n", 283 | "\n", 284 | " self.play(Write(title), Write(text, run_time=3))\n", 285 | " self.wait(3)" 286 | ], 287 | "metadata": { 288 | "id": "lpwyhULxJLTu" 289 | }, 290 | "execution_count": null, 291 | "outputs": [] 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "source": [ 296 | "### Scene Two" 297 | ], 298 | "metadata": { 299 | "id": "7i9Qjd660jWx" 300 | } 301 | }, 302 | { 303 | "cell_type": "code", 304 | "source": [ 305 | "%%manim -qm SceneTwo\n", 306 | "\n", 307 | "class SceneTwo(Scene):\n", 308 | " def construct(self):\n", 309 | " width = config.frame_width * 3 / 4\n", 310 | " mob = fsmob(6, width=width)\n", 311 | "\n", 312 | " self.play(fsmob_anim(mob))\n", 313 | " self.wait(2.5)\n", 314 | "\n", 315 | " self.play(fsmob_anim(mob, mode=\"OUT\"))\n", 316 | " self.wait(0.5)" 317 | ], 318 | "metadata": { 319 | "id": "4-UICKK0kcRM" 320 | }, 321 | "execution_count": null, 322 | "outputs": [] 323 | }, 324 | { 325 | "cell_type": "markdown", 326 | "source": [ 327 | "### Scene Three" 328 | ], 329 | "metadata": { 330 | "id": "ASnS01_W3CJ7" 331 | } 332 | }, 333 | { 334 | "cell_type": "code", 335 | "source": [ 336 | "%%manim -qm SceneThree\n", 337 | "\n", 338 | "class SceneThree(Scene):\n", 339 | " def construct(self):\n", 340 | " width = config.frame_width * 3 / 4\n", 341 | " terms = [4, 8, 12]\n", 342 | " term = None\n", 343 | " mob = None\n", 344 | "\n", 345 | " for n in terms:\n", 346 | " _mob = fsmob(n, width)\n", 347 | "\n", 348 | " if mob is None:\n", 349 | " self.play(fsmob_anim(_mob))\n", 350 | " else:\n", 351 | " i = term if term < n else -1\n", 352 | " self.play(ReplacementTransform(mob, _mob[:i]))\n", 353 | " self.wait(0.5)\n", 354 | " self.play(fsmob_anim(_mob[i:]))\n", 355 | "\n", 356 | " mob = _mob\n", 357 | " term = n\n", 358 | " self.wait(1.5)\n", 359 | "\n", 360 | " self.play(fsmob_anim(mob, mode=\"OUT\", lag_ratio=0))\n", 361 | " self.wait(0.5)" 362 | ], 363 | "metadata": { 364 | "id": "XiJ9FA52khQW" 365 | }, 366 | "execution_count": null, 367 | "outputs": [] 368 | }, 369 | { 370 | "cell_type": "markdown", 371 | "source": [ 372 | "### Scene Four" 373 | ], 374 | "metadata": { 375 | "id": "5AhcdR6t8ChM" 376 | } 377 | }, 378 | { 379 | "cell_type": "code", 380 | "source": [ 381 | "%%manim -qm SceneFour\n", 382 | "\n", 383 | "class SceneFour(Scene):\n", 384 | " def construct(self):\n", 385 | " n = 12\n", 386 | " width = config.frame_width * 3 / 4\n", 387 | " mob = fsmob(n, width)\n", 388 | " mob.save_state()\n", 389 | "\n", 390 | " width *= sum(fseq(n)[-2:]) * 3 / 4\n", 391 | " _mob = fsmob(n, width)\n", 392 | " _mob.shift(-_mob[0].get_center())\n", 393 | "\n", 394 | " self.add(mob)\n", 395 | " self.wait(0.5)\n", 396 | "\n", 397 | " self.play(Transform(mob, _mob, run_time=6))\n", 398 | " self.wait(2)\n", 399 | "\n", 400 | " self.play(Restore(mob, run_time=5))\n", 401 | " self.wait(0.5)" 402 | ], 403 | "metadata": { 404 | "id": "7JrAqW--3Ucl" 405 | }, 406 | "execution_count": null, 407 | "outputs": [] 408 | }, 409 | { 410 | "cell_type": "markdown", 411 | "source": [ 412 | "## Download Script\n", 413 | "\n", 414 | "Run the cell below to download the scene script." 415 | ], 416 | "metadata": { 417 | "id": "P542Tc9j1C_x" 418 | } 419 | }, 420 | { 421 | "cell_type": "code", 422 | "source": [ 423 | "import mscene\n", 424 | "%mscene fibonacci_spiral.py" 425 | ], 426 | "metadata": { 427 | "id": "Wq9zWnd54En3" 428 | }, 429 | "execution_count": null, 430 | "outputs": [] 431 | }, 432 | { 433 | "cell_type": "markdown", 434 | "source": [ 435 | "## End Session\n", 436 | "\n", 437 | "Run the cell below to disconnect the runtime and terminate the session." 438 | ], 439 | "metadata": { 440 | "id": "DUB1R9jiPvfm" 441 | } 442 | }, 443 | { 444 | "cell_type": "code", 445 | "source": [ 446 | "from google.colab import runtime\n", 447 | "runtime.unassign()" 448 | ], 449 | "metadata": { 450 | "id": "nRudbBSBOlQn" 451 | }, 452 | "execution_count": null, 453 | "outputs": [] 454 | } 455 | ] 456 | } --------------------------------------------------------------------------------