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

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

@ -113,6 +113,17 @@ class Write(DrawBorderThenFill):
else: else:
self.run_time = 2 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 # Fading
@ -203,6 +214,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

@ -392,7 +392,10 @@ class Camera(object):
vmobject vmobject
) )
ctx.set_line_width( 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() ctx.stroke_preserve()
return self return self

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

@ -298,7 +298,7 @@ class TeacherStudentsScene(PiCreatureScene):
"raise_left_hand", "raise_left_hand",
]) ])
kwargs["target_mode"] = target_mode 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( return self.pi_creature_says(
student, *content, **kwargs student, *content, **kwargs
) )
@ -309,7 +309,7 @@ class TeacherStudentsScene(PiCreatureScene):
) )
def student_thinks(self, *content, **kwargs): 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) return self.pi_creature_thinks(student, *content, **kwargs)
def change_all_student_modes(self, mode, **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 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_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) 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

@ -23,6 +23,7 @@ class Arc(VMobject):
"start_angle": 0, "start_angle": 0,
"num_anchors": 9, "num_anchors": 9,
"anchors_span_full_range": True, "anchors_span_full_range": True,
"arc_center": ORIGIN,
} }
def __init__(self, angle, **kwargs): def __init__(self, angle, **kwargs):
@ -50,6 +51,7 @@ class Arc(VMobject):
anchors, handles1, handles2 anchors, handles1, handles2
) )
self.scale(self.radius, about_point=ORIGIN) 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): def add_tip(self, tip_length=0.25, at_start=False, at_end=True):
# clear out any old tips # clear out any old tips
@ -166,7 +168,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
@ -180,6 +182,14 @@ class Circle(Arc):
np.sqrt(mobject.get_width()**2 + mobject.get_height()**2)) np.sqrt(mobject.get_width()**2 + mobject.get_height()**2))
self.scale(buffer_factor) 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): class Dot(Circle):
CONFIG = { CONFIG = {

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

@ -105,7 +105,10 @@ class DecimalNumber(VMobject):
full_config.update(self.initial_config) full_config.update(self.initial_config)
full_config.update(config) full_config.update(config)
new_decimal = DecimalNumber(number, **full_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.move_to(self, self.edge_to_fix)
new_decimal.match_style(self) new_decimal.match_style(self)
@ -129,3 +132,6 @@ class Integer(DecimalNumber):
def increment_value(self): def increment_value(self):
self.set_value(self.get_value() + 1) 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 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,
@ -732,19 +793,48 @@ 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')
combine_process = subprocess.Popen(commands) combine_process = subprocess.Popen(commands)
combine_process.wait() combine_process.wait()
for pf_path in partial_movie_files: for pf_path in partial_movie_files:
os.remove(pf_path) os.remove(pf_path)
os.remove(file_list) 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)) 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