2022-02-15 18:39:45 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-02-08 00:21:53 +08:00
|
|
|
import itertools as it
|
2023-02-03 17:28:27 -08:00
|
|
|
from difflib import SequenceMatcher
|
2022-02-15 18:39:45 +08:00
|
|
|
|
2021-01-04 23:09:29 -08:00
|
|
|
from manimlib.animation.composition import AnimationGroup
|
2021-01-05 18:03:06 -08:00
|
|
|
from manimlib.animation.fading import FadeInFromPoint
|
|
|
|
from manimlib.animation.fading import FadeOutToPoint
|
2021-01-04 23:09:29 -08:00
|
|
|
from manimlib.animation.transform import Transform
|
2021-01-05 18:03:06 -08:00
|
|
|
from manimlib.mobject.mobject import Mobject
|
2021-01-04 23:09:29 -08:00
|
|
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
2023-02-03 17:28:27 -08:00
|
|
|
from manimlib.mobject.svg.string_mobject import StringMobject
|
2021-01-04 23:09:29 -08:00
|
|
|
|
2022-02-15 18:39:45 +08:00
|
|
|
from typing import TYPE_CHECKING
|
2022-02-16 21:08:25 +08:00
|
|
|
|
2022-02-15 18:39:45 +08:00
|
|
|
if TYPE_CHECKING:
|
2022-12-30 13:54:55 -08:00
|
|
|
from typing import Iterable
|
2022-02-15 18:39:45 +08:00
|
|
|
from manimlib.scene.scene import Scene
|
|
|
|
|
2021-01-04 23:09:29 -08:00
|
|
|
|
|
|
|
class TransformMatchingParts(AnimationGroup):
|
2022-12-28 13:39:46 -08:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-12-30 13:54:55 -08:00
|
|
|
source: Mobject,
|
|
|
|
target: Mobject,
|
|
|
|
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
|
2022-12-28 13:39:46 -08:00
|
|
|
match_animation: type = Transform,
|
|
|
|
mismatch_animation: type = Transform,
|
2022-12-30 13:54:55 -08:00
|
|
|
run_time: float = 2,
|
|
|
|
lag_ratio: float = 0,
|
2022-12-28 13:39:46 -08:00
|
|
|
**kwargs,
|
2022-03-28 18:54:43 +08:00
|
|
|
):
|
2022-12-28 13:39:46 -08:00
|
|
|
self.source = source
|
|
|
|
self.target = target
|
2022-12-30 13:54:55 -08:00
|
|
|
self.match_animation = match_animation
|
|
|
|
self.mismatch_animation = mismatch_animation
|
2022-12-29 10:44:52 -08:00
|
|
|
self.anim_config = dict(**kwargs)
|
2022-12-28 13:39:46 -08:00
|
|
|
|
|
|
|
# We will progressively build up a list of transforms
|
2023-01-10 16:01:34 -08:00
|
|
|
# from pieces in source to those in target. These
|
|
|
|
# two lists keep track of which pieces are accounted
|
2022-12-28 13:39:46 -08:00
|
|
|
# for so far
|
2022-12-30 13:54:55 -08:00
|
|
|
self.source_pieces = source.family_members_with_points()
|
|
|
|
self.target_pieces = target.family_members_with_points()
|
2022-12-28 13:39:46 -08:00
|
|
|
self.anims = []
|
|
|
|
|
2022-12-30 13:54:55 -08:00
|
|
|
for pair in matched_pairs:
|
|
|
|
self.add_transform(*pair)
|
|
|
|
|
2022-12-28 13:39:46 -08:00
|
|
|
# Match any pairs with the same shape
|
2022-12-30 13:54:55 -08:00
|
|
|
for pair in self.find_pairs_with_matching_shapes(self.source_pieces, self.target_pieces):
|
|
|
|
self.add_transform(*pair)
|
|
|
|
|
2022-12-28 13:39:46 -08:00
|
|
|
# Finally, account for mismatches
|
2023-01-10 16:01:34 -08:00
|
|
|
for source_piece in self.source_pieces:
|
|
|
|
if any([source_piece in anim.mobject.get_family() for anim in self.anims]):
|
|
|
|
continue
|
2022-12-28 13:39:46 -08:00
|
|
|
self.anims.append(FadeOutToPoint(
|
2023-01-10 16:01:34 -08:00
|
|
|
source_piece, target.get_center(),
|
2022-12-28 13:39:46 -08:00
|
|
|
**self.anim_config
|
|
|
|
))
|
2023-01-10 16:01:34 -08:00
|
|
|
for target_piece in self.target_pieces:
|
|
|
|
if any([target_piece in anim.mobject.get_family() for anim in self.anims]):
|
|
|
|
continue
|
2022-12-28 13:39:46 -08:00
|
|
|
self.anims.append(FadeInFromPoint(
|
2023-01-10 16:01:34 -08:00
|
|
|
target_piece, source.get_center(),
|
2022-12-28 13:39:46 -08:00
|
|
|
**self.anim_config
|
|
|
|
))
|
2022-12-30 13:54:55 -08:00
|
|
|
|
2022-12-29 10:44:52 -08:00
|
|
|
super().__init__(
|
|
|
|
*self.anims,
|
|
|
|
run_time=run_time,
|
|
|
|
lag_ratio=lag_ratio,
|
|
|
|
)
|
2022-03-30 21:53:00 +08:00
|
|
|
|
2022-12-28 13:39:46 -08:00
|
|
|
def add_transform(
|
|
|
|
self,
|
2022-12-30 13:54:55 -08:00
|
|
|
source: Mobject,
|
|
|
|
target: Mobject,
|
2022-12-28 13:39:46 -08:00
|
|
|
):
|
2022-12-30 13:54:55 -08:00
|
|
|
new_source_pieces = source.family_members_with_points()
|
|
|
|
new_target_pieces = target.family_members_with_points()
|
|
|
|
if len(new_source_pieces) == 0 or len(new_target_pieces) == 0:
|
2022-12-30 15:09:07 -08:00
|
|
|
# Don't animate null sorces or null targets
|
2022-12-30 13:54:55 -08:00
|
|
|
return
|
|
|
|
source_is_new = all(char in self.source_pieces for char in new_source_pieces)
|
|
|
|
target_is_new = all(char in self.target_pieces for char in new_target_pieces)
|
2022-12-30 15:09:07 -08:00
|
|
|
if not source_is_new or not target_is_new:
|
|
|
|
return
|
|
|
|
|
|
|
|
transform_type = self.mismatch_animation
|
|
|
|
if source.has_same_shape_as(target):
|
|
|
|
transform_type = self.match_animation
|
|
|
|
|
|
|
|
self.anims.append(transform_type(source, target, **self.anim_config))
|
|
|
|
for char in new_source_pieces:
|
|
|
|
self.source_pieces.remove(char)
|
|
|
|
for char in new_target_pieces:
|
|
|
|
self.target_pieces.remove(char)
|
2022-12-30 13:54:55 -08:00
|
|
|
|
|
|
|
def find_pairs_with_matching_shapes(
|
|
|
|
self,
|
|
|
|
chars1: list[Mobject],
|
|
|
|
chars2: list[Mobject]
|
|
|
|
) -> list[tuple[Mobject, Mobject]]:
|
2022-12-28 13:39:46 -08:00
|
|
|
result = []
|
|
|
|
for char1, char2 in it.product(chars1, chars2):
|
2022-12-29 12:03:17 -08:00
|
|
|
if char1.has_same_shape_as(char2):
|
2022-12-28 13:39:46 -08:00
|
|
|
result.append((char1, char2))
|
|
|
|
return result
|
2022-05-28 21:43:37 +08:00
|
|
|
|
2022-12-28 13:39:46 -08:00
|
|
|
def clean_up_from_scene(self, scene: Scene) -> None:
|
|
|
|
super().clean_up_from_scene(scene)
|
|
|
|
scene.remove(self.mobject)
|
|
|
|
scene.add(self.target)
|
2022-05-28 21:43:37 +08:00
|
|
|
|
2022-02-08 00:21:53 +08:00
|
|
|
|
2022-12-30 13:54:55 -08:00
|
|
|
class TransformMatchingShapes(TransformMatchingParts):
|
|
|
|
"""Alias for TransformMatchingParts"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class TransformMatchingStrings(TransformMatchingParts):
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
source: StringMobject,
|
|
|
|
target: StringMobject,
|
|
|
|
matched_keys: Iterable[str] = [],
|
|
|
|
key_map: dict[str, str] = dict(),
|
2023-02-03 17:28:27 -08:00
|
|
|
matched_pairs: Iterable[tuple[VMobject, VMobject]] = [],
|
2022-12-30 13:54:55 -08:00
|
|
|
**kwargs,
|
|
|
|
):
|
2023-02-03 17:28:27 -08:00
|
|
|
matched_pairs = [
|
|
|
|
*matched_pairs,
|
|
|
|
*self.matching_blocks(source, target, matched_keys, key_map),
|
2022-12-30 13:54:55 -08:00
|
|
|
]
|
2023-02-03 17:28:27 -08:00
|
|
|
|
2022-12-30 13:54:55 -08:00
|
|
|
super().__init__(
|
|
|
|
source, target,
|
|
|
|
matched_pairs=matched_pairs,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
2023-02-03 17:28:27 -08:00
|
|
|
def matching_blocks(
|
|
|
|
self,
|
|
|
|
source: StringMobject,
|
|
|
|
target: StringMobject,
|
|
|
|
matched_keys: Iterable[str],
|
|
|
|
key_map: dict[str, str]
|
|
|
|
) -> list[tuple[VMobject, VMobject]]:
|
|
|
|
syms1 = source.get_symbol_substrings()
|
|
|
|
syms2 = target.get_symbol_substrings()
|
|
|
|
counts1 = list(map(source.substr_to_path_count, syms1))
|
|
|
|
counts2 = list(map(target.substr_to_path_count, syms2))
|
|
|
|
|
|
|
|
# Start with user specified matches
|
|
|
|
blocks = [(source[key], target[key]) for key in matched_keys]
|
|
|
|
blocks += [(source[key1], target[key2]) for key1, key2 in key_map.items()]
|
|
|
|
|
|
|
|
# Nullify any intersections with those matches in the two symbol lists
|
|
|
|
for sub_source, sub_target in blocks:
|
|
|
|
for i in range(len(syms1)):
|
|
|
|
if source[i] in sub_source.family_members_with_points():
|
|
|
|
syms1[i] = "Null1"
|
|
|
|
for j in range(len(syms2)):
|
|
|
|
if target[j] in sub_target.family_members_with_points():
|
|
|
|
syms2[j] = "Null2"
|
|
|
|
|
|
|
|
# Group together longest matching substrings
|
|
|
|
while True:
|
|
|
|
matcher = SequenceMatcher(None, syms1, syms2)
|
|
|
|
match = matcher.find_longest_match(0, len(syms1), 0, len(syms2))
|
|
|
|
if match.size == 0:
|
|
|
|
break
|
|
|
|
|
|
|
|
i1 = sum(counts1[:match.a])
|
|
|
|
i2 = sum(counts2[:match.b])
|
|
|
|
size = sum(counts1[match.a:match.a + match.size])
|
|
|
|
|
|
|
|
blocks.append((source[i1:i1 + size], target[i2:i2 + size]))
|
|
|
|
|
|
|
|
for i in range(match.size):
|
|
|
|
syms1[match.a + i] = "Null1"
|
|
|
|
syms2[match.b + i] = "Null2"
|
|
|
|
|
|
|
|
return blocks
|
|
|
|
|
2022-12-30 13:54:55 -08:00
|
|
|
|
2022-12-28 13:39:46 -08:00
|
|
|
class TransformMatchingTex(TransformMatchingStrings):
|
|
|
|
"""Alias for TransformMatchingStrings"""
|
|
|
|
pass
|