mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
bfce8b47cd
23 changed files with 2630 additions and 200 deletions
23
Dockerfile
23
Dockerfile
|
@ -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"]
|
||||||
|
|
|
@ -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
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue