Last active
July 29, 2023 14:08
-
-
Save Elteoremadebeethoven/716f10a394bc123497188eefcc618b78 to your computer and use it in GitHub Desktop.
Manim: Class Animations in depth
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"from manim import *\n", | |
"config.media_embed = True; config.media_width = \"100%\"\n", | |
"_RV = \"-v WARNING -qm --progress_bar None --disable_caching Example\"\n", | |
"_RI = \"-v WARNING -s --progress_bar None --disable_caching Example\"" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Submobjects" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" vmob = VMobject()\n", | |
" vmob.add(Square())\n", | |
" vmob.add(Circle())\n", | |
" # Same as: vmob = VGroup(Square(), Circle())\n", | |
"\n", | |
" vmob[0].set_color(ORANGE)\n", | |
" vmob.submobjects[1].set_color(YELLOW)\n", | |
"\n", | |
" vmob.arrange(DOWN)\n", | |
"\n", | |
" self.add(vmob)\n", | |
"\n", | |
"%manim $_RI" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# `Animation` Theory\n", | |
"\n", | |
"```python\n", | |
"class CustomAnimation(Animation):\n", | |
" def __init__(self, \n", | |
" mobject, # Mobject\n", | |
" run_time, # Animation duration\n", | |
" rate_func, # Function that is applied to the %run_time\n", | |
" remover, # remove Mobject after finish animation\n", | |
" lag_ratio, # Lag between submobjects\n", | |
" ):\n", | |
" super().__init__(mobject,\n", | |
" run_time=run_time,\n", | |
" rate_func=rate_func,\n", | |
" remover=remover,\n", | |
" lag_ratio=lag_ratio)\n", | |
" # Animation.mobject = mobject\n", | |
" # Animation.run_time = run_time\n", | |
" # Animation.rate_func = rate_func\n", | |
" # Animation.remover = remover\n", | |
" # Animation.lag_ratio = lag_ratio\n", | |
"\n", | |
"# ⚠️⚠️⚠️ The real codes are more complex, but here\n", | |
"# I am summarizing its behavior to make it\n", | |
"# easier to understand ⚠️⚠️⚠️\n", | |
"\n", | |
" def _setup_scene(self, scene: Scene) -> None:\n", | |
" \"\"\"\n", | |
" Add Animation.mobject to Scene, that is,\n", | |
" to Scene.mobjects array.\n", | |
" \"\"\"\n", | |
" scene.add(self.mobject)\n", | |
"\n", | |
" def begin(self) -> None:\n", | |
" \"\"\"\n", | |
" Creates Animation.starting_mobject, a copy of\n", | |
" the Animation.mobject before the animation\n", | |
" starts running.\n", | |
" Also executes Animation.interpolate(0)\n", | |
"\n", | |
" !!! NOTE !!!\n", | |
" In case new objects are added to the scene during\n", | |
" the animation, the objects must be added to\n", | |
" Animation.mobject, or they have to be added to the\n", | |
" scene with the Animation._setup_scene method.\n", | |
" \"\"\"\n", | |
" self.starting_mobject = self.mobject.copy()\n", | |
" self.interpolate(0)\n", | |
"\n", | |
" def interpolate(self, alpha) -> None:\n", | |
" \"\"\"\n", | |
" Calls Animation.interpolate_mobject(alpha),\n", | |
" this alpha = %run_time, so, this alpha have a\n", | |
" linear behavior always.\n", | |
"\n", | |
" With this method we can easily control the\n", | |
" progress of the animation.\n", | |
" \"\"\"\n", | |
" self.interpolate_mobject(alpha)\n", | |
"\n", | |
" def interpolate_mobject(self, alpha) -> None:\n", | |
" \"\"\"\n", | |
" This alpha value is the %run_tima, to get the true\n", | |
" alpha you have to use:\n", | |
"\n", | |
" >>> real_alpha = self.rate_func(alpha)\n", | |
"\n", | |
" Here we distinguish two cases:\n", | |
"\n", | |
" [CASE 1]\n", | |
"\n", | |
" If the animation is going to be executed for all\n", | |
" the objects (and with lag_ratio=0) here you can\n", | |
" indicate (overwrite) the behavior of the animation,\n", | |
" it is the equivalent of the \"updater\" function.\n", | |
"\n", | |
" Remember to use the value of \"real_alpha\" instead\n", | |
" of alpha.\n", | |
"\n", | |
" In case each submobject is needed to have a different\n", | |
" behavior, it will be necessary to execute:\n", | |
"\n", | |
" [CASE 2]\n", | |
"\n", | |
" >>> super().interpolate_mobject(alpha)\n", | |
"\n", | |
" An object called \"families\" is obtained, this\n", | |
" object is basically something like this:\n", | |
"\n", | |
" families = [\n", | |
" [submobject_1, starting_submobject_1],\n", | |
" [submobject_2, starting_submobject_2],\n", | |
" [submobject_3, starting_submobject_3],\n", | |
" ...\n", | |
" ]\n", | |
"\n", | |
" Each \"submobject_i\" is, as the name says, the \n", | |
" submobject inside Animation.mobject, while the \n", | |
" \"starting_submobject_i\" is a copy of each the\n", | |
" \"submobject_i\" before starting the animation.\n", | |
"\n", | |
" If the lag_ratio > 0 (default is 0) then it will execute:\n", | |
"\n", | |
" >>> Animation.interpolate_submobject(submob, start_submob, sub_alpha)\n", | |
"\n", | |
" Where the \"sub_alpha\" is calculated with the lag_ratio.\n", | |
" \"\"\"\n", | |
" families = list(self.get_all_families_zipped())\n", | |
" for i, mobs in enumerate(families):\n", | |
" sub_alpha = self.get_sub_alpha(alpha, i, len(families))\n", | |
" self.interpolate_submobject(*mobs, sub_alpha)\n", | |
"\n", | |
" def interpolate_submobject(self, submob, starting_submob, alpha) -> None:\n", | |
" \"\"\"\n", | |
" This is the method where the behavior of each submobject is indicated.\n", | |
" Obviously, if Animation.interpolate_mobject or Animation.interpolate is\n", | |
" overridden this method will **not** be called.\n", | |
" In this method, the alpha that is received has already been processed\n", | |
" by the rate_func and adjusted by the lag_ratio.\n", | |
" \"\"\"\n", | |
"\n", | |
" def clean_up_from_scene(self, scene: Scene) -> None:\n", | |
" \"\"\"\n", | |
" It is used to set Animation.mobject before finishing the\n", | |
" animation, in case \"remover\" is True, then Animation.mobject\n", | |
" will be removed from the scene.\n", | |
" \"\"\"\n", | |
" if self.remover:\n", | |
" scene.remove(self.mobject)\n", | |
"\n", | |
" def finish(self) -> None:\n", | |
" \"\"\"\n", | |
" This method gets called when the animation is over.\n", | |
"\n", | |
" !!! NOTE !!!\n", | |
" It is possible that \"clean_up_from_scene\" and \"finish\" \n", | |
" methods will be merged or swapped in later versions of Manim.\n", | |
" \"\"\"\n", | |
" self.interpolate(1)\n", | |
" if self.suspend_mobject_updating and self.mobject is not None:\n", | |
" self.mobject.resume_updating()\n", | |
"```" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Example 1: Move and change color" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class MoveAndChangeColor(Animation):\n", | |
" def __init__(self, mob, position, color, **kwargs):\n", | |
" self.position = position\n", | |
" self.target_color = color\n", | |
" self.starting_color = mob.get_color()\n", | |
" super().__init__(mob, **kwargs)\n", | |
"\n", | |
" # If we are not going to control each submobject,we can\n", | |
" # override the \"interpolate\" or \"interpolate_submobject\" \n", | |
" # method, either one works for us.\n", | |
" # By tradition, \"interpolate_mobject\" is used.\n", | |
" def interpolate_mobject(self, alpha): # this alpha = %run_time\n", | |
" real_alpha = self.rate_func(alpha)\n", | |
" color = interpolate_color(self.starting_color, self.target_color, real_alpha)\n", | |
" self.mobject.become(self.starting_mobject) # similar to Mobject.restore()\n", | |
" self.mobject.move_to(self.position * real_alpha)\n", | |
" self.mobject.set_color(color)\n", | |
"\n", | |
"\n", | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" t = Text(\"Hello world\").scale(2)\n", | |
" t.set_color(TEAL) # To use t.get_color()\n", | |
" self.add(t)\n", | |
" self.play(\n", | |
" MoveAndChangeColor(t, DOWN*3 + RIGHT*3.2, ORANGE),\n", | |
" run_time=3,\n", | |
" # rate_func=there_and_back\n", | |
" )\n", | |
" self.wait()\n", | |
"\n", | |
" starting_color = GREEN\n", | |
" target_color = YELLOW\n", | |
" position = DOWN*3 + RIGHT*3.2\n", | |
" t.save_state()\n", | |
" def move_and_change_color(mob, alpha): # This is the real_alpha\n", | |
" color = interpolate_color(starting_color, target_color, alpha)\n", | |
" mob.restore()\n", | |
" mob.move_to(position * alpha)\n", | |
" mob.set_color(color)\n", | |
" \n", | |
" self.play(\n", | |
" UpdateFromAlphaFunc(t, move_and_change_color),\n", | |
" run_time=3,\n", | |
" )\n", | |
" self.wait()\n", | |
"\n", | |
"%manim $_RV" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Example 2: `interpolate` method" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" t = Text(\"Hello world\").scale(2)\n", | |
" anim = Write(t)\n", | |
" anim.begin() # <- Creates the self.starting_mobject\n", | |
" anim.interpolate(0.6)\n", | |
"\n", | |
" self.add(t)\n", | |
" print(f\"'t' is equal to 'anim.mobject'? -> {t == anim.mobject}\")\n", | |
"\n", | |
"%manim $_RI" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Example 3: Merging animations " | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Problem" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" t = Text(\"Hello world\").scale(2)\n", | |
" rf = smooth\n", | |
" move = ApplyMethod(t.move_to, DOWN*3 + RIGHT*3.2, rate_func=rf)\n", | |
" change_color = FadeToColor(t, ORANGE, rate_func=rf)\n", | |
"\n", | |
" move.begin()\n", | |
" change_color.begin()\n", | |
"\n", | |
" def updater(_, alpha):\n", | |
" # Here the order matters\n", | |
" change_color.interpolate(alpha)\n", | |
" move.interpolate(alpha)\n", | |
"\n", | |
" self.play(\n", | |
" UpdateFromAlphaFunc(t, updater),\n", | |
" run_time=3,\n", | |
" # It is important that this rate_func is linear,\n", | |
" # since within the interpolate method the %run_time\n", | |
" # has to go, if it is not linear, both rate_functions\n", | |
" # will be composed.\n", | |
" rate_func=linear\n", | |
" )\n", | |
" self.wait()\n", | |
"\n", | |
"%manim $_RV" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Problem reason:\n", | |
"\n", | |
"When we execute the `Animation.begin` method, a copy of the `mobject` is created, called `starting_mobject`, almost in all animations, when we execute `Animation.interpolate`, the command `self.mobject.become(self.starting_mobject)` is executed, the same way we did in example 1.\n", | |
"\n", | |
"## Solution:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" t = Text(\"Hello world\").scale(2)\n", | |
" rf = smooth\n", | |
"\n", | |
" move = ApplyMethod(t.move_to, DOWN*3 + RIGHT*3.2, rate_func=rf)\n", | |
" change_color = FadeToColor(t, ORANGE, rate_func=rf)\n", | |
" move.begin()\n", | |
"\n", | |
" def updater(_, alpha):\n", | |
" # Again, order matters\n", | |
" move.interpolate(alpha)\n", | |
" change_color.begin() # redefine starting_mobject\n", | |
" change_color.interpolate(alpha)\n", | |
"\n", | |
" self.play(\n", | |
" UpdateFromAlphaFunc(t, updater),\n", | |
" run_time=3,\n", | |
" rate_func=linear\n", | |
" )\n", | |
" self.wait()\n", | |
"\n", | |
"%manim $_RV" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Example 4: Adding extra objects to the scene\n", | |
"\n", | |
"### Version 1" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class RemarkWithRectangle(Animation):\n", | |
" DEFAULT_RECT_CONFIG = {\n", | |
" \"color\": RED,\n", | |
" \"stroke_width\": 4\n", | |
" }\n", | |
" def __init__(self, mob, buff=0.2, rate_func=there_and_back, **kwargs):\n", | |
" init_rect_kwargs = {}\n", | |
" anim_kwargs = kwargs.copy()\n", | |
" # rect_color=YELLOW, rect_stroke_width=10\n", | |
" for k in kwargs.keys():\n", | |
" if k.startswith(\"rect_\"):\n", | |
" init_rect_kwargs[k[len(\"rect_\"):]] = kwargs[k]\n", | |
" del anim_kwargs[k]\n", | |
" # Create rectangle\n", | |
" rect_kwargs = merge_dicts_recursively(self.DEFAULT_RECT_CONFIG, init_rect_kwargs)\n", | |
" self.rect = Rectangle(\n", | |
" width=mob.width+buff,\n", | |
" height=mob.height+buff,\n", | |
" **rect_kwargs\n", | |
" ).move_to(mob)\n", | |
" self.rect.save_state()\n", | |
" super().__init__(mob, rate_func=rate_func, **anim_kwargs)\n", | |
"\n", | |
" def begin(self):\n", | |
" super().begin()\n", | |
" # This needs to be after calling super, otherwise when\n", | |
" # \"starting_mobject\" is created it will also copy the\n", | |
" # new objects and give problems when using\n", | |
" # self.mobject.become(self.starting_mobject)\n", | |
" self.mobject.add(self.rect)\n", | |
"\n", | |
" def clean_up_from_scene(self, scene):\n", | |
" super().clean_up_from_scene(scene)\n", | |
" self.mobject.remove(self.rect)\n", | |
"\n", | |
" def interpolate_mobject(self, alpha):\n", | |
" real_alpha = self.rate_func(alpha)\n", | |
" self.rect.restore()\n", | |
" self.rect.become(\n", | |
" self.rect.get_subcurve(0, real_alpha)\n", | |
" # See https://docs.devtaoism.com/docs/html/contents/_11_manim_utils.html#get-subcurve\n", | |
" )\n", | |
"\n", | |
"\n", | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" t = Text(\"Hello world\").scale(2)\n", | |
"\n", | |
" self.play(\n", | |
" RemarkWithRectangle(t, rect_color=YELLOW, rect_stroke_width=10, buff=1),\n", | |
" run_time=3,\n", | |
" )\n", | |
" self.wait()\n", | |
"\n", | |
"%manim $_RV" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Version 2: With `_setup_scene`" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class RemarkWithRectangleV2(RemarkWithRectangle):\n", | |
" def begin(self):\n", | |
" Animation.begin(self)\n", | |
"\n", | |
" def _setup_scene(self, scene):\n", | |
" Animation._setup_scene(self, scene)\n", | |
" scene.add(self.rect)\n", | |
"\n", | |
" def clean_up_from_scene(self, scene):\n", | |
" Animation.clean_up_from_scene(self, scene)\n", | |
" scene.remove(self.rect)\n", | |
"\n", | |
"\n", | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" t = Text(\"Hello world\").scale(2)\n", | |
"\n", | |
" self.play(\n", | |
" RemarkWithRectangleV2(t, rect_color=PINK, rect_stroke_width=10, buff=1),\n", | |
" run_time=3,\n", | |
" )\n", | |
" self.wait()\n", | |
"\n", | |
"%manim $_RV" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Example 5: Replacing objects" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class ReplacingFading(Animation):\n", | |
" def __init__(self, base_mob, target_mob, **kwargs):\n", | |
" self.target_mob = target_mob\n", | |
" super().__init__(base_mob, **kwargs)\n", | |
" \n", | |
" def _setup_scene(self, scene):\n", | |
" super()._setup_scene(scene)\n", | |
" scene.add(self.target_mob)\n", | |
"\n", | |
" def clean_up_from_scene(self, scene):\n", | |
" super().clean_up_from_scene(scene)\n", | |
" scene.remove(self.mobject)\n", | |
"\n", | |
" def interpolate_mobject(self, alpha):\n", | |
" real_alpha = self.rate_func(alpha)\n", | |
" self.target_mob.set_opacity(real_alpha)\n", | |
" self.mobject.set_opacity(1-real_alpha)\n", | |
"\n", | |
"\n", | |
"class Example(Scene):\n", | |
" def construct(self):\n", | |
" base = Text(\"Hello\", color=RED).scale(3)\n", | |
" target = Text(\"World\", color=BLUE).scale(3)\n", | |
"\n", | |
" self.add(base)\n", | |
" print(self.mobjects)\n", | |
" self.play( ReplacingFading(base, target), run_time=2 )\n", | |
" print(self.mobjects)\n", | |
" self.play(target.animate.scale(3))\n", | |
" self.wait()\n", | |
"\n", | |
"%manim $_RV" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "venv", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.10.12" | |
}, | |
"orig_nbformat": 4 | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment