Skip to content

Instantly share code, notes, and snippets.

@Elteoremadebeethoven
Last active July 29, 2023 14:08
Show Gist options
  • Save Elteoremadebeethoven/716f10a394bc123497188eefcc618b78 to your computer and use it in GitHub Desktop.
Save Elteoremadebeethoven/716f10a394bc123497188eefcc618b78 to your computer and use it in GitHub Desktop.
Manim: Class Animations in depth
Display the source blob
Display the rendered blob
Raw
{
"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