Merge branch 'master' into master

This commit is contained in:
Grant Sanderson 2019-01-18 09:06:22 -08:00 committed by GitHub
commit bfce8b47cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 2630 additions and 200 deletions

View file

@ -1,37 +1,24 @@
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update -qqy
RUN apt-get install -qqy --no-install-recommends apt-utils
WORKDIR /root
RUN apt-get install -qqy build-essential libsqlite3-dev sqlite3 bzip2 \
libbz2-dev zlib1g-dev libssl-dev openssl libgdbm-dev \
libgdbm-compat-dev liblzma-dev libreadline-dev \
libncursesw5-dev libffi-dev uuid-dev
RUN apt-get install -qqy wget
RUN apt-get install --no-install-recommends -qqy build-essential libsqlite3-dev sqlite3 bzip2 \
libbz2-dev zlib1g-dev libssl-dev openssl libgdbm-dev \
libgdbm-compat-dev liblzma-dev libreadline-dev \
libncursesw5-dev libffi-dev uuid-dev wget ffmpeg apt-transport-https texlive-latex-base \
texlive-full texlive-fonts-extra sox git libcairo2-dev libjpeg-dev libgif-dev && rm -rf /var/lib/apt/lists/*
RUN wget -q https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz
RUN tar -xf Python-3.7.0.tgz
WORKDIR Python-3.7.0
RUN ./configure > /dev/null && make -s && make -s install
RUN python3 -m pip install --upgrade pip
RUN apt-get install -qqy libcairo2-dev libjpeg-dev libgif-dev
COPY requirements.txt requirements.txt
RUN python3 -m pip install -r requirements.txt
RUN rm requirements.txt
WORKDIR /root
RUN rm -rf Python-3.7.0*
RUN apt-get install -qqy ffmpeg
ENV TZ=America/Los_Angeles
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get install -qqy apt-transport-https
RUN apt-get install -qqy texlive-latex-base
RUN apt-get install -qqy texlive-full
RUN apt-get install -qqy texlive-fonts-extra
RUN apt-get install -qqy sox
RUN apt-get install -qqy git
ENV DEBIAN_FRONTEND teletype
ENTRYPOINT ["/bin/bash"]

View file

@ -3,34 +3,75 @@ import subprocess
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):
CONFIG = {
"block1_config": {
"mass": 1,
"velocity": -2,
"distance": 7,
"width": None,
"color": None,
"label_text": None,
"mass": 1e6,
"velocity": -2,
},
"block2_config": {
"distance": 3,
"mass": 1,
"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,
}
@ -54,30 +95,13 @@ class SlidingBlocks(VGroup):
if self.collect_clack_data:
self.clack_data = self.get_clack_data()
def get_block(self, mass, distance, velocity, width, color, label_text):
if width is None:
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)
def get_block(self, distance, **kwargs):
block = Block(**kwargs)
block.move_to(
self.floor.get_top()[1] * UP +
(self.wall.get_right()[0] + distance) * RIGHT,
DL,
)
label = block.label = TextMobject(label_text)
label.scale(0.8)
label.next_to(block, UP, SMALL_BUFF)
block.add(label)
return block
def get_phase_space_point_tracker(self):
@ -104,36 +128,6 @@ class SlidingBlocks(VGroup):
)
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):
block1, block2 = self.block1, self.block2
@ -199,23 +193,6 @@ class SlidingBlocks(VGroup):
clack_data.append((location, time))
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):
CONFIG = {
@ -225,6 +202,7 @@ class ClackFlashes(ContinualAnimation):
"flash_radius": 0.2,
},
"start_up_time": 0,
"min_time_between_flashes": 1 / 30,
}
def __init__(self, clack_data, **kwargs):
@ -233,7 +211,7 @@ class ClackFlashes(ContinualAnimation):
group = Group()
last_time = 0
for location, time in clack_data:
if (time - last_time) < MIN_TIME_BETWEEN_FLASHES:
if (time - last_time) < self.min_time_between_flashes:
continue
last_time = time
flash = Flash(location, **self.flash_config)
@ -256,6 +234,33 @@ class ClackFlashes(ContinualAnimation):
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):
CONFIG = {
"include_sound": True,
@ -268,6 +273,8 @@ class BlocksAndWallScene(Scene):
"counter_label": "\\# Collisions: ",
"collision_sound": "clack.wav",
"show_flash_animations": True,
"min_time_between_sounds": 0.004,
"allow_sound": True,
}
def setup(self):
@ -320,16 +327,10 @@ class BlocksAndWallScene(Scene):
self.counter_mob = counter_mob
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)
lines = VGroup(*[
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)
wall.to_edge(UP, buff=0)
return wall
def get_floor(self):
@ -359,7 +360,7 @@ class BlocksAndWallScene(Scene):
total_time = max(times) + 1
clacks = AudioSegment.silent(int(1000 * total_time))
last_position = 0
min_diff = int(1000 * MIN_TIME_BETWEEN_FLASHES)
min_diff = int(1000 * self.min_time_between_sounds)
for time in times:
position = int(1000 * time)
d_position = position - last_position
@ -583,8 +584,7 @@ class BlocksAndWallExample(BlocksAndWallScene):
CONFIG = {
"sliding_blocks_config": {
"block1_config": {
# "mass": 1e0,
"mass": 64,
"mass": 1e0,
"velocity": -2,
}
},
@ -927,7 +927,7 @@ class PiComputingAlgorithmsAxes(Scene):
lag_ratio=0.4,
))
self.wait()
self.play(CircleThenFadeAround(algorithms[-1][0]))
self.play(ShowCreationThenFadeAround(algorithms[-1][0]))
def get_machin_like_formula(self):
formula = TexMobject(
@ -1152,7 +1152,7 @@ class CompareToGalacticMass(Scene):
"velocity": -0.01,
"distance": 4.5,
"label_text": "$100^{(20 - 1)}$ kg",
"color": BLACK,
"fill_color": BLACK,
},
"block2_config": {
"distance": 1,

File diff suppressed because it is too large Load diff

View file

@ -113,6 +113,17 @@ class Write(DrawBorderThenFill):
else:
self.run_time = 2
class ShowIncreasingSubsets(Animation):
def __init__(self, group, **kwargs):
self.all_submobs = group.submobjects
Animation.__init__(self, group, **kwargs)
def update_mobject(self, alpha):
n_submobs = len(self.all_submobs)
index = int(alpha * n_submobs)
self.mobject.submobjects = self.all_submobs[:index]
# Fading
@ -203,6 +214,7 @@ class VFadeIn(Animation):
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
"""
def update_submobject(self, submobject, starting_submobject, alpha):
submobject.set_stroke(
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 there_and_back
from manimlib.utils.rate_functions import wiggle
from manimlib.utils.rate_functions import double_smooth
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):
CONFIG = {
"surrounding_rectangle_config": {},
@ -174,7 +189,7 @@ class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle):
}
class CircleThenFadeAround(AnimationOnSurroundingRectangle):
class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
CONFIG = {
"rect_to_animation": lambda rect: Succession(
ShowCreation, rect,

View file

@ -392,7 +392,10 @@ class Camera(object):
vmobject
)
ctx.set_line_width(
width * self.cairo_line_width_multiple
width * self.cairo_line_width_multiple *
# This ensures lines have constant width
# as you zoom in on them.
(self.get_frame_width() / FRAME_WIDTH)
)
ctx.stroke_preserve()
return self

View file

@ -19,32 +19,92 @@ def parse_cli():
)
parser.add_argument(
"scene_names",
nargs="+",
nargs="*",
help="Name of the Scene class you want to see",
)
optional_args = [
("-p", "--preview"),
("-w", "--write_to_movie"),
("-s", "--show_last_frame"),
("-l", "--low_quality"),
("-m", "--medium_quality"),
("-g", "--save_pngs"),
("-f", "--show_file_in_finder"),
("-t", "--transparent"),
("-q", "--quiet"),
("-a", "--write_all")
]
for short_arg, long_arg in optional_args:
parser.add_argument(short_arg, long_arg, action="store_true")
parser.add_argument("-o", "--output_file_name")
parser.add_argument("-n", "--start_at_animation_number")
parser.add_argument("-r", "--resolution")
parser.add_argument("-c", "--color")
parser.add_argument(
"-p", "--preview",
action="store_true",
help="Automatically open movie file once its done",
),
parser.add_argument(
"-w", "--write_to_movie",
action="store_true",
help="Render the scene as a movie file",
),
parser.add_argument(
"-s", "--show_last_frame",
action="store_true",
help="Save the last frame and open the image file",
),
parser.add_argument(
"-l", "--low_quality",
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(
"--sound",
action="store_true",
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(
"--livestream",
action="store_true",
@ -129,6 +189,7 @@ def get_configuration(args):
"start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None,
"sound": args.sound,
"leave_progress_bars": args.leave_progress_bars
}
# Camera configuration

View file

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

View file

@ -298,7 +298,7 @@ class TeacherStudentsScene(PiCreatureScene):
"raise_left_hand",
])
kwargs["target_mode"] = target_mode
student = self.get_students()[kwargs.get("student_index", 1)]
student = self.get_students()[kwargs.get("student_index", 2)]
return self.pi_creature_says(
student, *content, **kwargs
)
@ -309,7 +309,7 @@ class TeacherStudentsScene(PiCreatureScene):
)
def student_thinks(self, *content, **kwargs):
student = self.get_students()[kwargs.get("student_index", 1)]
student = self.get_students()[kwargs.get("student_index", 2)]
return self.pi_creature_thinks(student, *content, **kwargs)
def change_all_student_modes(self, mode, **kwargs):

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 VMobject
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
# TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene
@ -35,16 +36,21 @@ class Axes(VGroup):
def __init__(self, **kwargs):
VGroup.__init__(self, **kwargs)
self.x_axis = self.get_axis(self.x_min, self.x_max, self.x_axis_config)
self.y_axis = self.get_axis(self.y_min, self.y_max, self.y_axis_config)
self.y_axis.rotate(np.pi / 2, about_point=ORIGIN)
x_axis_config = merge_config([
self.x_axis_config,
{"x_min": self.x_min, "x_max": self.x_max},
self.number_line_config,
])
y_axis_config = merge_config([
self.y_axis_config,
{"x_min": self.y_min, "x_max": self.y_max},
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)
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):
origin = self.x_axis.number_to_point(0)
result = np.array(origin)

View file

@ -23,6 +23,7 @@ class Arc(VMobject):
"start_angle": 0,
"num_anchors": 9,
"anchors_span_full_range": True,
"arc_center": ORIGIN,
}
def __init__(self, angle, **kwargs):
@ -50,6 +51,7 @@ class Arc(VMobject):
anchors, handles1, handles2
)
self.scale(self.radius, about_point=ORIGIN)
self.shift(self.arc_center)
def add_tip(self, tip_length=0.25, at_start=False, at_end=True):
# clear out any old tips
@ -166,7 +168,7 @@ class Circle(Arc):
}
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):
# Ignores dim_to_match and stretch; result will always be a circle
@ -180,6 +182,14 @@ class Circle(Arc):
np.sqrt(mobject.get_width()**2 + mobject.get_height()**2))
self.scale(buffer_factor)
def get_point_from_angle(self, angle):
start_angle = angle_of_vector(
self.points[0] - self.get_center()
)
return self.point_from_proportion(
(angle - start_angle) / TAU
)
class Dot(Circle):
CONFIG = {

View file

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

View file

@ -105,7 +105,10 @@ class DecimalNumber(VMobject):
full_config.update(self.initial_config)
full_config.update(config)
new_decimal = DecimalNumber(number, **full_config)
new_decimal.match_height(self)
# new_decimal.match_height(self)
new_decimal.scale(
self[0].get_height() / new_decimal[0].get_height()
)
new_decimal.move_to(self, self.edge_to_fix)
new_decimal.match_style(self)
@ -129,3 +132,6 @@ class Integer(DecimalNumber):
def increment_value(self):
self.set_value(self.get_value() + 1)
def get_value(self):
return int(np.round(super().get_value()))

View file

@ -10,6 +10,7 @@ import warnings
from tqdm import tqdm as ProgressDisplay
import numpy as np
from pydub import AudioSegment
from manimlib.animation.animation import Animation
from manimlib.animation.creation import Write
@ -35,7 +36,6 @@ class Scene(Container):
"frame_duration": LOW_QUALITY_FRAME_DURATION,
"construct_args": [],
"skip_animations": False,
"ignore_waits": False,
"write_to_movie": False,
"save_pngs": False,
"pngs_mode": "RGBA",
@ -48,6 +48,7 @@ class Scene(Container):
"to_twitch": False,
"twitch_key": None,
"output_file_name": None,
"leave_progress_bars": False,
}
def __init__(self, **kwargs):
@ -66,17 +67,20 @@ class Scene(Container):
random.seed(self.random_seed)
np.random.seed(self.random_seed)
self.init_audio()
self.setup()
if self.livestreaming:
return None
try:
self.construct(*self.construct_args)
except EndSceneEarlyException:
pass
if hasattr(self, "writing_process"):
self.writing_process.terminate()
self.tear_down()
if self.write_to_movie:
self.combine_movie_files()
self.print_end_message()
def handle_play_like_call(func):
def wrapper(self, *args, **kwargs):
@ -117,6 +121,9 @@ class Scene(Container):
return self.output_file_name
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):
"""
This method is slightly hacky, making it a little easier
@ -135,6 +142,38 @@ class Scene(Container):
def get_attrs(self, *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
def set_camera(self, camera):
@ -386,20 +425,26 @@ class Scene(Container):
return mobjects[i:]
return []
def get_time_progression(self, run_time):
if self.skip_animations:
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
if self.skip_animations and not override_skip_animations:
times = [run_time]
else:
step = self.frame_duration
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
def get_run_time(self, animations):
return np.max([animation.run_time for animation in 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.set_description("".join([
"Animation %d: " % self.num_plays,
"Animation {}: ".format(self.num_plays),
str(animations[0]),
(", etc." if len(animations) > 1 else ""),
]))
@ -498,20 +543,18 @@ class Scene(Container):
# have to be rendered every frame
self.update_frame(excluded_mobjects=moving_mobjects)
static_image = self.get_frame()
total_run_time = 0
for t in self.get_animation_time_progression(animations):
for animation in animations:
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.add_frames(self.get_frame())
total_run_time = t
self.mobjects_from_last_animation = [
anim.mobject for anim in animations
]
self.clean_up_animations(*animations)
if self.skip_animations:
self.continual_update(total_run_time)
self.continual_update(self.get_run_time(animations))
else:
self.continual_update(0)
@ -542,15 +585,35 @@ class Scene(Container):
return self.mobjects_from_last_animation
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
def wait(self, duration=DEFAULT_WAIT_TIME):
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
if self.should_continually_update():
total_time = 0
for t in self.get_time_progression(duration):
self.continual_update(dt=t - total_time)
time_progression = self.get_wait_time_progression(duration, stop_condition)
for t in time_progression:
self.continual_update(dt=self.frame_duration)
self.update_frame()
self.add_frames(self.get_frame())
total_time = t
if stop_condition and stop_condition():
time_progression.close()
break
elif self.skip_animations:
# Do nothing
return self
@ -561,16 +624,8 @@ class Scene(Container):
self.add_frames(*[frame] * n_frames)
return self
def wait_to(self, time, assert_positive=True):
if self.ignore_waits:
return
time -= self.get_time()
if assert_positive:
assert(time >= 0)
elif time < 0:
return
self.wait(time)
def wait_until(self, stop_condition, max_time=60):
self.wait(max_time, stop_condition=stop_condition)
def force_skipping(self):
self.original_skipping_status = self.skip_animations
@ -647,7 +702,9 @@ class Scene(Container):
)
)
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)
height = self.camera.get_pixel_height()
@ -693,9 +750,13 @@ class Scene(Container):
self.writing_process.wait()
if self.livestreaming:
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):
# TODO, this could probably use a refactor
partial_movie_file_directory = self.get_partial_movie_directory()
kwargs = {
"remove_non_integer_files": True,
@ -732,19 +793,48 @@ class Scene(Container):
'-safe', '0',
'-i', file_list,
'-c', 'copy',
'-an', # Tells FFMPEG not to expect any audio
'-loglevel', 'error',
movie_file_path
]
if not self.includes_sound:
commands.insert(-1, '-an')
combine_process = subprocess.Popen(commands)
combine_process.wait()
for pf_path in partial_movie_files:
os.remove(pf_path)
os.remove(file_list)
os.rmdir(partial_movie_file_directory)
os.rmdir(os.path.join(partial_movie_file_directory, os.path.pardir))
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):
eq = TextMobject(latex)
anims = []

View file

@ -527,9 +527,9 @@ class FunctionGInSymbols(Scene):
VGroup(seeking_text, g_equals_zero).shift, 1.5 * DOWN
)
self.wait()
self.play(CircleThenFadeAround(g_of_neg_p[2]))
self.play(ShowCreationThenFadeAround(g_of_neg_p[2]))
self.wait()
self.play(CircleThenFadeAround(neg_g_of_p))
self.play(ShowCreationThenFadeAround(neg_g_of_p))
self.wait()
self.play(neg_g_of_p.restore)
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)
self.play(ShowCreation(e_copy))
self.play(
CircleThenFadeAround(
ShowCreationThenFadeAround(
eccentricity_labels[i],
),
FadeOut(e_copy)

View file

@ -857,7 +857,7 @@ class CylinderModel(Scene):
self.wait()
self.play(
movers.apply_complex_function, joukowsky_map,
CircleThenFadeAround(self.func_label),
ShowCreationThenFadeAround(self.func_label),
run_time=2
)
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.wait()
self.play(CircleThenFadeAround(rhs[1]))
self.play(ShowCreationThenFadeAround(rhs[1]))
self.wait()
self.play(CircleThenFadeAround(rhs[2:]))
self.play(ShowCreationThenFadeAround(rhs[2:]))
self.wait()
self.play(
GrowFromCenter(int_brace),

View file

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

View file

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

View file

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

View file

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

View file

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