Merge pull request #406 from 3b1b/clacks

Clacks
This commit is contained in:
Grant Sanderson 2019-01-16 11:13:33 -08:00 committed by GitHub
commit 5d09a892d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1436 additions and 177 deletions

View file

@ -3,34 +3,75 @@ import subprocess
from pydub import AudioSegment from pydub import AudioSegment
MIN_TIME_BETWEEN_FLASHES = 0.004 class Block(Square):
CONFIG = {
"mass": 1,
"velocity": 0,
"width": None,
"label_text": None,
"label_scale_value": 0.8,
"fill_opacity": 1,
"stroke_width": 3,
"stroke_color": WHITE,
"fill_color": None,
"sheen_direction": UL,
"sheen": 0.5,
"sheen_direction": UL,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
if self.width is None:
self.width = self.mass_to_width(self.mass)
if self.fill_color is None:
self.fill_color = self.mass_to_color(self.mass)
if self.label_text is None:
self.label_text = self.mass_to_label_text(self.mass)
Square.__init__(self, side_length=self.width, **kwargs)
self.label = self.get_label()
self.add(self.label)
def get_label(self):
label = TextMobject(self.label_text)
label.scale(self.label_scale_value)
label.next_to(self, UP, SMALL_BUFF)
return label
def get_points_defining_boundary(self):
return self.points
def mass_to_color(self, mass):
colors = [
LIGHT_GREY,
BLUE_B,
BLUE_D,
BLUE_E,
BLUE_E,
DARK_GREY,
DARK_GREY,
BLACK,
]
index = min(int(np.log10(mass)), len(colors) - 1)
return colors[index]
def mass_to_width(self, mass):
return 1 + 0.25 * np.log10(mass)
def mass_to_label_text(self, mass):
return "{:,}\\,kg".format(int(mass))
class SlidingBlocks(VGroup): class SlidingBlocks(VGroup):
CONFIG = { CONFIG = {
"block1_config": { "block1_config": {
"mass": 1,
"velocity": -2,
"distance": 7, "distance": 7,
"width": None, "mass": 1e6,
"color": None, "velocity": -2,
"label_text": None,
}, },
"block2_config": { "block2_config": {
"distance": 3,
"mass": 1, "mass": 1,
"velocity": 0, "velocity": 0,
"distance": 3,
"width": None,
"color": None,
"label_text": None,
},
"block_style": {
"fill_opacity": 1,
"stroke_width": 3,
"stroke_color": WHITE,
"sheen_direction": UL,
"sheen_factor": 0.5,
"sheen_direction": UL,
}, },
"collect_clack_data": True, "collect_clack_data": True,
} }
@ -54,30 +95,13 @@ class SlidingBlocks(VGroup):
if self.collect_clack_data: if self.collect_clack_data:
self.clack_data = self.get_clack_data() self.clack_data = self.get_clack_data()
def get_block(self, mass, distance, velocity, width, color, label_text): def get_block(self, distance, **kwargs):
if width is None: block = Block(**kwargs)
width = self.mass_to_width(mass)
if color is None:
color = self.mass_to_color(mass)
if label_text is None:
label_text = "{:,}\\,kg".format(int(mass))
block = Square(side_length=width)
block.mass = mass
block.velocity = velocity
style = dict(self.block_style)
style["fill_color"] = color
block.set_style(**style)
block.move_to( block.move_to(
self.floor.get_top()[1] * UP + self.floor.get_top()[1] * UP +
(self.wall.get_right()[0] + distance) * RIGHT, (self.wall.get_right()[0] + distance) * RIGHT,
DL, DL,
) )
label = block.label = TextMobject(label_text)
label.scale(0.8)
label.next_to(block, UP, SMALL_BUFF)
block.add(label)
return block return block
def get_phase_space_point_tracker(self): def get_phase_space_point_tracker(self):
@ -104,36 +128,6 @@ class SlidingBlocks(VGroup):
) )
self.update_blocks_from_phase_space_point_tracker() self.update_blocks_from_phase_space_point_tracker()
def old_update_positions(self, dt):
# Based on velocity diagram bouncing...didn't work for
# large masses, due to frame rate mismatch
blocks = self.submobjects
for block in blocks:
block.shift(block.velocity * dt * RIGHT)
if blocks[0].get_left()[0] < blocks[1].get_right()[0]:
# Two blocks collide
m1 = blocks[0].mass
m2 = blocks[1].mass
v1 = blocks[0].velocity
v2 = blocks[1].velocity
v_phase_space_point = np.array([
np.sqrt(m1) * v1, -np.sqrt(m2) * v2
])
angle = 2 * np.arctan(np.sqrt(m2 / m1))
new_vps_point = rotate_vector(v_phase_space_point, angle)
for block, value in zip(blocks, new_vps_point):
block.velocity = value / np.sqrt(block.mass)
blocks[1].move_to(blocks[0].get_corner(DL), DR)
self.surrounding_scene.clack(blocks[0].get_left())
if blocks[1].get_left()[0] < self.wall.get_right()[0]:
# Second block hits wall
blocks[1].velocity *= -1
blocks[1].move_to(self.wall.get_corner(DR), DL)
if blocks[0].get_left()[0] < blocks[1].get_right()[0]:
blocks[0].move_to(blocks[1].get_corner(DR), DL)
self.surrounding_scene.clack(blocks[1].get_left())
return self
def update_blocks_from_phase_space_point_tracker(self): def update_blocks_from_phase_space_point_tracker(self):
block1, block2 = self.block1, self.block2 block1, block2 = self.block1, self.block2
@ -199,23 +193,6 @@ class SlidingBlocks(VGroup):
clack_data.append((location, time)) clack_data.append((location, time))
return clack_data return clack_data
def mass_to_color(self, mass):
colors = [
LIGHT_GREY,
BLUE_B,
BLUE_D,
BLUE_E,
BLUE_E,
DARK_GREY,
DARK_GREY,
BLACK,
]
index = min(int(np.log10(mass)), len(colors) - 1)
return colors[index]
def mass_to_width(self, mass):
return 1 + 0.25 * np.log10(mass)
class ClackFlashes(ContinualAnimation): class ClackFlashes(ContinualAnimation):
CONFIG = { CONFIG = {
@ -225,6 +202,7 @@ class ClackFlashes(ContinualAnimation):
"flash_radius": 0.2, "flash_radius": 0.2,
}, },
"start_up_time": 0, "start_up_time": 0,
"min_time_between_flashes": 1 / 30,
} }
def __init__(self, clack_data, **kwargs): def __init__(self, clack_data, **kwargs):
@ -233,7 +211,7 @@ class ClackFlashes(ContinualAnimation):
group = Group() group = Group()
last_time = 0 last_time = 0
for location, time in clack_data: for location, time in clack_data:
if (time - last_time) < MIN_TIME_BETWEEN_FLASHES: if (time - last_time) < self.min_time_between_flashes:
continue continue
last_time = time last_time = time
flash = Flash(location, **self.flash_config) flash = Flash(location, **self.flash_config)
@ -256,6 +234,33 @@ class ClackFlashes(ContinualAnimation):
self.mobject.remove(flash.mobject) self.mobject.remove(flash.mobject)
class Wall(Line):
CONFIG = {
"tick_spacing": 0.5,
"tick_length": 0.25,
"tick_style": {
"stroke_width": 1,
"stroke_color": WHITE,
},
}
def __init__(self, height, **kwargs):
Line.__init__(self, ORIGIN, height * UP, **kwargs)
self.height = height
self.ticks = self.get_ticks()
self.add(self.ticks)
def get_ticks(self):
n_lines = int(self.height / self.tick_spacing)
lines = VGroup(*[
Line(ORIGIN, self.tick_length * UR).shift(n * self.tick_spacing * UP)
for n in range(n_lines)
])
lines.set_style(**self.tick_style)
lines.move_to(self, DR)
return lines
class BlocksAndWallScene(Scene): class BlocksAndWallScene(Scene):
CONFIG = { CONFIG = {
"include_sound": True, "include_sound": True,
@ -268,6 +273,8 @@ class BlocksAndWallScene(Scene):
"counter_label": "\\# Collisions: ", "counter_label": "\\# Collisions: ",
"collision_sound": "clack.wav", "collision_sound": "clack.wav",
"show_flash_animations": True, "show_flash_animations": True,
"min_time_between_sounds": 0.004,
"allow_sound": True,
} }
def setup(self): def setup(self):
@ -320,16 +327,10 @@ class BlocksAndWallScene(Scene):
self.counter_mob = counter_mob self.counter_mob = counter_mob
def get_wall(self): def get_wall(self):
wall = Line(self.floor_y * UP, FRAME_HEIGHT * UP / 2) height = (FRAME_HEIGHT / 2) - self.floor_y
wall = Wall(height=height)
wall.shift(self.wall_x * RIGHT) wall.shift(self.wall_x * RIGHT)
lines = VGroup(*[ wall.to_edge(UP, buff=0)
Line(ORIGIN, 0.25 * UR)
for x in range(self.n_wall_ticks)
])
lines.set_stroke(width=1)
lines.arrange_submobjects(UP, buff=MED_SMALL_BUFF)
lines.move_to(wall, DR)
wall.add(lines)
return wall return wall
def get_floor(self): def get_floor(self):
@ -359,7 +360,7 @@ class BlocksAndWallScene(Scene):
total_time = max(times) + 1 total_time = max(times) + 1
clacks = AudioSegment.silent(int(1000 * total_time)) clacks = AudioSegment.silent(int(1000 * total_time))
last_position = 0 last_position = 0
min_diff = int(1000 * MIN_TIME_BETWEEN_FLASHES) min_diff = int(1000 * self.min_time_between_sounds)
for time in times: for time in times:
position = int(1000 * time) position = int(1000 * time)
d_position = position - last_position d_position = position - last_position
@ -583,8 +584,7 @@ class BlocksAndWallExample(BlocksAndWallScene):
CONFIG = { CONFIG = {
"sliding_blocks_config": { "sliding_blocks_config": {
"block1_config": { "block1_config": {
# "mass": 1e0, "mass": 1e0,
"mass": 64,
"velocity": -2, "velocity": -2,
} }
}, },
@ -927,7 +927,7 @@ class PiComputingAlgorithmsAxes(Scene):
lag_ratio=0.4, lag_ratio=0.4,
)) ))
self.wait() self.wait()
self.play(CircleThenFadeAround(algorithms[-1][0])) self.play(ShowCreationThenFadeAround(algorithms[-1][0]))
def get_machin_like_formula(self): def get_machin_like_formula(self):
formula = TexMobject( formula = TexMobject(
@ -1152,7 +1152,7 @@ class CompareToGalacticMass(Scene):
"velocity": -0.01, "velocity": -0.01,
"distance": 4.5, "distance": 4.5,
"label_text": "$100^{(20 - 1)}$ kg", "label_text": "$100^{(20 - 1)}$ kg",
"color": BLACK, "fill_color": BLACK,
}, },
"block2_config": { "block2_config": {
"distance": 1, "distance": 1,

File diff suppressed because it is too large Load diff

View file

@ -203,6 +203,7 @@ class VFadeIn(Animation):
to mobjects while they are being animated in some other way (e.g. shifting to mobjects while they are being animated in some other way (e.g. shifting
then) in a way that does not work with FadeIn and FadeOut then) in a way that does not work with FadeIn and FadeOut
""" """
def update_submobject(self, submobject, starting_submobject, alpha): def update_submobject(self, submobject, starting_submobject, alpha):
submobject.set_stroke( submobject.set_stroke(
opacity=interpolate(0, starting_submobject.get_stroke_opacity(), alpha) opacity=interpolate(0, starting_submobject.get_stroke_opacity(), alpha)

View file

@ -24,6 +24,7 @@ from manimlib.utils.rate_functions import smooth
from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.rate_functions import squish_rate_func
from manimlib.utils.rate_functions import there_and_back from manimlib.utils.rate_functions import there_and_back
from manimlib.utils.rate_functions import wiggle from manimlib.utils.rate_functions import wiggle
from manimlib.utils.rate_functions import double_smooth
class FocusOn(Transform): class FocusOn(Transform):
@ -143,6 +144,20 @@ class ShowCreationThenDestruction(ShowPassingFlash):
} }
class ShowCreationThenFadeOut(Succession):
CONFIG = {
"remover": True,
}
def __init__(self, mobject, **kwargs):
Succession.__init__(
self,
ShowCreation, mobject,
FadeOut, mobject,
**kwargs
)
class AnimationOnSurroundingRectangle(AnimationGroup): class AnimationOnSurroundingRectangle(AnimationGroup):
CONFIG = { CONFIG = {
"surrounding_rectangle_config": {}, "surrounding_rectangle_config": {},
@ -174,7 +189,7 @@ class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle):
} }
class CircleThenFadeAround(AnimationOnSurroundingRectangle): class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
CONFIG = { CONFIG = {
"rect_to_animation": lambda rect: Succession( "rect_to_animation": lambda rect: Succession(
ShowCreation, rect, ShowCreation, rect,

View file

@ -19,32 +19,92 @@ def parse_cli():
) )
parser.add_argument( parser.add_argument(
"scene_names", "scene_names",
nargs="+", nargs="*",
help="Name of the Scene class you want to see", help="Name of the Scene class you want to see",
) )
optional_args = [ parser.add_argument(
("-p", "--preview"), "-p", "--preview",
("-w", "--write_to_movie"), action="store_true",
("-s", "--show_last_frame"), help="Automatically open movie file once its done",
("-l", "--low_quality"), ),
("-m", "--medium_quality"), parser.add_argument(
("-g", "--save_pngs"), "-w", "--write_to_movie",
("-f", "--show_file_in_finder"), action="store_true",
("-t", "--transparent"), help="Render the scene as a movie file",
("-q", "--quiet"), ),
("-a", "--write_all") parser.add_argument(
] "-s", "--show_last_frame",
for short_arg, long_arg in optional_args: action="store_true",
parser.add_argument(short_arg, long_arg, action="store_true") help="Save the last frame and open the image file",
parser.add_argument("-o", "--output_file_name") ),
parser.add_argument("-n", "--start_at_animation_number") parser.add_argument(
parser.add_argument("-r", "--resolution") "-l", "--low_quality",
parser.add_argument("-c", "--color") action="store_true",
help="Render at a low quality (for faster rendering)",
),
parser.add_argument(
"-m", "--medium_quality",
action="store_true",
help="Render at a medium quality",
),
parser.add_argument(
"-g", "--save_pngs",
action="store_true",
help="Save each frame as a png",
),
parser.add_argument(
"-f", "--show_file_in_finder",
action="store_true",
help="Show the output file in finder",
),
parser.add_argument(
"-t", "--transparent",
action="store_true",
help="Render to a movie file with an alpha channel",
),
parser.add_argument(
"-q", "--quiet",
action="store_true",
help="",
),
parser.add_argument(
"-a", "--write_all",
action="store_true",
help="Write all the scenes from a file",
),
parser.add_argument(
"-o", "--output_file_name",
nargs=1,
help="Specify the name of the output file, if"
"it should be different from the scene class name",
)
parser.add_argument(
"-n", "--start_at_animation_number",
help="Start rendering not from the first animation, but"
"from another, specified by its index. If you pass"
"in two comma separated values, e.g. \"3,6\", it will end"
"the rendering at the second value",
)
parser.add_argument(
"-r", "--resolution",
help="Resolution, passed as \"height,width\"",
)
parser.add_argument(
"-c", "--color",
help="Background color",
)
parser.add_argument( parser.add_argument(
"--sound", "--sound",
action="store_true", action="store_true",
help="Play a success/failure sound", help="Play a success/failure sound",
) )
parser.add_argument(
"--leave_progress_bars",
action="store_true",
help="Leave progress bars displayed in terminal",
)
# For live streaming
module_location.add_argument( module_location.add_argument(
"--livestream", "--livestream",
action="store_true", action="store_true",
@ -129,6 +189,7 @@ def get_configuration(args):
"start_at_animation_number": args.start_at_animation_number, "start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None, "end_at_animation_number": None,
"sound": args.sound, "sound": args.sound,
"leave_progress_bars": args.leave_progress_bars
} }
# Camera configuration # Camera configuration

View file

@ -133,7 +133,8 @@ def main(config):
"movie_file_extension", "movie_file_extension",
"start_at_animation_number", "start_at_animation_number",
"end_at_animation_number", "end_at_animation_number",
"output_file_name" "output_file_name",
"leave_progress_bars",
] ]
]) ])
if config["save_pngs"]: if config["save_pngs"]:

View file

@ -9,6 +9,7 @@ from manimlib.mobject.svg.tex_mobject import TexMobject
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
from manimlib.utils.config_ops import merge_config
from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import angle_of_vector
# TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene # TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene
@ -35,16 +36,21 @@ class Axes(VGroup):
def __init__(self, **kwargs): def __init__(self, **kwargs):
VGroup.__init__(self, **kwargs) VGroup.__init__(self, **kwargs)
self.x_axis = self.get_axis(self.x_min, self.x_max, self.x_axis_config) x_axis_config = merge_config([
self.y_axis = self.get_axis(self.y_min, self.y_max, self.y_axis_config) self.x_axis_config,
self.y_axis.rotate(np.pi / 2, about_point=ORIGIN) {"x_min": self.x_min, "x_max": self.x_min},
self.number_line_config,
])
y_axis_config = merge_config([
self.y_axis_config,
{"x_min": self.y_min, "x_max": self.y_min},
self.number_line_config,
])
self.x_axis = NumberLine(**x_axis_config)
self.y_axis = NumberLine(**y_axis_config)
self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN)
self.add(self.x_axis, self.y_axis) self.add(self.x_axis, self.y_axis)
def get_axis(self, min_val, max_val, extra_config):
config = dict(self.number_line_config)
config.update(extra_config)
return NumberLine(x_min=min_val, x_max=max_val, **config)
def coords_to_point(self, *coords): def coords_to_point(self, *coords):
origin = self.x_axis.number_to_point(0) origin = self.x_axis.number_to_point(0)
result = np.array(origin) result = np.array(origin)

View file

@ -166,7 +166,7 @@ class Circle(Arc):
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
Arc.__init__(self, 2 * np.pi, **kwargs) Arc.__init__(self, TAU, **kwargs)
def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2): def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2):
# Ignores dim_to_match and stretch; result will always be a circle # Ignores dim_to_match and stretch; result will always be a circle

View file

@ -145,7 +145,7 @@ class Mobject(Container):
# Updating # Updating
def update(self, dt): def update(self, dt=0):
for updater in self.updaters: for updater in self.updaters:
num_args = get_num_args(updater) num_args = get_num_args(updater)
if num_args == 1: if num_args == 1:

View file

@ -10,6 +10,7 @@ import warnings
from tqdm import tqdm as ProgressDisplay from tqdm import tqdm as ProgressDisplay
import numpy as np import numpy as np
from pydub import AudioSegment
from manimlib.animation.animation import Animation from manimlib.animation.animation import Animation
from manimlib.animation.creation import Write from manimlib.animation.creation import Write
@ -35,7 +36,6 @@ class Scene(Container):
"frame_duration": LOW_QUALITY_FRAME_DURATION, "frame_duration": LOW_QUALITY_FRAME_DURATION,
"construct_args": [], "construct_args": [],
"skip_animations": False, "skip_animations": False,
"ignore_waits": False,
"write_to_movie": False, "write_to_movie": False,
"save_pngs": False, "save_pngs": False,
"pngs_mode": "RGBA", "pngs_mode": "RGBA",
@ -48,6 +48,7 @@ class Scene(Container):
"to_twitch": False, "to_twitch": False,
"twitch_key": None, "twitch_key": None,
"output_file_name": None, "output_file_name": None,
"leave_progress_bars": False,
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -66,17 +67,20 @@ class Scene(Container):
random.seed(self.random_seed) random.seed(self.random_seed)
np.random.seed(self.random_seed) np.random.seed(self.random_seed)
self.init_audio()
self.setup() self.setup()
if self.livestreaming: if self.livestreaming:
return None return None
try: try:
self.construct(*self.construct_args) self.construct(*self.construct_args)
except EndSceneEarlyException: except EndSceneEarlyException:
pass if hasattr(self, "writing_process"):
self.writing_process.terminate()
self.tear_down() self.tear_down()
if self.write_to_movie: if self.write_to_movie:
self.combine_movie_files() self.combine_movie_files()
self.print_end_message()
def handle_play_like_call(func): def handle_play_like_call(func):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
@ -117,6 +121,9 @@ class Scene(Container):
return self.output_file_name return self.output_file_name
return str(self) return str(self)
def print_end_message(self):
print("Played {} animations".format(self.num_plays))
def set_variables_as_attrs(self, *objects, **newly_named_objects): def set_variables_as_attrs(self, *objects, **newly_named_objects):
""" """
This method is slightly hacky, making it a little easier This method is slightly hacky, making it a little easier
@ -135,6 +142,38 @@ class Scene(Container):
def get_attrs(self, *keys): def get_attrs(self, *keys):
return [getattr(self, key) for key in keys] return [getattr(self, key) for key in keys]
# Sound
def init_audio(self):
self.includes_sound = False
def create_audio_segment(self):
self.audio_segment = AudioSegment.silent()
def add_audio_segment(self, new_segment, time_offset=0):
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
segment = self.audio_segment
overly_time = self.get_time() + time_offset
if overly_time < 0:
raise Exception("Adding sound at timestamp < 0")
curr_end = segment.duration_seconds
new_end = overly_time + new_segment.duration_seconds
diff = new_end - curr_end
if diff > 0:
segment = segment.append(
AudioSegment.silent(int(np.ceil(diff * 1000))),
crossfade=0,
)
self.audio_segment = segment.overlay(
new_segment, position=int(1000 * overly_time)
)
def add_sound(self, sound_file, time_offset=0):
new_segment = AudioSegment.from_file(sound_file)
self.add_audio_segment(new_segment, 0)
# Only these methods should touch the camera # Only these methods should touch the camera
def set_camera(self, camera): def set_camera(self, camera):
@ -386,20 +425,26 @@ class Scene(Container):
return mobjects[i:] return mobjects[i:]
return [] return []
def get_time_progression(self, run_time): def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
if self.skip_animations: if self.skip_animations and not override_skip_animations:
times = [run_time] times = [run_time]
else: else:
step = self.frame_duration step = self.frame_duration
times = np.arange(0, run_time, step) times = np.arange(0, run_time, step)
time_progression = ProgressDisplay(times) time_progression = ProgressDisplay(
times, total=n_iterations,
leave=self.leave_progress_bars,
)
return time_progression return time_progression
def get_run_time(self, animations):
return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations): def get_animation_time_progression(self, animations):
run_time = np.max([animation.run_time for animation in animations]) run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time) time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([ time_progression.set_description("".join([
"Animation %d: " % self.num_plays, "Animation {}: ".format(self.num_plays),
str(animations[0]), str(animations[0]),
(", etc." if len(animations) > 1 else ""), (", etc." if len(animations) > 1 else ""),
])) ]))
@ -498,20 +543,18 @@ class Scene(Container):
# have to be rendered every frame # have to be rendered every frame
self.update_frame(excluded_mobjects=moving_mobjects) self.update_frame(excluded_mobjects=moving_mobjects)
static_image = self.get_frame() static_image = self.get_frame()
total_run_time = 0
for t in self.get_animation_time_progression(animations): for t in self.get_animation_time_progression(animations):
for animation in animations: for animation in animations:
animation.update(t / animation.run_time) animation.update(t / animation.run_time)
self.continual_update(dt=t - total_run_time) self.continual_update(dt=self.frame_duration)
self.update_frame(moving_mobjects, static_image) self.update_frame(moving_mobjects, static_image)
self.add_frames(self.get_frame()) self.add_frames(self.get_frame())
total_run_time = t
self.mobjects_from_last_animation = [ self.mobjects_from_last_animation = [
anim.mobject for anim in animations anim.mobject for anim in animations
] ]
self.clean_up_animations(*animations) self.clean_up_animations(*animations)
if self.skip_animations: if self.skip_animations:
self.continual_update(total_run_time) self.continual_update(self.get_run_time(animations))
else: else:
self.continual_update(0) self.continual_update(0)
@ -542,15 +585,35 @@ class Scene(Container):
return self.mobjects_from_last_animation return self.mobjects_from_last_animation
return [] return []
def get_wait_time_progression(self, duration, stop_condition):
if stop_condition is not None:
time_progression = self.get_time_progression(
duration,
n_iterations=-1, # So it doesn't show % progress
override_skip_animations=True
)
time_progression.set_description(
"Waiting for {}".format(stop_condition.__name__)
)
else:
time_progression = self.get_time_progression(duration)
time_progression.set_description(
"Waiting {}".format(self.num_plays)
)
return time_progression
@handle_play_like_call @handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME): def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
if self.should_continually_update(): if self.should_continually_update():
total_time = 0 time_progression = self.get_wait_time_progression(duration, stop_condition)
for t in self.get_time_progression(duration): for t in time_progression:
self.continual_update(dt=t - total_time) self.continual_update(dt=self.frame_duration)
self.update_frame() self.update_frame()
self.add_frames(self.get_frame()) self.add_frames(self.get_frame())
total_time = t if stop_condition and stop_condition():
time_progression.close()
break
elif self.skip_animations: elif self.skip_animations:
# Do nothing # Do nothing
return self return self
@ -561,16 +624,8 @@ class Scene(Container):
self.add_frames(*[frame] * n_frames) self.add_frames(*[frame] * n_frames)
return self return self
def wait_to(self, time, assert_positive=True): def wait_until(self, stop_condition, max_time=60):
if self.ignore_waits: self.wait(max_time, stop_condition=stop_condition)
return
time -= self.get_time()
if assert_positive:
assert(time >= 0)
elif time < 0:
return
self.wait(time)
def force_skipping(self): def force_skipping(self):
self.original_skipping_status = self.skip_animations self.original_skipping_status = self.skip_animations
@ -647,7 +702,9 @@ class Scene(Container):
) )
) )
temp_file_path = file_path.replace(".", "_temp.") temp_file_path = file_path.replace(".", "_temp.")
self.args_to_rename_file = (temp_file_path, file_path)
self.movie_file_path = file_path
self.temp_movie_file_path = temp_file_path
fps = int(1 / self.frame_duration) fps = int(1 / self.frame_duration)
height = self.camera.get_pixel_height() height = self.camera.get_pixel_height()
@ -693,9 +750,13 @@ class Scene(Container):
self.writing_process.wait() self.writing_process.wait()
if self.livestreaming: if self.livestreaming:
return True return True
shutil.move(*self.args_to_rename_file) shutil.move(
self.temp_movie_file_path,
self.movie_file_path,
)
def combine_movie_files(self): def combine_movie_files(self):
# TODO, this could probably use a refactor
partial_movie_file_directory = self.get_partial_movie_directory() partial_movie_file_directory = self.get_partial_movie_directory()
kwargs = { kwargs = {
"remove_non_integer_files": True, "remove_non_integer_files": True,
@ -729,14 +790,43 @@ class Scene(Container):
'-safe', '0', '-safe', '0',
'-i', file_list, '-i', file_list,
'-c', 'copy', '-c', 'copy',
'-an', # Tells FFMPEG not to expect any audio
'-loglevel', 'error', '-loglevel', 'error',
movie_file_path movie_file_path
] ]
if not self.includes_sound:
commands.insert(-1, '-an')
subprocess.call(commands) subprocess.call(commands)
os.remove(file_list) os.remove(file_list)
print("File ready at {}".format(movie_file_path))
if self.includes_sound:
sound_file_path = movie_file_path.replace(
self.movie_file_extension, ".wav"
)
# Makes sure sound file length will match video file
self.add_audio_segment(AudioSegment.silent(0))
self.audio_segment.export(sound_file_path)
temp_file_path = movie_file_path.replace(".", "_temp.")
commands = commands = [
"ffmpeg",
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists
"-c:v", "copy", "-c:a", "aac",
'-loglevel', 'error',
"-shortest",
"-strict", "experimental",
temp_file_path,
]
subprocess.call(commands)
shutil.move(temp_file_path, movie_file_path)
# subprocess.call(["rm", self.temp_movie_file_path])
subprocess.call(["rm", sound_file_path])
print("\nAnimation ready at {}\n".format(movie_file_path))
# TODO, this doesn't belong in Scene, but should be
# part of some more specialized subclass optimized
# for livestreaming
def tex(self, latex): def tex(self, latex):
eq = TextMobject(latex) eq = TextMobject(latex)
anims = [] anims = []

View file

@ -527,9 +527,9 @@ class FunctionGInSymbols(Scene):
VGroup(seeking_text, g_equals_zero).shift, 1.5 * DOWN VGroup(seeking_text, g_equals_zero).shift, 1.5 * DOWN
) )
self.wait() self.wait()
self.play(CircleThenFadeAround(g_of_neg_p[2])) self.play(ShowCreationThenFadeAround(g_of_neg_p[2]))
self.wait() self.wait()
self.play(CircleThenFadeAround(neg_g_of_p)) self.play(ShowCreationThenFadeAround(neg_g_of_p))
self.wait() self.wait()
self.play(neg_g_of_p.restore) self.play(neg_g_of_p.restore)
rects = VGroup(*map(SurroundingRectangle, [f_of_p, f_of_neg_p])) rects = VGroup(*map(SurroundingRectangle, [f_of_p, f_of_neg_p]))

View file

@ -387,7 +387,7 @@ class ShowArrayOfEccentricities(Scene):
e_copy.set_color(RED) e_copy.set_color(RED)
self.play(ShowCreation(e_copy)) self.play(ShowCreation(e_copy))
self.play( self.play(
CircleThenFadeAround( ShowCreationThenFadeAround(
eccentricity_labels[i], eccentricity_labels[i],
), ),
FadeOut(e_copy) FadeOut(e_copy)

View file

@ -857,7 +857,7 @@ class CylinderModel(Scene):
self.wait() self.wait()
self.play( self.play(
movers.apply_complex_function, joukowsky_map, movers.apply_complex_function, joukowsky_map,
CircleThenFadeAround(self.func_label), ShowCreationThenFadeAround(self.func_label),
run_time=2 run_time=2
) )
self.add(self.get_stream_lines_animation(stream_lines)) self.add(self.get_stream_lines_animation(stream_lines))

View file

@ -310,9 +310,9 @@ class IntegralSymbols(Scene):
self.play(FadeInFrom(rhs, 4 * LEFT)) self.play(FadeInFrom(rhs, 4 * LEFT))
self.wait() self.wait()
self.play(CircleThenFadeAround(rhs[1])) self.play(ShowCreationThenFadeAround(rhs[1]))
self.wait() self.wait()
self.play(CircleThenFadeAround(rhs[2:])) self.play(ShowCreationThenFadeAround(rhs[2:]))
self.wait() self.wait()
self.play( self.play(
GrowFromCenter(int_brace), GrowFromCenter(int_brace),

View file

@ -3360,7 +3360,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities):
delta_t_numerator.scale, 1.5, {"about_edge": DOWN}, delta_t_numerator.scale, 1.5, {"about_edge": DOWN},
delta_t_numerator.set_color, YELLOW delta_t_numerator.set_color, YELLOW
) )
self.play(CircleThenFadeAround(prop_exp[:-2])) self.play(ShowCreationThenFadeAround(prop_exp[:-2]))
self.play( self.play(
delta_t_numerator.fade, 1, delta_t_numerator.fade, 1,
MoveToTarget(moving_R_squared), MoveToTarget(moving_R_squared),
@ -3447,7 +3447,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities):
polygon.set_fill(BLUE_E, opacity=0.8) polygon.set_fill(BLUE_E, opacity=0.8)
polygon.set_stroke(WHITE, 3) polygon.set_stroke(WHITE, 3)
self.play(CircleThenFadeAround(v1)) self.play(ShowCreationThenFadeAround(v1))
self.play( self.play(
MoveToTarget(v1), MoveToTarget(v1),
GrowFromCenter(root_dot), GrowFromCenter(root_dot),

View file

@ -4182,7 +4182,7 @@ class IntroduceQuaternions(Scene):
FadeInFromDown(number), FadeInFromDown(number),
Write(label), Write(label),
) )
self.play(CircleThenFadeAround( self.play(ShowCreationThenFadeAround(
number[2:], number[2:],
surrounding_rectangle_config={"color": BLUE} surrounding_rectangle_config={"color": BLUE}
)) ))
@ -4630,7 +4630,7 @@ class BreakUpQuaternionMultiplicationInParts(Scene):
) )
self.play( self.play(
randy.change, "confused", rotate_words, randy.change, "confused", rotate_words,
CircleThenFadeAround(rotate_words), ShowCreationThenFadeAround(rotate_words),
) )
self.play(LaggedStart( self.play(LaggedStart(
FadeInFrom, q_marks, FadeInFrom, q_marks,

View file

@ -1861,7 +1861,7 @@ class JustifyHeightSquish(MovingCameraScene):
)) ))
self.wait() self.wait()
self.play(ReplacementTransform(q_mark, alpha_label1)) self.play(ReplacementTransform(q_mark, alpha_label1))
self.play(CircleThenFadeAround( self.play(ShowCreationThenFadeAround(
equation, equation,
surrounding_rectangle_config={ surrounding_rectangle_config={
"buff": 0.015, "buff": 0.015,

View file

@ -1018,7 +1018,7 @@ class ShowNavierStokesEquations(Scene):
FadeInFromDown(labels[0]), FadeInFromDown(labels[0]),
newtons_second.next_to, variables, RIGHT, LARGE_BUFF newtons_second.next_to, variables, RIGHT, LARGE_BUFF
) )
self.play(CircleThenFadeAround(parts[0])) self.play(ShowCreationThenFadeAround(parts[0]))
self.wait() self.wait()
self.play(LaggedStart(FadeInFrom, labels[1:])) self.play(LaggedStart(FadeInFrom, labels[1:]))
self.wait(3) self.wait(3)

View file

@ -7,3 +7,4 @@ scipy==1.1.0
tqdm==4.24.0 tqdm==4.24.0
opencv-python==3.4.2.17 opencv-python==3.4.2.17
pycairo==1.17.1 pycairo==1.17.1
pydub==0.23.0