Skip to content

Commit

Permalink
Merge pull request 3b1b#1989 from 3b1b/video-work
Browse files Browse the repository at this point in the history
A few more fixes and tweaks.
  • Loading branch information
3b1b authored Feb 5, 2023
2 parents fbcbbc9 + d1b1df6 commit 66b78d0
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 192 deletions.
80 changes: 50 additions & 30 deletions example_scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,17 @@ def construct(self):
self.add(lines[0])
# The animation TransformMatchingStrings will line up parts
# of the source and target which have matching substring strings.
# Here, giving it a little path_arc makes each part sort of
# rotate into their final positions, which feels appropriate
# for the idea of rearranging an equation
# Here, giving it a little path_arc makes each part rotate into
# their final positions, which feels appropriate for the idea of
# rearranging an equation
self.play(
TransformMatchingStrings(
lines[0].copy(), lines[1],
# matched_keys specifies which substring should
# line up. If it's not specified, the animation
# will try its best, but may not quite give the
# intended effect
# will align the longest matching substrings.
# In this case, the substring "^2 = C^2" would
# trip it up
matched_keys=["A^2", "B^2", "C^2"],
# When you want a substring from the source
# to go to a non-equal substring from the target,
Expand All @@ -206,25 +207,57 @@ def construct(self):
),
)
self.wait(2)
self.play(LaggedStartMap(FadeOut, lines, shift=2 * RIGHT))

# You can also index into Tex mobject (or other StringMobjects)
# by substrings and regular expressions
top_equation = lines[0]
low_equation = lines[3]
# TransformMatchingShapes will try to line up all pieces of a
# source mobject with those of a target, regardless of the
# what Mobject type they are.
source = Text("the morse code", height=1)
target = Text("here come dots", height=1)
saved_source = source.copy()

self.play(LaggedStartMap(FlashAround, low_equation["C"], lag_ratio=0.5))
self.play(LaggedStartMap(FlashAround, low_equation["B"], lag_ratio=0.5))
self.play(LaggedStartMap(FlashAround, top_equation[re.compile(r"\w\^2")]))
self.play(Indicate(low_equation[R"\sqrt"]))
self.play(Write(source))
self.wait()
self.play(LaggedStartMap(FadeOut, lines, shift=2 * RIGHT))
kw = dict(run_time=3, path_arc=PI / 2)
self.play(TransformMatchingShapes(source, target, **kw))
self.wait()
self.play(TransformMatchingShapes(target, saved_source, **kw))
self.wait()


class TexIndexing(Scene):
def construct(self):
# You can index into Tex mobject (or other StringMobjects) by substrings
equation = Tex(R"e^{\pi i} = -1", font_size=144)

self.add(equation)
self.play(FlashAround(equation["e"]))
self.wait()
self.play(Indicate(equation[R"\pi"]))
self.wait()
self.play(TransformFromCopy(
equation[R"e^{\pi i}"].copy().set_opacity(0.5),
equation["-1"],
path_arc=-PI / 2,
run_time=3
))
self.play(FadeOut(equation))

# Or regular expressions
equation = Tex("A^2 + B^2 = C^2", font_size=144)

self.play(Write(equation))
for part in equation[re.compile(r"\w\^2")]:
self.play(FlashAround(part))
self.wait()
self.play(FadeOut(equation))

# Indexing by substrings like this may not work when
# the order in which Latex draws symbols does not match
# the order in which they show up in the string.
# For example, here the infinity is drawn before the sigma
# so we don't get the desired behavior.
equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}")
equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}", font_size=72)
self.play(FadeIn(equation))
self.play(equation[R"\infty"].animate.set_color(RED)) # Doesn't hit the infinity
self.wait()
Expand All @@ -236,27 +269,14 @@ def construct(self):
equation = Tex(
R"\sum_{n = 1}^\infty {1 \over n^2} = {\pi^2 \over 6}",
# Explicitly mark "\infty" as a substring you might want to access
isolate=[R"\infty"]
isolate=[R"\infty"],
font_size=72
)
self.play(FadeIn(equation))
self.play(equation[R"\infty"].animate.set_color(RED)) # Got it!
self.wait()
self.play(FadeOut(equation))

# TransformMatchingShapes will try to line up all pieces of a
# source mobject with those of a target, regardless of the
# what Mobject type they are.
source = Text("the morse code", height=1)
target = Text("here come dots", height=1)

self.play(Write(source))
self.wait()
kw = dict(run_time=3, path_arc=PI / 2)
self.play(TransformMatchingShapes(source, target, **kw))
self.wait()
self.play(TransformMatchingShapes(target, source, **kw))
self.wait()


class UpdatersExample(Scene):
def construct(self):
Expand Down
4 changes: 2 additions & 2 deletions manimlib/animation/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def __init__(
class LaggedStartMap(LaggedStart):
def __init__(
self,
AnimationClass: type,
anim_func: Callable[[Mobject], Animation],
group: Mobject,
arg_creator: Callable[[Mobject], tuple] | None = None,
run_time: float = 2.0,
Expand All @@ -175,7 +175,7 @@ def __init__(
anim_kwargs = dict(kwargs)
anim_kwargs.pop("lag_ratio", None)
super().__init__(
*(AnimationClass(submob, **anim_kwargs) for submob in group),
*(anim_func(submob, **anim_kwargs) for submob in group),
run_time=run_time,
lag_ratio=lag_ratio,
)
2 changes: 0 additions & 2 deletions manimlib/animation/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ def begin(self) -> None:
def finish(self) -> None:
super().finish()
self.mobject.unlock_data()
if self.target_mobject is not None and self.rate_func(1) == 1:
self.mobject.become(self.target_mobject)

def create_target(self) -> Mobject:
# Has no meaningful effect here, but may be useful
Expand Down
70 changes: 51 additions & 19 deletions manimlib/animation/transform_matching_parts.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
from __future__ import annotations

import itertools as it

import numpy as np
from difflib import SequenceMatcher

from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeInFromPoint
from manimlib.animation.fading import FadeOutToPoint
from manimlib.animation.fading import FadeTransformPieces
from manimlib.animation.transform import Transform
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Group
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.svg.string_mobject import StringMobject

from typing import TYPE_CHECKING

Expand Down Expand Up @@ -131,28 +127,64 @@ def __init__(
target: StringMobject,
matched_keys: Iterable[str] = [],
key_map: dict[str, str] = dict(),
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
matched_pairs: Iterable[tuple[VMobject, VMobject]] = [],
**kwargs,
):
matched_pairs = list(matched_pairs) + [
*[(source[key], target[key]) for key in matched_keys],
*[(source[key1], target[key2]) for key1, key2 in key_map.items()],
*[
(source[substr], target[substr])
for substr in [
*source.get_specified_substrings(),
*target.get_specified_substrings(),
*source.get_symbol_substrings(),
*target.get_symbol_substrings(),
]
]
matched_pairs = [
*matched_pairs,
*self.matching_blocks(source, target, matched_keys, key_map),
]

super().__init__(
source, target,
matched_pairs=matched_pairs,
**kwargs,
)

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


class TransformMatchingTex(TransformMatchingStrings):
"""Alias for TransformMatchingStrings"""
Expand Down
13 changes: 5 additions & 8 deletions manimlib/camera/camera_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ def __init__(
self.set_height(frame_shape[1], stretch=True)
self.move_to(center_point)

def note_changed_data(self, recurse_up: bool = True):
super().note_changed_data(recurse_up)
self.get_view_matrix(refresh=True)
self.get_implied_camera_location(refresh=True)

def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"][:] = rotation.as_quat()
return self
Expand Down Expand Up @@ -89,7 +84,7 @@ def get_view_matrix(self, refresh=False):
Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system
"""
if refresh:
if self._data_has_changed:
shift = np.identity(4)
rotation = np.identity(4)
scale_mat = np.identity(4)
Expand Down Expand Up @@ -169,10 +164,12 @@ def increment_gamma(self, dgamma: float):
self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2])
return self

@Mobject.affects_data
def set_focal_distance(self, focal_distance: float):
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
return self

@Mobject.affects_data
def set_field_of_view(self, field_of_view: float):
self.uniforms["fovy"] = field_of_view
return self
Expand Down Expand Up @@ -202,8 +199,8 @@ def get_focal_distance(self) -> float:
def get_field_of_view(self) -> float:
return self.uniforms["fovy"]

def get_implied_camera_location(self, refresh=False) -> np.ndarray:
if refresh:
def get_implied_camera_location(self) -> np.ndarray:
if self._data_has_changed:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
self.camera_location = self.get_center() + dist * to_camera
Expand Down
29 changes: 28 additions & 1 deletion manimlib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ def parse_cli():
action="store_true",
help="Render to a movie file with an alpha channel",
)
parser.add_argument(
"--vcodec",
help="Video codec to use with ffmpeg",
)
parser.add_argument(
"--pix_fmt",
help="Pixel format to use for the output of ffmpeg, defaults to `yuv420p`",
)
parser.add_argument(
"-q", "--quiet",
action="store_true",
Expand Down Expand Up @@ -160,6 +168,12 @@ def parse_cli():
action="store_true",
help="Show progress bar for each animation",
)
parser.add_argument(
"--prerun",
action="store_true",
help="Calculate total framecount, to display in a progress bar, by doing " + \
"an initial run of the scene which skips animations."
)
parser.add_argument(
"--video_dir",
help="Directory to write video",
Expand Down Expand Up @@ -386,7 +400,7 @@ def get_output_directory(args: Namespace, custom_config: dict) -> str:


def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
return {
result = {
"write_to_movie": not args.skip_animations and args.write_file,
"break_into_partial_movies": custom_config["break_into_partial_movies"],
"save_last_frame": args.skip_animations and args.write_file,
Expand All @@ -402,6 +416,18 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
"quiet": args.quiet,
}

if args.vcodec:
result["video_codec"] = args.vcodec
elif args.transparent:
result["video_codec"] = 'prores_ks'
elif args.gif:
result["video_codec"] = ''

if args.pix_fmt:
result["pix_fmt"] = args.pix_fmt

return result


def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict:
# Default to making window half the screen size
Expand Down Expand Up @@ -489,6 +515,7 @@ def get_configuration(args: Namespace) -> dict:
"presenter_mode": args.presenter_mode,
"leave_progress_bars": args.leave_progress_bars,
"show_animation_progress": args.show_animation_progress,
"prerun": args.prerun,
"embed_exception_mode": custom_config["embed_exception_mode"],
"embed_error_sound": custom_config["embed_error_sound"],
}
Loading

0 comments on commit 66b78d0

Please sign in to comment.