From 6212a607da4e6e8ede3d44e1bf9ba4a9e82b7e25 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Wed, 3 Jun 2026 21:14:04 +0200 Subject: [PATCH 01/11] Create first version of a helper method to derive an equation in sequential steps --- utils/__init__.py | 0 utils/latex_helpers.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 utils/__init__.py create mode 100644 utils/latex_helpers.py diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py new file mode 100644 index 0000000..f81343f --- /dev/null +++ b/utils/latex_helpers.py @@ -0,0 +1,38 @@ +from manim import MathTex, TransformMatchingTex, Succession + + +def derive_equation(base_equation: MathTex, steps: list[tuple[MathTex, float] | MathTex]) -> Succession: + """ + Generates a sequence of animations representing a step-by-step equation derivation. + + Args: + base_equation (MathTex): + The initial equation from which the derivation begins. + + steps (list[tuple[MathTex, float] | MathTex]): + A list of the successive steps of the mathematical derivation animation to be returned. + + Each step may be a tuple of the form (MathTex, float), where: + - The MathTex represents the resulting expression after the derivation step. + - The float represents the duration (in seconds) of the transition from the previous step to this one. + + Or, alternatively, a single MathTex, in which case the transition duration is 1. + """ + + if len(steps) == 0: + return Succession() + + animations = [] + + current = base_equation + for next_step in steps: + if isinstance(next_step, tuple): + next_step, duration = next_step + else: + duration = 1 + + animations.append(TransformMatchingTex(current, next_step, run_time=duration)) + + current = next_step + + return Succession(*animations) \ No newline at end of file From 524106c688c8d063714ea03c92cb396c19e64134 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Thu, 4 Jun 2026 16:07:02 +0200 Subject: [PATCH 02/11] Defined class `DerivationStep`, used by `derive_equation`, for clarity and extensibility I'm having a problem, though: If more than one transformation is performed, steps will appear on the scene right from the beginning of the Succession animation. I'm guessing the cause of this problem is that Manim is creating all the submobjects of my transformations in `derive_equation`, and for some reason adding them to the scene. (Just speculating.) --- utils/latex_helpers.py | 52 +++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index f81343f..65e3400 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -1,38 +1,44 @@ -from manim import MathTex, TransformMatchingTex, Succession +from manim import (MathTex, TransformMatchingTex, + Wait, Succession) -def derive_equation(base_equation: MathTex, steps: list[tuple[MathTex, float] | MathTex]) -> Succession: - """ - Generates a sequence of animations representing a step-by-step equation derivation. +START_G = r"\left." +END_G = r"\right." - Args: - base_equation (MathTex): - The initial equation from which the derivation begins. - steps (list[tuple[MathTex, float] | MathTex]): - A list of the successive steps of the mathematical derivation animation to be returned. +class DerivationStep: + def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: float = 0): + """ + Parameters: + *tex_strings (str): + A series of LaTeX strings that form the resulting MathTex expression. - Each step may be a tuple of the form (MathTex, float), where: - - The MathTex represents the resulting expression after the derivation step. - - The float represents the duration (in seconds) of the transition from the previous step to this one. + transition_duration (float, optional): + Duration of the transition from the previous step to this one. - Or, alternatively, a single MathTex, in which case the transition duration is 1. - """ + delay (float, optional): + Delay before performing the transition from the previous step to this one. + """ - if len(steps) == 0: + if tex_strings: self.tex = MathTex(*tex_strings) + self.transition_duration = transition_duration + self.delay = delay + +def derive_equation(base_equation: MathTex, steps: list[DerivationStep]) -> Succession: + if not steps: return Succession() animations = [] - current = base_equation + current_step = DerivationStep() + current_step.tex = base_equation for next_step in steps: - if isinstance(next_step, tuple): - next_step, duration = next_step - else: - duration = 1 - - animations.append(TransformMatchingTex(current, next_step, run_time=duration)) + animations.append(Wait(next_step.delay)) + animations.append(TransformMatchingTex( + current_step.tex, next_step.tex, + run_time=next_step.transition_duration + )) - current = next_step + current_step = next_step return Succession(*animations) \ No newline at end of file From a1df8c8e3d9eda1aef82fd9cde294b053ea0b257 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Thu, 4 Jun 2026 16:40:21 +0200 Subject: [PATCH 03/11] Turn `derive_equation` into a generator --- utils/latex_helpers.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index 65e3400..ebc43bc 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -1,5 +1,5 @@ -from manim import (MathTex, TransformMatchingTex, - Wait, Succession) +from manim import MathTex, TransformMatchingTex, Wait, Animation +from typing import Generator START_G = r"\left." @@ -20,25 +20,22 @@ def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: flo Delay before performing the transition from the previous step to this one. """ - if tex_strings: self.tex = MathTex(*tex_strings) + self.tex_strings = tex_strings self.transition_duration = transition_duration self.delay = delay -def derive_equation(base_equation: MathTex, steps: list[DerivationStep]) -> Succession: - if not steps: - return Succession() + def build_tex(self) -> MathTex: + return MathTex(*self.tex_strings) - animations = [] - current_step = DerivationStep() - current_step.tex = base_equation +def derive_equation(base_equation: MathTex, steps: list[DerivationStep]) -> Generator[Animation]: + current_tex = base_equation for next_step in steps: - animations.append(Wait(next_step.delay)) - animations.append(TransformMatchingTex( - current_step.tex, next_step.tex, + next_tex = next_step.build_tex() + yield Wait(next_step.delay) + yield TransformMatchingTex( + current_tex, next_tex, run_time=next_step.transition_duration - )) + ) - current_step = next_step - - return Succession(*animations) \ No newline at end of file + current_tex = next_tex \ No newline at end of file From eba4df5e93c66e64e56e1bbe42e240956438b75d Mon Sep 17 00:00:00 2001 From: Pituivan Date: Thu, 4 Jun 2026 17:46:14 +0200 Subject: [PATCH 04/11] Make `derive_equation` play the animations itself --- utils/latex_helpers.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index ebc43bc..7c94bd7 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -1,5 +1,4 @@ -from manim import MathTex, TransformMatchingTex, Wait, Animation -from typing import Generator +from manim import Scene, MathTex, TransformMatchingTex START_G = r"\left." @@ -20,22 +19,18 @@ def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: flo Delay before performing the transition from the previous step to this one. """ - self.tex_strings = tex_strings + self.tex = MathTex(*tex_strings) self.transition_duration = transition_duration self.delay = delay - def build_tex(self) -> MathTex: - return MathTex(*self.tex_strings) - -def derive_equation(base_equation: MathTex, steps: list[DerivationStep]) -> Generator[Animation]: +def derive_equation(scene: Scene, base_equation: MathTex, steps: list[DerivationStep]) -> None: current_tex = base_equation for next_step in steps: - next_tex = next_step.build_tex() - yield Wait(next_step.delay) - yield TransformMatchingTex( - current_tex, next_tex, + if next_step.delay: scene.wait(next_step.delay) + scene.play(TransformMatchingTex( + current_tex, next_step.tex, run_time=next_step.transition_duration - ) + )) - current_tex = next_tex \ No newline at end of file + current_tex = next_step.tex \ No newline at end of file From 9884cab5d4038149f191e485895e1b2da9288081 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Thu, 4 Jun 2026 18:48:53 +0200 Subject: [PATCH 05/11] Phase 1: Present quadratic equation, convert left hand to monic form --- animations/QuadraticFormulaDerivation.py | 50 ++++++++++++++++++++++++ utils/latex_helpers.py | 12 +++++- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 animations/QuadraticFormulaDerivation.py diff --git a/animations/QuadraticFormulaDerivation.py b/animations/QuadraticFormulaDerivation.py new file mode 100644 index 0000000..628eaee --- /dev/null +++ b/animations/QuadraticFormulaDerivation.py @@ -0,0 +1,50 @@ +from manim import * +from utils.latex_helpers import (derive_equation, DerivationStep, + START_G, END_G) + + +class QuadraticFormulaDerivation(Scene): + def construct(self): + # --- Title + + self.wait(1) + + self.play( + Write(Text( + "Derivation of the Quadratic Formula", + font_size=36 + ).to_edge(UP).shift(DOWN)), + run_time=1.25 + ) + + self.wait(1) + + # --- Phase 1: Present the quadratic equation, then convert the left hand to monic form + + equation = MathTex("a", "x^2", "+", "b", "x", "+", "c", "=", "0") + steps = [ + # We need to keep TeX groups separate so TransformMatchingTex animation doesn't turn into a fade. + # Hence, we can't use \frac for fractions since they require grouped {numerator}{denominator} in a single TeX group. + # We also can't use \over, since the = sign and the right side of the equation would fall into the denominator. + # A feasible option is delimiting numerator and denominator with \left. and \right. + + # Alternate between a and {a} when it's not supposed to move between terms. + + DerivationStep( + START_G, "a", "x^2", "+", "b", "x", "+", "c", r"\over a", END_G, + START_G, "=", END_G, + START_G, "0", r"\over a", END_G, + delay=1 + ), + DerivationStep( + "x^2", "+", START_G, "b", r"\over {a}", END_G, "x", "+", START_G, "c", r"\over {a}", END_G, + START_G, "=", END_G, + "0", + delay=.75, transition_duration=.75 + ) + ] + + self.play(Write(equation, run_time=1.5)) + equation = derive_equation(self, equation, steps) + + self.wait(1.5) \ No newline at end of file diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index 7c94bd7..786206d 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -24,7 +24,13 @@ def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: flo self.delay = delay -def derive_equation(scene: Scene, base_equation: MathTex, steps: list[DerivationStep]) -> None: +def derive_equation(scene: Scene, base_equation: MathTex, steps: list[DerivationStep]) -> MathTex: + """ + Returns: + MathTex: + The final equation after all derivation steps have been applied. + """ + current_tex = base_equation for next_step in steps: if next_step.delay: scene.wait(next_step.delay) @@ -33,4 +39,6 @@ def derive_equation(scene: Scene, base_equation: MathTex, steps: list[Derivation run_time=next_step.transition_duration )) - current_tex = next_step.tex \ No newline at end of file + current_tex = next_step.tex + + return current_tex \ No newline at end of file From fb4079611bf1ed08df9c9d57838a21a4a394c95d Mon Sep 17 00:00:00 2001 From: Pituivan Date: Thu, 4 Jun 2026 19:14:07 +0200 Subject: [PATCH 06/11] Remove `steps` variable; inline it in `derive_equation` usage --- animations/QuadraticFormulaDerivation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/animations/QuadraticFormulaDerivation.py b/animations/QuadraticFormulaDerivation.py index 628eaee..4a5324a 100644 --- a/animations/QuadraticFormulaDerivation.py +++ b/animations/QuadraticFormulaDerivation.py @@ -22,7 +22,9 @@ def construct(self): # --- Phase 1: Present the quadratic equation, then convert the left hand to monic form equation = MathTex("a", "x^2", "+", "b", "x", "+", "c", "=", "0") - steps = [ + + self.play(Write(equation, run_time=1.5)) + equation = derive_equation(self, equation, [ # We need to keep TeX groups separate so TransformMatchingTex animation doesn't turn into a fade. # Hence, we can't use \frac for fractions since they require grouped {numerator}{denominator} in a single TeX group. # We also can't use \over, since the = sign and the right side of the equation would fall into the denominator. @@ -42,9 +44,6 @@ def construct(self): "0", delay=.75, transition_duration=.75 ) - ] - - self.play(Write(equation, run_time=1.5)) - equation = derive_equation(self, equation, steps) + ]) self.wait(1.5) \ No newline at end of file From 6a254d32f43591d90319d749c924c82f5d7aaa07 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Fri, 5 Jun 2026 21:53:09 +0200 Subject: [PATCH 07/11] Add option to `DerivationStep` to use any kind of matching transform as transition --- utils/latex_helpers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index 786206d..d4a4ee3 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -1,4 +1,5 @@ from manim import Scene, MathTex, TransformMatchingTex +from manim.animation.transform_matching_parts import TransformMatchingAbstractBase START_G = r"\left." @@ -6,7 +7,8 @@ class DerivationStep: - def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: float = 0): + def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: float = 0, + transform_type: type[TransformMatchingAbstractBase] = TransformMatchingTex): """ Parameters: *tex_strings (str): @@ -22,7 +24,7 @@ def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: flo self.tex = MathTex(*tex_strings) self.transition_duration = transition_duration self.delay = delay - + self.transform_type = transform_type def derive_equation(scene: Scene, base_equation: MathTex, steps: list[DerivationStep]) -> MathTex: """ @@ -34,7 +36,7 @@ def derive_equation(scene: Scene, base_equation: MathTex, steps: list[Derivation current_tex = base_equation for next_step in steps: if next_step.delay: scene.wait(next_step.delay) - scene.play(TransformMatchingTex( + scene.play(next_step.transform_type( current_tex, next_step.tex, run_time=next_step.transition_duration )) From b4c48ef23cbd80a2d9d945cc4490683448c518b4 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Sat, 6 Jun 2026 03:47:27 +0200 Subject: [PATCH 08/11] Phase 2: Square completion analytical proof (binomial square identity) --- animations/QuadraticFormulaDerivation.py | 224 ++++++++++++++++++++++- utils/latex_helpers.py | 33 +++- 2 files changed, 249 insertions(+), 8 deletions(-) diff --git a/animations/QuadraticFormulaDerivation.py b/animations/QuadraticFormulaDerivation.py index 4a5324a..4bd0458 100644 --- a/animations/QuadraticFormulaDerivation.py +++ b/animations/QuadraticFormulaDerivation.py @@ -1,7 +1,11 @@ from manim import * +from manim.utils.rate_functions import ease_in_cubic + from utils.latex_helpers import (derive_equation, DerivationStep, START_G, END_G) +MOROCCAN_BLUE = ManimColor("#27ADF5") + class QuadraticFormulaDerivation(Scene): def construct(self): @@ -13,7 +17,7 @@ def construct(self): Write(Text( "Derivation of the Quadratic Formula", font_size=36 - ).to_edge(UP).shift(DOWN)), + ).to_edge(UP)), run_time=1.25 ) @@ -35,8 +39,7 @@ def construct(self): DerivationStep( START_G, "a", "x^2", "+", "b", "x", "+", "c", r"\over a", END_G, START_G, "=", END_G, - START_G, "0", r"\over a", END_G, - delay=1 + START_G, "0", r"\over a", END_G ), DerivationStep( "x^2", "+", START_G, "b", r"\over {a}", END_G, "x", "+", START_G, "c", r"\over {a}", END_G, @@ -46,4 +49,219 @@ def construct(self): ) ]) + self.wait(1) + + # --- Phase 2: Analytically prove square completion using the binomial square identity + + # -- Present the binomial square identity in the form (x+p)² = x² + 2px + p² + + self.play( + equation.animate.shift(2 * UP) + ) + + self.wait(.5) + + bsi_title = Text("Binomial Square Identity", font_size=36) + bsi_title.move_to(5 * DOWN) + + bsi_formula = MathTex("(x +", "p", ")^2 =", "x^2", "+", "2", "p", "x", "+", "p", "^2") + bsi_formula.add_updater(lambda m: m.next_to(bsi_title, DOWN, buff=0.5)) + + self.add(bsi_title, bsi_formula) + self.play( + bsi_title.animate.move_to(.5 * UP), + run_time=1.5 + ) + + self.wait(.75) + self.play(FadeOut(bsi_title)) + bsi_formula.clear_updaters() + + self.wait(.75) + + # -- Replace p with b / a in binomial square identity + + # Parentheses from the original equation don't change size at first, so the animation looks cleaner + bsi_formula_replaced = MathTex( + "(x +", START_G, "b", r"\over {a}", END_G, ")^2 =", "x^2", "+", "2", START_G, "b", r"\over {a}", + END_G, "x", "+", r"\bigl(", START_G, "b", r"\over {a}", END_G, r"\bigr)", "^2" + ).move_to(bsi_formula) + + p_indices = [(1, 5), (10, 13), (17, 20)] + + equation[2:6].set_color(ORANGE) # b / a + b_over_a_copies = { + (start, end): equation[2:6].copy() + for (start, end) in p_indices + } + + self.play( + AnimationGroup( + Indicate(equation[2:6], color=ORANGE, run_time=.5), + AnimationGroup( + AnimationGroup( + *[ + Transform( + b_over_a_copies[start, end], + bsi_formula_replaced[start:end].set_color(ORANGE) + ) + for start, end in p_indices + ] + ), + TransformMatchingShapes(bsi_formula, bsi_formula_replaced, run_time=.65), + lag_ratio=.45 + ), + lag_ratio=.35 + ) + ) + + self.remove(bsi_formula, *b_over_a_copies.values()) + bsi_formula = bsi_formula_replaced + self.add(bsi_formula) + + # Make initial parentheses bigger since they're now wrapping a fraction + bsi_formula_replaced = MathTex( + r"\bigl(x +", START_G, "b", r"\over {a}", END_G, r"\bigr)^2 =", "x^2", "+", "2", START_G, "{b}", + r"\over {a}", END_G, "x", "+", r"\bigl(", START_G, "{{b}}", r"\over {a}", END_G, r"\bigr)", "^2" + ).move_to(bsi_formula) + for b_over_a_index in (replaced_b_over_a_indices := [2, 3, 10, 11, 17, 18]): + bsi_formula_replaced[b_over_a_index].set_color(ORANGE) + + self.play(bsi_formula.animate.become(bsi_formula_replaced)) + + self.wait(.5) + + # -- Replace b / a with b / 2a in binomial square identity + + # Separate denominators from the fractions + # Make multiplying 2 phantom so TransformMatchingShapes doesn't copy it to the new denominators + two_copy = bsi_formula[8].copy() + bsi_formula.become( + MathTex( + r"\bigl(x +", START_G, "b", r"\over", "{a}", END_G, r"\bigr)^2 =", "x^2", "+", r"\phantom{2}", START_G, "{b}", + r"\over", "{a}", END_G, "x", "+", r"\bigl(", START_G, "{{b}}", r"\over", "{a}", END_G, r"\bigr)", "^2" + ).move_to(bsi_formula) + ) + for start, end in [(1, 5), (10, 14), (18, 22)]: + bsi_formula[start:end].set_color(ORANGE) + + bsi_formula_replaced = MathTex( + r"\bigl(x +", START_G, "b", r"\over {2a}", END_G, r"\bigr)^2 =", "x^2", "+", "2", START_G, "b", r"\over {2a}", + END_G, "x", "+", r"\bigl(", START_G, "b", r"\over {2a}", END_G, r"\bigr)", "^2" + ).move_to(bsi_formula) + for b_over_a_index in replaced_b_over_a_indices: + bsi_formula_replaced[b_over_a_index].set_color(YELLOW_E) + + two_final = bsi_formula_replaced[8] + + self.play( + # Separate it in two transformations so initial parentheses aren't copied to the final ones + TransformMatchingShapes(bsi_formula[0], replaced := bsi_formula_replaced[0]), + TransformMatchingShapes(bsi_formula[1:], replaced1 := bsi_formula_replaced[1:]), + + Transform(two_copy, two_final) + ) + + bsi_formula_replaced = MathTex( + r"\bigl(x +", START_G, "b", r"\over {2a}", END_G, r"\bigr)^2 =", "x^2", "+", r"\phantom{2}", START_G, + "b", r"\over {2a}", END_G, "x", "+", r"\bigl(", START_G, "b", r"\over {2a}", END_G, r"\bigr)", "^2" + ).move_to(bsi_formula) + for start, end in [(1, 5), (9, 13), (16, 20)]: + bsi_formula_replaced[start:end].set_color(YELLOW_E) + + self.remove(replaced, replaced1) + self.add(bsi_formula.become(bsi_formula_replaced)) + + self.wait(1) + + # -- Cancel twos in 2(b / 2a), in binomial square identity + + bsi_formula_replaced = MathTex( + r"\bigl(x +", START_G, "b", r"\over", "{2a}", END_G, r"\bigr)^2 =", "x^2", "+", START_G, "b", + r"\over", "{a}", END_G, "x", "+", r"\bigl(", START_G, "b", r"\over", "{2a}", END_G, r"\bigr)", + "^2" + ).move_to(bsi_formula) + + bsi_formula_replaced[1:6].set_color(YELLOW_E) + bsi_formula_replaced[9:14].set_color(ORANGE) + bsi_formula_replaced[17:22].set_color(YELLOW_E) + + denominator_copies = [bsi_formula_replaced[index].copy() for index in [4, 12, 20]] + for denominator in denominator_copies: + denominator.shift(.1 * DOWN) + + bsi_formula_replaced1 = MathTex( + r"\bigl(x +", START_G, "b", r"\over \phantom{{2a}}", END_G, r"\bigr)^2 =", "x^2", "+", START_G, "b", + r"\over \phantom{{a}}", END_G, "x", "+", r"\bigl(", START_G, "b", r"\over \phantom{{2a}}", END_G, r"\bigr)", "^2" + ).move_to(bsi_formula) + + bsi_formula_replaced1[1:5].set_color(YELLOW_E) + bsi_formula_replaced1[9:11].set_color(ORANGE) + bsi_formula_replaced1[16:18].set_color(YELLOW_E) + + self.play( + FadeIn(*denominator_copies, run_time=.75), + FadeOut(two_copy, run_time=.75), + TransformMatchingShapes(bsi_formula, bsi_formula_replaced1) + ) + + self.remove(*denominator_copies, bsi_formula_replaced1) + self.add(bsi_formula.become(bsi_formula_replaced).shift(.1 * DOWN)) + + # -- (b / 2a)² to left hand of binomial square identity + + bsi_formula = derive_equation(self, bsi_formula, [ + DerivationStep( + r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", "=", + "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x", + "+", r"{\bigl(}", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", + delay=0, transition_duration=.25, transform_type=FadeTransform + ), + DerivationStep( + r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", + "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"{\bigr)^2}", "=", + "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x" + "+", r"{\bigl(}", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", + "-", r"{\bigl(}", (rf"{{{{{START_G} b \over 2a {END_G}}}}}", YELLOW_E), r"{{\bigr)^2}}", + delay=.75, transition_duration=1.25 + ), + DerivationStep( + r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", + "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"\bigr)^2", "=", + "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x" + ) + ]) + + # -- Left hand of binomial square identity to quadratic equation + + self.wait(.5) + self.play( + bsi_formula[:7].animate.set_color(GREEN), + bsi_formula[8:].animate.set_color(MOROCCAN_BLUE), + equation[:7].animate.set_color(MOROCCAN_BLUE), + run_time=1.5, rate_func=ease_in_cubic + ) + + equation = derive_equation(self, equation, [ + DerivationStep( + r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", + "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"\bigr)^2", + "+", START_G, "c", r"\over {a}", END_G, START_G, "=", END_G, "0", + on_build=lambda tex: tex[:7].set_color(GREEN), + delay = 1.5, transform_type=TransformMatchingShapes + ) + ]) + + # -- Fade out binomial square identity, equation back to focus + + self.wait(1.5) + self.play( + LaggedStart( + FadeOut(bsi_formula), + equation.animate.shift(2 * DOWN) \ + .set_color(WHITE), + lag_ratio=.5 + ) + ) + self.wait(1.5) \ No newline at end of file diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index d4a4ee3..dea9d6b 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -1,5 +1,6 @@ -from manim import Scene, MathTex, TransformMatchingTex +from manim import Scene, MathTex, ManimColor, Transform, TransformMatchingTex from manim.animation.transform_matching_parts import TransformMatchingAbstractBase +from typing import Callable START_G = r"\left." @@ -7,25 +8,45 @@ class DerivationStep: - def __init__(self, *tex_strings: str, transition_duration: float = 1, delay: float = 0, - transform_type: type[TransformMatchingAbstractBase] = TransformMatchingTex): + def __init__(self, *parts: str | tuple[str, ManimColor], transition_duration: float = 1, delay: float =1, + transform_type: type[Transform | TransformMatchingAbstractBase] = TransformMatchingTex, + on_build: Callable[[MathTex], None] = None): """ Parameters: - *tex_strings (str): - A series of LaTeX strings that form the resulting MathTex expression. + *parts (str | tuple[str, ManimColor]): + A series of LaTeX strings that form the resulting MathTex expression. They may optionally define + a custom color for their corresponding section in the expression when in the form of a tuple. transition_duration (float, optional): Duration of the transition from the previous step to this one. delay (float, optional): Delay before performing the transition from the previous step to this one. + + transform_type (type[TransformMatchingAbstractBase], optional): + Type of Transform animation that will be used as transitioning animation from the previous step to this one. + + on_build (Callable[[MathTex], None], optional): + Callback called after the step is initialized and its corresponding TeX is built. """ + tex_strings = [ + part if isinstance(part, str) else part[0] + for part in parts + ] + self.tex = MathTex(*tex_strings) self.transition_duration = transition_duration self.delay = delay self.transform_type = transform_type + for i, part in enumerate(parts): + if isinstance(part, tuple): + self.tex[i].set_color(part[1]) + + if on_build: on_build(self.tex) + + def derive_equation(scene: Scene, base_equation: MathTex, steps: list[DerivationStep]) -> MathTex: """ Returns: @@ -35,6 +56,8 @@ def derive_equation(scene: Scene, base_equation: MathTex, steps: list[Derivation current_tex = base_equation for next_step in steps: + next_step.tex.move_to(current_tex) + if next_step.delay: scene.wait(next_step.delay) scene.play(next_step.transform_type( current_tex, next_step.tex, From baad4358390a39e3fab9d457f5fb1f6d92e15089 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Sat, 6 Jun 2026 18:28:29 +0200 Subject: [PATCH 09/11] Slight improvements to derivation --- animations/QuadraticFormulaDerivation.py | 21 +++++++++++++++------ utils/latex_helpers.py | 12 +++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/animations/QuadraticFormulaDerivation.py b/animations/QuadraticFormulaDerivation.py index 4bd0458..4d6456e 100644 --- a/animations/QuadraticFormulaDerivation.py +++ b/animations/QuadraticFormulaDerivation.py @@ -1,6 +1,5 @@ from manim import * -from manim.utils.rate_functions import ease_in_cubic - +from manim.utils.rate_functions import ease_in_cubic, ease_out_sine from utils.latex_helpers import (derive_equation, DerivationStep, START_G, END_G) @@ -211,13 +210,13 @@ def construct(self): # -- (b / 2a)² to left hand of binomial square identity bsi_formula = derive_equation(self, bsi_formula, [ - DerivationStep( + DerivationStep( # Reformat TeX r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", "=", "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x", "+", r"{\bigl(}", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", delay=0, transition_duration=.25, transform_type=FadeTransform ), - DerivationStep( + DerivationStep( # Add -(b / a)² to both hands r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"{\bigr)^2}", "=", "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x" @@ -225,10 +224,20 @@ def construct(self): "-", r"{\bigl(}", (rf"{{{{{START_G} b \over 2a {END_G}}}}}", YELLOW_E), r"{{\bigr)^2}}", delay=.75, transition_duration=1.25 ), - DerivationStep( + DerivationStep( # Color cancelling terms in red r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", - "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"\bigr)^2", "=", + "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"{\bigr)^2}", "=", "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x" + "+", r"{\bigl(}", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + "-", r"{\bigl(}", rf"{{{{{START_G} b \over 2a {END_G}}}}}", r"{{\bigr)^2}}", + on_build=lambda tex: tex[11:].set_color(RED_C), + rate_func=ease_out_sine, transition_duration=.5 + ), + DerivationStep( # Remove cancelling terms + r"\bigl(x +", (rf"{START_G} b \over 2a {END_G}", YELLOW_E), r"\bigr)^2", + "-", r"\bigl(", (rf"{{{START_G} b \over 2a {END_G}}}", YELLOW_E), r"\bigr)^2", "=", + "x^2 +", (rf"{START_G} b \over a {END_G}", ORANGE), "x", + delay=.5 ) ]) diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index dea9d6b..d1250a2 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -10,6 +10,7 @@ class DerivationStep: def __init__(self, *parts: str | tuple[str, ManimColor], transition_duration: float = 1, delay: float =1, transform_type: type[Transform | TransformMatchingAbstractBase] = TransformMatchingTex, + rate_func: Callable[[float], float] = None, on_build: Callable[[MathTex], None] = None): """ Parameters: @@ -26,6 +27,9 @@ def __init__(self, *parts: str | tuple[str, ManimColor], transition_duration: fl transform_type (type[TransformMatchingAbstractBase], optional): Type of Transform animation that will be used as transitioning animation from the previous step to this one. + rate_func (Callable[[float], float], optional): + Easing function that will be applied to the Transform transition animation. + on_build (Callable[[MathTex], None], optional): Callback called after the step is initialized and its corresponding TeX is built. """ @@ -39,6 +43,7 @@ def __init__(self, *parts: str | tuple[str, ManimColor], transition_duration: fl self.transition_duration = transition_duration self.delay = delay self.transform_type = transform_type + self.rate_func = rate_func for i, part in enumerate(parts): if isinstance(part, tuple): @@ -66,4 +71,9 @@ def derive_equation(scene: Scene, base_equation: MathTex, steps: list[Derivation current_tex = next_step.tex - return current_tex \ No newline at end of file + return current_tex + + +def color_tex_by_ranges(tex: MathTex, color: ManimColor, *ranges: tuple[int, int]): + for start, end in ranges: + tex[start:end].set_color(color) \ No newline at end of file From 0e0ffdd17827b8d43a48d3193b0fc9ead32e3a8c Mon Sep 17 00:00:00 2001 From: Pituivan Date: Sat, 6 Jun 2026 20:13:49 +0200 Subject: [PATCH 10/11] Phase 3: Isolate x and simplify expression into quadratic formula --- animations/QuadraticFormulaDerivation.py | 125 ++++++++++++++++++++++- utils/latex_helpers.py | 8 +- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/animations/QuadraticFormulaDerivation.py b/animations/QuadraticFormulaDerivation.py index 4d6456e..3a4e7cd 100644 --- a/animations/QuadraticFormulaDerivation.py +++ b/animations/QuadraticFormulaDerivation.py @@ -1,6 +1,6 @@ from manim import * -from manim.utils.rate_functions import ease_in_cubic, ease_out_sine -from utils.latex_helpers import (derive_equation, DerivationStep, +from manim.utils.rate_functions import ease_in_cubic, ease_out_sine, ease_in_out_cubic +from utils.latex_helpers import (derive_equation, DerivationStep, color_tex_by_ranges, START_G, END_G) MOROCCAN_BLUE = ManimColor("#27ADF5") @@ -273,4 +273,125 @@ def construct(self): ) ) + # --- Phase 3: Isolate x and simplify expression + + equation = derive_equation(self, equation, [ + DerivationStep( # Add (b / 2a)² to both hands + r"\bigl(x +", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + "-", r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + "+", START_G, "c", r"\over {a}", END_G, START_G, + "+", r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + "=", END_G, "0", + "+", r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + delay=1.5 + ), + DerivationStep( # Color cancelling terms in red + r"\bigl(x +", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + "-", r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + "+", START_G, "c", r"\over {a}", END_G, START_G, + "+", r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + "=", END_G, "0", + "+", r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + on_build=lambda tex: color_tex_by_ranges(tex, RED_C, (3,7), (13,17)), + rate_func=ease_out_sine, transition_duration=.5, delay=.5 + ), + DerivationStep( # Remove cancelling terms + r"\bigl(x +", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + "+", START_G, "c", r"\over {a}", END_G, + START_G, "=", END_G, + r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + delay=.5 + ), + DerivationStep( # Subtract c / a to both hands + r"\bigl(x +", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + "+", START_G, "c", r"\over {a}", END_G, + rf"- {START_G} c \over {{a}} {END_G}", + START_G, "=", END_G, + r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + rf"{{- {START_G} c \over {{a}} {END_G}}}" + ), + DerivationStep( # Color cancelling terms in red + r"\bigl(x +", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + ("+", RED_C), START_G, ("c", RED_C), (r"\over {a}", RED_C), END_G, + (rf"- {START_G} c \over {{a}} {END_G}", RED_C), + START_G, "=", END_G, + r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + rf"{{- {START_G} c \over {{a}} {END_G}}}", + rate_func=ease_out_sine, transition_duration=.5, delay=.5 + ), + DerivationStep( # Remove cancelling terms + r"\bigl(x +", rf"{START_G} b \over 2a {END_G}", r"\bigr)^2", + START_G, "=", END_G, + r"\bigl(", rf"{{{START_G} b \over 2a {END_G}}}", r"\bigr)^2", + rf"{{- {START_G} c \over {{a}} {END_G}}}", + delay=.5 + ), + DerivationStep( # (b / 2a)² ⟶ b² / 4a² + r"{\bigl(}x +", rf"{START_G} b \over 2a {END_G}", r"{\bigr)}^2", + START_G, "=", END_G, + rf"{{{START_G} b^2 \over 4a^2 {END_G}}}", + rf"{{- {START_G} c \over {{a}} {END_G}}}" + ), + DerivationStep( # Combine the two fractions in right hand + r"{\bigl(}x +", rf"{START_G} b \over 2a {END_G}", r"{\bigr)}^2", + START_G, "=", END_G, + r"\frac{b^2 - 4ac}{4a^2}" + ), + DerivationStep( # Take square out of left hand + "x", "+", START_G, "b" r"\over", "2a", END_G, + START_G, "=", END_G, + r"\pm \sqrt \frac{b^2 - 4ac}{4a^2}", + transform_type=FadeTransform + ), + DerivationStep( # Simplify square root from right hand + "x", "+", START_G, "b" r"\over", "2a", END_G, + START_G, "=", END_G, + r"\pm", START_G, r"\sqrt{b^2 - 4ac}", r"\over 2a", END_G + ), + DerivationStep( # ± to right hand numerator + "x", "+", START_G, "b" r"\over", "2a", END_G, + START_G, "=", END_G, + START_G, r"\pm", r"\sqrt{b^2 - 4ac}", r"\over 2a", END_G + ), + DerivationStep( # Subtract b / 2a from both hands + "x", "+", START_G, "b" r"\over", "2a", END_G, + "{-}", START_G, "{b}" r"\over", "{2a}", END_G, + START_G, "=", END_G, + START_G, r"\pm", r"\sqrt{b^2 - 4ac}", r"\over 2a", END_G, + "-", START_G, "b" r"\over", "2a", END_G, + ), + DerivationStep( # Color cancelling terms in red + "x", "+", START_G, "b" r"\over", "2a", END_G, + "{-}", START_G, "{b}" r"\over", "{2a}", END_G, + START_G, "=", END_G, + START_G, r"\pm", r"\sqrt{b^2 - 4ac}", r"\over 2a", END_G, + "-", START_G, "b" r"\over", "2a", END_G, + on_build=lambda tex: tex[1:12].set_color(RED_C), + rate_func=ease_out_sine, transition_duration=.5, delay=.5 + ), + DerivationStep( # Remove cancelling terms + "x", START_G, "=", END_G, + START_G, r"\pm", r"\sqrt{b^2 - 4ac}", r"\over 2a", END_G, + "-", START_G, "b" r"\over", "2a", END_G, + delay=.5 + ), + DerivationStep( # Join remaining fractions in right hand + "x", START_G, "=", END_G, + START_G, r"\pm", r"\sqrt{b^2 - 4ac}", "-", "b", r"\over", "2a", END_G + ), + DerivationStep( # Reorder right hand numerator terms + "x", START_G, "=", END_G, + START_G, "-", "b", r"\pm", r"\sqrt{b^2 - 4ac}", r"\over", "2a", END_G + ) + ]) + + self.wait(1) + + # --- Show off result + + self.play(Create( + SurroundingRectangle(equation, color=YELLOW_E, buff=.25), + rate_func=ease_in_out_cubic + )) + self.wait(1.5) \ No newline at end of file diff --git a/utils/latex_helpers.py b/utils/latex_helpers.py index d1250a2..b31d672 100644 --- a/utils/latex_helpers.py +++ b/utils/latex_helpers.py @@ -8,8 +8,10 @@ class DerivationStep: + default_transform_type: type[Transform] | type[TransformMatchingAbstractBase] = TransformMatchingTex + def __init__(self, *parts: str | tuple[str, ManimColor], transition_duration: float = 1, delay: float =1, - transform_type: type[Transform | TransformMatchingAbstractBase] = TransformMatchingTex, + transform_type: type[Transform] | type[TransformMatchingAbstractBase] = default_transform_type, rate_func: Callable[[float], float] = None, on_build: Callable[[MathTex], None] = None): """ @@ -24,7 +26,7 @@ def __init__(self, *parts: str | tuple[str, ManimColor], transition_duration: fl delay (float, optional): Delay before performing the transition from the previous step to this one. - transform_type (type[TransformMatchingAbstractBase], optional): + transform_type (type[Transform] | type[TransformMatchingAbstractBase], optional): Type of Transform animation that will be used as transitioning animation from the previous step to this one. rate_func (Callable[[float], float], optional): @@ -74,6 +76,6 @@ def derive_equation(scene: Scene, base_equation: MathTex, steps: list[Derivation return current_tex -def color_tex_by_ranges(tex: MathTex, color: ManimColor, *ranges: tuple[int, int]): +def color_tex_by_ranges(tex: MathTex, color: ManimColor, *ranges: tuple[int | None, int | None]): for start, end in ranges: tex[start:end].set_color(color) \ No newline at end of file From b5d9c89ea3ed5316f10dcda20dd908ee431d53b8 Mon Sep 17 00:00:00 2001 From: Pituivan Date: Sat, 6 Jun 2026 20:42:24 +0200 Subject: [PATCH 11/11] Define `utils/` as a package in `pyproject.toml` (fix #1) --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea0a75c..ca3b596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,4 +8,7 @@ version = "0.0.0" dependencies = [ "manim==0.20.1", "numpy==2.4.6" -] \ No newline at end of file +] + +[tool.setuptools] +packages = ["utils"] \ No newline at end of file