mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +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
|
||||
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"]
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue