mirror of
https://github.com/3b1b/manim.git
synced 2025-09-01 00:48:45 +00:00
Merge pull request #418 from 3b1b/scene-file-writer-refactor
Scene file writer refactor
This commit is contained in:
commit
503ede97af
21 changed files with 1439 additions and 548 deletions
|
@ -60,7 +60,7 @@ Set MEDIA_DIR environment variable to determine where image and animation files
|
||||||
|
|
||||||
Look through the old_projects folder to see the code for previous 3b1b videos. Note, however, that developments are often made to the library without considering backwards compatibility on those old_projects. To run them with a guarantee that they will work, you will have to go back to the commit which complete that project.
|
Look through the old_projects folder to see the code for previous 3b1b videos. Note, however, that developments are often made to the library without considering backwards compatibility on those old_projects. To run them with a guarantee that they will work, you will have to go back to the commit which complete that project.
|
||||||
|
|
||||||
While developing a scene, the `-s` flag is helpful to just see what things look like at the end without having to generate the full animation. It can also be helpful to use the `-n` flag to skip over some number of animations.
|
While developing a scene, the `-sp` flags are helpful to just see what things look like at the end without having to generate the full animation. It can also be helpful to use the `-n` flag to skip over some number of animations.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
Documentation is in progress at [manim.readthedocs.io](https://manim.readthedocs.io).
|
Documentation is in progress at [manim.readthedocs.io](https://manim.readthedocs.io).
|
||||||
|
|
8
active_projects/clacks/all_questions_scenes.py
Normal file
8
active_projects/clacks/all_questions_scenes.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from active_projects import clacks
|
||||||
|
|
||||||
|
output_directory = "clacks_question"
|
||||||
|
all_scenes = [
|
||||||
|
clacks.NameIntro,
|
||||||
|
clacks.MathAndPhysicsConspiring,
|
||||||
|
clacks.LightBouncing,
|
||||||
|
]
|
84
active_projects/clacks/name_bump.py
Normal file
84
active_projects/clacks/name_bump.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
from big_ol_pile_of_manim_imports import *
|
||||||
|
|
||||||
|
from active_projects.clacks.question import BlocksAndWallExample
|
||||||
|
|
||||||
|
|
||||||
|
class NameBump(BlocksAndWallExample):
|
||||||
|
CONFIG = {
|
||||||
|
"name": "Magnus Lysfjord",
|
||||||
|
"sliding_blocks_config": {
|
||||||
|
"block1_config": {
|
||||||
|
"mass": 1e6,
|
||||||
|
"velocity": -0.5,
|
||||||
|
"distance": 7,
|
||||||
|
},
|
||||||
|
"block2_config": {},
|
||||||
|
},
|
||||||
|
"wait_time": 25,
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
names = self.name.split(" ")
|
||||||
|
n = len(names)
|
||||||
|
if n == 1:
|
||||||
|
names = 2 * [names[0]]
|
||||||
|
elif n > 2:
|
||||||
|
names = [
|
||||||
|
" ".join(names[:n // 2]),
|
||||||
|
" ".join(names[n // 2:]),
|
||||||
|
]
|
||||||
|
# Swap, to show first name on the left
|
||||||
|
names = [names[1], names[0]]
|
||||||
|
|
||||||
|
name_mobs = VGroup(*map(TextMobject, names))
|
||||||
|
name_mobs.set_stroke(BLACK, 3, background=True)
|
||||||
|
name_mobs.set_fill(LIGHT_GREY, 1)
|
||||||
|
name_mobs.set_sheen(3, UL)
|
||||||
|
name_mobs.scale(2)
|
||||||
|
configs = [
|
||||||
|
self.sliding_blocks_config["block1_config"],
|
||||||
|
self.sliding_blocks_config["block2_config"],
|
||||||
|
]
|
||||||
|
for name_mob, config in zip(name_mobs, configs):
|
||||||
|
config["width"] = name_mob.get_width()
|
||||||
|
self.name_mobs = name_mobs
|
||||||
|
|
||||||
|
super().setup()
|
||||||
|
|
||||||
|
def add_blocks(self):
|
||||||
|
super().add_blocks()
|
||||||
|
blocks = self.blocks
|
||||||
|
name_mobs = self.name_mobs
|
||||||
|
|
||||||
|
blocks.fade(1)
|
||||||
|
|
||||||
|
def update_name_mobs(name_mobs):
|
||||||
|
for name_mob, block in zip(name_mobs, self.blocks):
|
||||||
|
name_mob.move_to(block)
|
||||||
|
target_y = block.get_bottom()[1] + SMALL_BUFF
|
||||||
|
curr_y = name_mob[0].get_bottom()[1]
|
||||||
|
name_mob.shift((target_y - curr_y) * UP)
|
||||||
|
|
||||||
|
name_mobs.add_updater(update_name_mobs)
|
||||||
|
self.add(name_mobs)
|
||||||
|
|
||||||
|
clack_y = self.name_mobs[1].get_center()[1]
|
||||||
|
for location, time in self.clack_data:
|
||||||
|
location[1] = clack_y
|
||||||
|
|
||||||
|
for block, name_mob in zip(blocks, name_mobs):
|
||||||
|
block.label.next_to(name_mob, UP)
|
||||||
|
block.label.set_fill(YELLOW, opacity=1)
|
||||||
|
|
||||||
|
|
||||||
|
# for name in names:
|
||||||
|
# file_name = name.replace(".", "")
|
||||||
|
# file_name += " Name Bump"
|
||||||
|
# scene = NameBump(
|
||||||
|
# name=name,
|
||||||
|
# write_to_movie=True,
|
||||||
|
# output_file_name=file_name,
|
||||||
|
# camera_config=PRODUCTION_QUALITY_CAMERA_CONFIG,
|
||||||
|
# frame_duration=PRODUCTION_QUALITY_FRAME_DURATION,
|
||||||
|
# )
|
|
@ -27,6 +27,8 @@ class Block(Square):
|
||||||
self.fill_color = self.mass_to_color(self.mass)
|
self.fill_color = self.mass_to_color(self.mass)
|
||||||
if self.label_text is None:
|
if self.label_text is None:
|
||||||
self.label_text = self.mass_to_label_text(self.mass)
|
self.label_text = self.mass_to_label_text(self.mass)
|
||||||
|
if "width" in kwargs:
|
||||||
|
kwargs.pop("width")
|
||||||
Square.__init__(self, side_length=self.width, **kwargs)
|
Square.__init__(self, side_length=self.width, **kwargs)
|
||||||
self.label = self.get_label()
|
self.label = self.get_label()
|
||||||
self.add(self.label)
|
self.add(self.label)
|
||||||
|
@ -130,8 +132,8 @@ class SlidingBlocks(VGroup):
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
ps_point = self.phase_space_point_tracker.get_location()
|
ps_point = self.phase_space_point_tracker.get_location()
|
||||||
|
|
||||||
theta = np.arctan(np.sqrt(self.mass_ratio))
|
theta = np.arctan(np.sqrt(self.mass_ratio))
|
||||||
ps_point_angle = angle_of_vector(ps_point)
|
ps_point_angle = angle_of_vector(ps_point)
|
||||||
n_clacks = int(ps_point_angle / theta)
|
n_clacks = int(ps_point_angle / theta)
|
||||||
|
@ -274,7 +276,6 @@ class BlocksAndWallScene(Scene):
|
||||||
"collision_sound": "clack.wav",
|
"collision_sound": "clack.wav",
|
||||||
"show_flash_animations": True,
|
"show_flash_animations": True,
|
||||||
"min_time_between_sounds": 0.004,
|
"min_time_between_sounds": 0.004,
|
||||||
"allow_sound": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
|
@ -345,10 +346,7 @@ class BlocksAndWallScene(Scene):
|
||||||
self.counter_mob.set_value(n_clacks)
|
self.counter_mob.set_value(n_clacks)
|
||||||
|
|
||||||
def create_sound_file(self, clack_data):
|
def create_sound_file(self, clack_data):
|
||||||
directory = get_scene_output_directory(BlocksAndWallScene)
|
clack_file = os.path.join(SOUND_DIR, self.collision_sound)
|
||||||
clack_file = os.path.join(
|
|
||||||
directory, 'sounds', self.collision_sound,
|
|
||||||
)
|
|
||||||
output_file = self.get_movie_file_path(extension='.wav')
|
output_file = self.get_movie_file_path(extension='.wav')
|
||||||
times = [
|
times = [
|
||||||
time
|
time
|
||||||
|
@ -377,6 +375,8 @@ class BlocksAndWallScene(Scene):
|
||||||
clacks.export(output_file, format="wav")
|
clacks.export(output_file, format="wav")
|
||||||
return output_file
|
return output_file
|
||||||
|
|
||||||
|
# TODO, this no longer works
|
||||||
|
# should use Scene.add_sound instead
|
||||||
def combine_movie_files(self):
|
def combine_movie_files(self):
|
||||||
Scene.combine_movie_files(self)
|
Scene.combine_movie_files(self)
|
||||||
if self.include_sound:
|
if self.include_sound:
|
||||||
|
@ -848,9 +848,14 @@ class BlocksAndWallExampleMass1e10(BlocksAndWallExample):
|
||||||
|
|
||||||
|
|
||||||
class DigitsOfPi(Scene):
|
class DigitsOfPi(Scene):
|
||||||
|
CONFIG = {"n_digits": 9}
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
|
nd = self.n_digits
|
||||||
|
pow10 = int(10**nd)
|
||||||
|
rounded_pi = int(pow10 * PI) / pow10
|
||||||
equation = TexMobject(
|
equation = TexMobject(
|
||||||
"\\pi = 3.14159265..."
|
("\\pi = {:." + str(nd) + "f}...").format(rounded_pi)
|
||||||
)
|
)
|
||||||
equation.set_color(YELLOW)
|
equation.set_color(YELLOW)
|
||||||
pi_creature = Randolph(color=YELLOW)
|
pi_creature = Randolph(color=YELLOW)
|
||||||
|
@ -858,9 +863,11 @@ class DigitsOfPi(Scene):
|
||||||
pi_creature.scale(1.4)
|
pi_creature.scale(1.4)
|
||||||
pi_creature.move_to(equation[0], DOWN)
|
pi_creature.move_to(equation[0], DOWN)
|
||||||
self.add(pi_creature, equation[1])
|
self.add(pi_creature, equation[1])
|
||||||
for digit in equation[2:]:
|
self.play(ShowIncreasingSubsets(
|
||||||
self.add(digit)
|
equation[2:],
|
||||||
self.wait(0.1)
|
rate_func=None,
|
||||||
|
run_time=1,
|
||||||
|
))
|
||||||
self.play(Blink(pi_creature))
|
self.play(Blink(pi_creature))
|
||||||
self.wait()
|
self.wait()
|
||||||
|
|
||||||
|
@ -1553,7 +1560,7 @@ class EndScreen(Scene):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Thumbnail(BlocksAndWallExample):
|
class Thumbnail(BlocksAndWallExample, MovingCameraScene):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"sliding_blocks_config": {
|
"sliding_blocks_config": {
|
||||||
"block1_config": {
|
"block1_config": {
|
||||||
|
@ -1565,19 +1572,34 @@ class Thumbnail(BlocksAndWallExample):
|
||||||
"wait_time": 0,
|
"wait_time": 0,
|
||||||
"count_clacks": False,
|
"count_clacks": False,
|
||||||
"show_flash_animations": False,
|
"show_flash_animations": False,
|
||||||
"floor_y": -3,
|
"floor_y": -3.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
MovingCameraScene.setup(self)
|
||||||
|
BlocksAndWallExample.setup(self)
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
|
self.camera_frame.shift(0.9 * UP)
|
||||||
|
self.thicken_lines()
|
||||||
|
self.grow_labels()
|
||||||
|
self.add_vector()
|
||||||
|
self.add_text()
|
||||||
|
|
||||||
|
def thicken_lines(self):
|
||||||
self.floor.set_stroke(WHITE, 10)
|
self.floor.set_stroke(WHITE, 10)
|
||||||
self.wall.set_stroke(WHITE, 10)
|
self.wall.set_stroke(WHITE, 10)
|
||||||
self.wall[1:].set_stroke(WHITE, 4)
|
self.wall[1:].set_stroke(WHITE, 4)
|
||||||
|
|
||||||
|
def grow_labels(self):
|
||||||
blocks = self.blocks
|
blocks = self.blocks
|
||||||
for block in blocks.block1, blocks.block2:
|
for block in blocks.block1, blocks.block2:
|
||||||
block.remove(block.label)
|
block.remove(block.label)
|
||||||
block.label.scale(2.5, about_point=block.get_top())
|
block.label.scale(2.5, about_point=block.get_top())
|
||||||
self.add(block.label)
|
self.add(block.label)
|
||||||
|
|
||||||
|
def add_vector(self):
|
||||||
|
blocks = self.blocks
|
||||||
arrow = Vector(
|
arrow = Vector(
|
||||||
2.5 * LEFT,
|
2.5 * LEFT,
|
||||||
color=RED,
|
color=RED,
|
||||||
|
@ -1590,9 +1612,10 @@ class Thumbnail(BlocksAndWallExample):
|
||||||
)
|
)
|
||||||
self.add(arrow)
|
self.add(arrow)
|
||||||
|
|
||||||
|
def add_text(self):
|
||||||
question = TextMobject("How many\\\\collisions?")
|
question = TextMobject("How many\\\\collisions?")
|
||||||
question.scale(2.5)
|
question.scale(2.5)
|
||||||
question.to_edge(UP)
|
question.to_edge(UP)
|
||||||
question.set_color(YELLOW)
|
question.set_color(YELLOW)
|
||||||
question.set_stroke(RED, 2, background=True)
|
question.set_stroke(RED, 2, background=True)
|
||||||
self.add(question)
|
self.add(question)
|
File diff suppressed because it is too large
Load diff
|
@ -88,7 +88,7 @@ from manimlib.utils.color import *
|
||||||
from manimlib.utils.config_ops import *
|
from manimlib.utils.config_ops import *
|
||||||
from manimlib.utils.images import *
|
from manimlib.utils.images import *
|
||||||
from manimlib.utils.iterables import *
|
from manimlib.utils.iterables import *
|
||||||
from manimlib.utils.output_directory_getters import *
|
from manimlib.utils.file_ops import *
|
||||||
from manimlib.utils.paths import *
|
from manimlib.utils.paths import *
|
||||||
from manimlib.utils.rate_functions import *
|
from manimlib.utils.rate_functions import *
|
||||||
from manimlib.utils.simple_functions import *
|
from manimlib.utils.simple_functions import *
|
||||||
|
|
|
@ -7,8 +7,9 @@ from big_ol_pile_of_manim_imports import *
|
||||||
#
|
#
|
||||||
# Use the flat -l for a faster rendering at a lower
|
# Use the flat -l for a faster rendering at a lower
|
||||||
# quality.
|
# quality.
|
||||||
# Use -s to skip to the end and just show the final frame
|
# Use -s to skip to the end and just save the final frame
|
||||||
# Use the -p to have the animation pop up once done.
|
# Use the -p to have the animation (or image, if -s was
|
||||||
|
# used) pop up once done.
|
||||||
# Use -n <number> to skip ahead to the n'th animation of a scene.
|
# Use -n <number> to skip ahead to the n'th animation of a scene.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -246,11 +246,10 @@ class GrowFromPoint(Transform):
|
||||||
def __init__(self, mobject, point, **kwargs):
|
def __init__(self, mobject, point, **kwargs):
|
||||||
digest_config(self, kwargs)
|
digest_config(self, kwargs)
|
||||||
target = mobject.copy()
|
target = mobject.copy()
|
||||||
point_mob = VectorizedPoint(point)
|
mobject.scale(0)
|
||||||
|
mobject.move_to(point)
|
||||||
if self.point_color:
|
if self.point_color:
|
||||||
point_mob.set_color(self.point_color)
|
mobject.set_color(self.point_color)
|
||||||
mobject.replace(point_mob)
|
|
||||||
mobject.set_color(point_mob.get_color())
|
|
||||||
Transform.__init__(self, mobject, target, **kwargs)
|
Transform.__init__(self, mobject, target, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -191,10 +191,7 @@ class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle):
|
||||||
|
|
||||||
class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
|
class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"rect_to_animation": lambda rect: Succession(
|
"rect_to_animation": ShowCreationThenFadeOut
|
||||||
ShowCreation, rect,
|
|
||||||
FadeOut, rect,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ def parse_cli():
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p", "--preview",
|
"-p", "--preview",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Automatically open movie file once its done",
|
help="Automatically open the saved file once its done",
|
||||||
),
|
),
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-w", "--write_to_movie",
|
"-w", "--write_to_movie",
|
||||||
|
@ -33,9 +33,9 @@ def parse_cli():
|
||||||
help="Render the scene as a movie file",
|
help="Render the scene as a movie file",
|
||||||
),
|
),
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s", "--show_last_frame",
|
"-s", "--save_last_frame",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Save the last frame and open the image file",
|
help="Save the last frame",
|
||||||
),
|
),
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-l", "--low_quality",
|
"-l", "--low_quality",
|
||||||
|
@ -73,8 +73,7 @@ def parse_cli():
|
||||||
help="Write all the scenes from a file",
|
help="Write all the scenes from a file",
|
||||||
),
|
),
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o", "--output_file_name",
|
"-o", "--file_name",
|
||||||
nargs=1,
|
|
||||||
help="Specify the name of the output file, if"
|
help="Specify the name of the output file, if"
|
||||||
"it should be different from the scene class name",
|
"it should be different from the scene class name",
|
||||||
)
|
)
|
||||||
|
@ -156,36 +155,25 @@ def get_module(file_name):
|
||||||
|
|
||||||
|
|
||||||
def get_configuration(args):
|
def get_configuration(args):
|
||||||
if args.output_file_name is not None:
|
file_writer_config = {
|
||||||
output_file_name_root, output_file_name_ext = os.path.splitext(
|
# By default, write to file
|
||||||
args.output_file_name)
|
"write_to_movie": args.write_to_movie or not args.save_last_frame,
|
||||||
expected_ext = '.png' if args.show_last_frame else '.mp4'
|
"save_last_frame": args.save_last_frame,
|
||||||
if output_file_name_ext not in ['', expected_ext]:
|
"save_pngs": args.save_pngs,
|
||||||
print("WARNING: The output will be to (doubly-dotted) %s%s" %
|
# If -t is passed in (for transparent), this will be RGBA
|
||||||
output_file_name_root % expected_ext)
|
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||||
output_file_name = args.output_file_name
|
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
||||||
else:
|
"file_name": args.file_name,
|
||||||
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
|
}
|
||||||
output_file_name = output_file_name_root
|
|
||||||
else:
|
|
||||||
output_file_name = args.output_file_name
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"module": get_module(args.file),
|
"module": get_module(args.file),
|
||||||
"scene_names": args.scene_names,
|
"scene_names": args.scene_names,
|
||||||
"open_video_upon_completion": args.preview,
|
"open_video_upon_completion": args.preview,
|
||||||
"show_file_in_finder": args.show_file_in_finder,
|
"show_file_in_finder": args.show_file_in_finder,
|
||||||
# By default, write to file
|
"file_writer_config": file_writer_config,
|
||||||
"write_to_movie": args.write_to_movie or not args.show_last_frame,
|
|
||||||
"show_last_frame": args.show_last_frame,
|
|
||||||
"save_pngs": args.save_pngs,
|
|
||||||
# If -t is passed in (for transparent), this will be RGBA
|
|
||||||
"saved_image_mode": "RGBA" if args.transparent else "RGB",
|
|
||||||
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
|
||||||
"quiet": args.quiet or args.write_all,
|
"quiet": args.quiet or args.write_all,
|
||||||
"ignore_waits": args.preview,
|
"ignore_waits": args.preview,
|
||||||
"write_all": args.write_all,
|
"write_all": args.write_all,
|
||||||
"output_file_name": output_file_name,
|
|
||||||
"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,
|
||||||
|
@ -242,7 +230,7 @@ def get_configuration(args):
|
||||||
config["start_at_animation_number"] = int(stan)
|
config["start_at_animation_number"] = int(stan)
|
||||||
|
|
||||||
config["skip_animations"] = any([
|
config["skip_animations"] = any([
|
||||||
config["show_last_frame"] and not config["write_to_movie"],
|
file_writer_config["save_last_frame"],
|
||||||
config["start_at_animation_number"],
|
config["start_at_animation_number"],
|
||||||
])
|
])
|
||||||
return config
|
return config
|
||||||
|
|
|
@ -25,6 +25,7 @@ with open("media_dir.txt", 'w') as media_file:
|
||||||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||||
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
||||||
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
||||||
|
SOUND_DIR = os.path.join(MEDIA_DIR, "designs", "sounds")
|
||||||
###
|
###
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
FILE_DIR = os.path.join(THIS_DIR, "files")
|
FILE_DIR = os.path.join(THIS_DIR, "files")
|
||||||
|
|
|
@ -12,46 +12,43 @@ from manimlib.utils.sounds import play_finish_sound
|
||||||
import manimlib.constants
|
import manimlib.constants
|
||||||
|
|
||||||
|
|
||||||
def handle_scene(scene, **config):
|
def open_file_if_needed(file_writer, **config):
|
||||||
if config["quiet"]:
|
if config["quiet"]:
|
||||||
curr_stdout = sys.stdout
|
curr_stdout = sys.stdout
|
||||||
sys.stdout = open(os.devnull, "w")
|
sys.stdout = open(os.devnull, "w")
|
||||||
|
|
||||||
if config["show_last_frame"]:
|
|
||||||
scene.save_image(mode=config["saved_image_mode"])
|
|
||||||
open_file = any([
|
open_file = any([
|
||||||
config["show_last_frame"],
|
|
||||||
config["open_video_upon_completion"],
|
config["open_video_upon_completion"],
|
||||||
config["show_file_in_finder"]
|
config["show_file_in_finder"]
|
||||||
])
|
])
|
||||||
if open_file:
|
if open_file:
|
||||||
current_os = platform.system()
|
current_os = platform.system()
|
||||||
file_path = None
|
file_paths = []
|
||||||
|
|
||||||
if config["show_last_frame"]:
|
if config["file_writer_config"]["save_last_frame"]:
|
||||||
file_path = scene.get_image_file_path()
|
file_paths.append(file_writer.get_image_file_path())
|
||||||
else:
|
if config["file_writer_config"]["write_to_movie"]:
|
||||||
file_path = scene.get_movie_file_path()
|
file_paths.append(file_writer.get_movie_file_path())
|
||||||
|
|
||||||
if current_os == "Windows":
|
for file_path in file_paths:
|
||||||
os.startfile(file_path)
|
if current_os == "Windows":
|
||||||
else:
|
os.startfile(file_path)
|
||||||
commands = []
|
else:
|
||||||
|
commands = []
|
||||||
|
if (current_os == "Linux"):
|
||||||
|
commands.append("xdg-open")
|
||||||
|
else: # Assume macOS
|
||||||
|
commands.append("open")
|
||||||
|
|
||||||
if (current_os == "Linux"):
|
if config["show_file_in_finder"]:
|
||||||
commands.append("xdg-open")
|
commands.append("-R")
|
||||||
else: # Assume macOS
|
|
||||||
commands.append("open")
|
|
||||||
|
|
||||||
if config["show_file_in_finder"]:
|
commands.append(file_path)
|
||||||
commands.append("-R")
|
|
||||||
|
|
||||||
commands.append(file_path)
|
# commands.append("-g")
|
||||||
|
FNULL = open(os.devnull, 'w')
|
||||||
# commands.append("-g")
|
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
||||||
FNULL = open(os.devnull, 'w')
|
FNULL.close()
|
||||||
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
|
||||||
FNULL.close()
|
|
||||||
|
|
||||||
if config["quiet"]:
|
if config["quiet"]:
|
||||||
sys.stdout.close()
|
sys.stdout.close()
|
||||||
|
@ -128,23 +125,18 @@ def main(config):
|
||||||
"camera_config",
|
"camera_config",
|
||||||
"frame_duration",
|
"frame_duration",
|
||||||
"skip_animations",
|
"skip_animations",
|
||||||
"write_to_movie",
|
"file_writer_config",
|
||||||
"save_pngs",
|
|
||||||
"movie_file_extension",
|
|
||||||
"start_at_animation_number",
|
"start_at_animation_number",
|
||||||
"end_at_animation_number",
|
"end_at_animation_number",
|
||||||
"output_file_name",
|
|
||||||
"leave_progress_bars",
|
"leave_progress_bars",
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
if config["save_pngs"]:
|
|
||||||
print("We are going to save a PNG sequence as well...")
|
|
||||||
scene_kwargs["save_pngs"] = True
|
|
||||||
scene_kwargs["pngs_mode"] = config["saved_image_mode"]
|
|
||||||
|
|
||||||
for SceneClass in get_scene_classes(scene_names_to_classes, config):
|
for SceneClass in get_scene_classes(scene_names_to_classes, config):
|
||||||
try:
|
try:
|
||||||
handle_scene(SceneClass(**scene_kwargs), **config)
|
# By invoking, this renders the full scene
|
||||||
|
scene = SceneClass(**scene_kwargs)
|
||||||
|
open_file_if_needed(scene.file_writer, **config)
|
||||||
if config["sound"]:
|
if config["sound"]:
|
||||||
play_finish_sound()
|
play_finish_sound()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -293,3 +293,82 @@ class TODOStub(Scene):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
self.add(TextMobject("TODO: %s" % self.message))
|
self.add(TextMobject("TODO: %s" % self.message))
|
||||||
self.wait()
|
self.wait()
|
||||||
|
|
||||||
|
|
||||||
|
class Banner(Scene):
|
||||||
|
CONFIG = {
|
||||||
|
"camera_config": {
|
||||||
|
"pixel_height": 1440,
|
||||||
|
"pixel_width": 2560,
|
||||||
|
},
|
||||||
|
"pi_height": 1.25,
|
||||||
|
"pi_bottom": 0.25 * DOWN,
|
||||||
|
"use_date": False,
|
||||||
|
"date": "Sunday, February 3rd",
|
||||||
|
"message_scale_val": 0.9,
|
||||||
|
"add_supporter_note": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# Force these dimensions
|
||||||
|
self.camera_config = {
|
||||||
|
"pixel_height": 1440,
|
||||||
|
"pixel_width": 2560,
|
||||||
|
}
|
||||||
|
Scene.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
def construct(self):
|
||||||
|
pis = self.get_pis()
|
||||||
|
pis.set_height(self.pi_height)
|
||||||
|
pis.arrange_submobjects(RIGHT, aligned_edge=DOWN)
|
||||||
|
pis.move_to(self.pi_bottom, DOWN)
|
||||||
|
self.add(pis)
|
||||||
|
|
||||||
|
if self.use_date:
|
||||||
|
message = self.get_date_message()
|
||||||
|
else:
|
||||||
|
message = self.get_probabalistic_message()
|
||||||
|
message.scale(self.message_scale_val)
|
||||||
|
message.next_to(pis, DOWN)
|
||||||
|
self.add(message)
|
||||||
|
|
||||||
|
if self.add_supporter_note:
|
||||||
|
note = self.get_supporter_note()
|
||||||
|
note.scale(0.5)
|
||||||
|
message.shift((MED_SMALL_BUFF - SMALL_BUFF) * UP)
|
||||||
|
note.next_to(message, DOWN, SMALL_BUFF)
|
||||||
|
self.add(note)
|
||||||
|
|
||||||
|
yellow_parts = [sm for sm in message if sm.get_color() == YELLOW]
|
||||||
|
for pi in pis:
|
||||||
|
if yellow_parts:
|
||||||
|
pi.look_at(yellow_parts[-1])
|
||||||
|
else:
|
||||||
|
pi.look_at(message)
|
||||||
|
|
||||||
|
def get_pis(self):
|
||||||
|
return VGroup(
|
||||||
|
Randolph(color=BLUE_E, mode="pondering"),
|
||||||
|
Randolph(color=BLUE_D, mode="hooray"),
|
||||||
|
Randolph(color=BLUE_C, mode="sassy"),
|
||||||
|
Mortimer(color=GREY_BROWN, mode="thinking")
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_probabalistic_message(self):
|
||||||
|
return TextMobject(
|
||||||
|
"New video every", "Sunday",
|
||||||
|
"(with probability 0.3)",
|
||||||
|
tex_to_color_map={"Sunday": YELLOW},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_date_message(self):
|
||||||
|
return TextMobject(
|
||||||
|
"Next video on ", self.date,
|
||||||
|
tex_to_color_map={self.date: YELLOW},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_supporter_note(self):
|
||||||
|
return TextMobject(
|
||||||
|
"(Available to supporters for review now)",
|
||||||
|
color="#F96854",
|
||||||
|
)
|
||||||
|
|
|
@ -20,7 +20,9 @@ from manimlib.utils.color import invert_color
|
||||||
from manimlib.utils.space_ops import angle_of_vector
|
from manimlib.utils.space_ops import angle_of_vector
|
||||||
|
|
||||||
# TODO, this should probably reimplemented entirely, especially so as to
|
# TODO, this should probably reimplemented entirely, especially so as to
|
||||||
# better reuse code from mobject/coordinate_systems
|
# better reuse code from mobject/coordinate_systems.
|
||||||
|
# Also, I really dislike how the configuration is set up, this
|
||||||
|
# is way too messy to work with.
|
||||||
|
|
||||||
|
|
||||||
class GraphScene(Scene):
|
class GraphScene(Scene):
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
from time import sleep
|
|
||||||
import _thread as thread
|
|
||||||
import datetime
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import warnings
|
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
|
||||||
|
@ -21,12 +14,8 @@ from manimlib.container.container import Container
|
||||||
from manimlib.continual_animation.continual_animation import ContinualAnimation
|
from manimlib.continual_animation.continual_animation import ContinualAnimation
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.mobject.svg.tex_mobject import TextMobject
|
from manimlib.mobject.svg.tex_mobject import TextMobject
|
||||||
|
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||||
from manimlib.utils.iterables import list_update
|
from manimlib.utils.iterables import list_update
|
||||||
from manimlib.utils.output_directory_getters import add_extension_if_not_present
|
|
||||||
from manimlib.utils.output_directory_getters import get_image_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_movie_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_partial_movie_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_sorted_integer_files
|
|
||||||
|
|
||||||
|
|
||||||
class Scene(Container):
|
class Scene(Container):
|
||||||
|
@ -34,67 +23,42 @@ class Scene(Container):
|
||||||
"camera_class": Camera,
|
"camera_class": Camera,
|
||||||
"camera_config": {},
|
"camera_config": {},
|
||||||
"frame_duration": LOW_QUALITY_FRAME_DURATION,
|
"frame_duration": LOW_QUALITY_FRAME_DURATION,
|
||||||
"construct_args": [],
|
"file_writer_config": {},
|
||||||
"skip_animations": False,
|
"skip_animations": False,
|
||||||
"write_to_movie": False,
|
|
||||||
"save_pngs": False,
|
|
||||||
"pngs_mode": "RGBA",
|
|
||||||
"movie_file_extension": ".mp4",
|
|
||||||
"always_continually_update": False,
|
"always_continually_update": False,
|
||||||
"random_seed": 0,
|
"random_seed": 0,
|
||||||
"start_at_animation_number": None,
|
"start_at_animation_number": None,
|
||||||
"end_at_animation_number": None,
|
"end_at_animation_number": None,
|
||||||
"livestreaming": False,
|
|
||||||
"to_twitch": False,
|
|
||||||
"twitch_key": None,
|
|
||||||
"output_file_name": None,
|
|
||||||
"leave_progress_bars": False,
|
"leave_progress_bars": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# Perhaps allow passing in a non-empty *mobjects parameter?
|
|
||||||
Container.__init__(self, **kwargs)
|
Container.__init__(self, **kwargs)
|
||||||
self.camera = self.camera_class(**self.camera_config)
|
self.camera = self.camera_class(**self.camera_config)
|
||||||
|
self.file_writer = SceneFileWriter(
|
||||||
|
self, **self.file_writer_config,
|
||||||
|
)
|
||||||
|
|
||||||
self.mobjects = []
|
self.mobjects = []
|
||||||
self.continual_animations = []
|
self.continual_animations = []
|
||||||
|
# TODO, remove need for foreground mobjects
|
||||||
self.foreground_mobjects = []
|
self.foreground_mobjects = []
|
||||||
self.num_plays = 0
|
self.num_plays = 0
|
||||||
self.frame_num = 0
|
|
||||||
self.time = 0
|
self.time = 0
|
||||||
self.original_skipping_status = self.skip_animations
|
self.original_skipping_status = self.skip_animations
|
||||||
self.stream_lock = False
|
|
||||||
if self.random_seed is not None:
|
if self.random_seed is not None:
|
||||||
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:
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
self.construct(*self.construct_args)
|
self.construct()
|
||||||
except EndSceneEarlyException:
|
except EndSceneEarlyException:
|
||||||
if hasattr(self, "writing_process"):
|
pass
|
||||||
self.writing_process.terminate()
|
|
||||||
self.tear_down()
|
self.tear_down()
|
||||||
|
self.file_writer.finish()
|
||||||
if self.write_to_movie:
|
|
||||||
self.combine_movie_files()
|
|
||||||
self.print_end_message()
|
self.print_end_message()
|
||||||
|
|
||||||
def handle_play_like_call(func):
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
self.handle_animation_skipping()
|
|
||||||
should_write = self.write_to_movie and not self.skip_animations
|
|
||||||
if should_write:
|
|
||||||
self.open_movie_pipe()
|
|
||||||
func(self, *args, **kwargs)
|
|
||||||
self.close_movie_pipe()
|
|
||||||
else:
|
|
||||||
func(self, *args, **kwargs)
|
|
||||||
self.num_plays += 1
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""
|
"""
|
||||||
This is meant to be implement by any scenes which
|
This is meant to be implement by any scenes which
|
||||||
|
@ -116,11 +80,6 @@ class Scene(Container):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def get_output_file_name(self):
|
|
||||||
if self.output_file_name is not None:
|
|
||||||
return self.output_file_name
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
def print_end_message(self):
|
def print_end_message(self):
|
||||||
print("Played {} animations".format(self.num_plays))
|
print("Played {} animations".format(self.num_plays))
|
||||||
|
|
||||||
|
@ -142,40 +101,7 @@ 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):
|
||||||
self.camera = camera
|
self.camera = camera
|
||||||
|
|
||||||
|
@ -202,9 +128,9 @@ class Scene(Container):
|
||||||
mobjects=None,
|
mobjects=None,
|
||||||
background=None,
|
background=None,
|
||||||
include_submobjects=True,
|
include_submobjects=True,
|
||||||
dont_update_when_skipping=True,
|
ignore_skipping=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
if self.skip_animations and dont_update_when_skipping:
|
if self.skip_animations and not ignore_skipping:
|
||||||
return
|
return
|
||||||
if mobjects is None:
|
if mobjects is None:
|
||||||
mobjects = list_update(
|
mobjects = list_update(
|
||||||
|
@ -511,7 +437,7 @@ class Scene(Container):
|
||||||
compile_method(state)
|
compile_method(state)
|
||||||
return animations
|
return animations
|
||||||
|
|
||||||
def handle_animation_skipping(self):
|
def update_skipping_status(self):
|
||||||
if self.start_at_animation_number:
|
if self.start_at_animation_number:
|
||||||
if self.num_plays == self.start_at_animation_number:
|
if self.num_plays == self.start_at_animation_number:
|
||||||
self.skip_animations = False
|
self.skip_animations = False
|
||||||
|
@ -520,10 +446,18 @@ class Scene(Container):
|
||||||
self.skip_animations = True
|
self.skip_animations = True
|
||||||
raise EndSceneEarlyException()
|
raise EndSceneEarlyException()
|
||||||
|
|
||||||
|
def handle_play_like_call(func):
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
self.update_skipping_status()
|
||||||
|
allow_write = not self.skip_animations
|
||||||
|
self.file_writer.begin_animation(allow_write)
|
||||||
|
func(self, *args, **kwargs)
|
||||||
|
self.file_writer.end_animation(allow_write)
|
||||||
|
self.num_plays += 1
|
||||||
|
return wrapper
|
||||||
|
|
||||||
@handle_play_like_call
|
@handle_play_like_call
|
||||||
def play(self, *args, **kwargs):
|
def play(self, *args, **kwargs):
|
||||||
if self.livestreaming:
|
|
||||||
self.stream_lock = False
|
|
||||||
if len(args) == 0:
|
if len(args) == 0:
|
||||||
warnings.warn("Called Scene.play with no animations")
|
warnings.warn("Called Scene.play with no animations")
|
||||||
return
|
return
|
||||||
|
@ -558,22 +492,10 @@ class Scene(Container):
|
||||||
else:
|
else:
|
||||||
self.continual_update(0)
|
self.continual_update(0)
|
||||||
|
|
||||||
if self.livestreaming:
|
|
||||||
self.stream_lock = True
|
|
||||||
thread.start_new_thread(self.idle_stream, ())
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def idle_stream(self):
|
def idle_stream(self):
|
||||||
while(self.stream_lock):
|
self.file_writer.idle_stream()
|
||||||
a = datetime.datetime.now()
|
|
||||||
self.update_frame()
|
|
||||||
n_frames = 1
|
|
||||||
frame = self.get_frame()
|
|
||||||
self.add_frames(*[frame] * n_frames)
|
|
||||||
b = datetime.datetime.now()
|
|
||||||
time_diff = (b - a).total_seconds()
|
|
||||||
if time_diff < self.frame_duration:
|
|
||||||
sleep(self.frame_duration - time_diff)
|
|
||||||
|
|
||||||
def clean_up_animations(self, *animations):
|
def clean_up_animations(self, *animations):
|
||||||
for animation in animations:
|
for animation in animations:
|
||||||
|
@ -638,202 +560,20 @@ class Scene(Container):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_frames(self, *frames):
|
def add_frames(self, *frames):
|
||||||
|
self.increment_time(len(frames) * self.frame_duration)
|
||||||
if self.skip_animations:
|
if self.skip_animations:
|
||||||
return
|
return
|
||||||
self.increment_time(len(frames) * self.frame_duration)
|
for frame in frames:
|
||||||
if self.write_to_movie:
|
self.file_writer.write_frame(frame)
|
||||||
for frame in frames:
|
|
||||||
if self.save_pngs:
|
|
||||||
self.save_image(
|
|
||||||
"frame" + str(self.frame_num), self.pngs_mode, True
|
|
||||||
)
|
|
||||||
self.frame_num = self.frame_num + 1
|
|
||||||
self.writing_process.stdin.write(frame.tostring())
|
|
||||||
|
|
||||||
# Display methods
|
def add_sound(self, sound_file, time_offset=0):
|
||||||
|
time = self.get_time() + time_offset
|
||||||
|
self.file_writer.add_sound(sound_file, time)
|
||||||
|
|
||||||
def show_frame(self):
|
def show_frame(self):
|
||||||
self.update_frame(dont_update_when_skipping=False)
|
self.update_frame(ignore_skipping=True)
|
||||||
self.get_image().show()
|
self.get_image().show()
|
||||||
|
|
||||||
def get_image_file_path(self, name=None, dont_update=False):
|
|
||||||
sub_dir = "images"
|
|
||||||
output_file_name = self.get_output_file_name()
|
|
||||||
if dont_update:
|
|
||||||
sub_dir = output_file_name
|
|
||||||
path = get_image_output_directory(self.__class__, sub_dir)
|
|
||||||
file_name = add_extension_if_not_present(
|
|
||||||
name or output_file_name, ".png"
|
|
||||||
)
|
|
||||||
return os.path.join(path, file_name)
|
|
||||||
|
|
||||||
def save_image(self, name=None, mode="RGB", dont_update=False):
|
|
||||||
path = self.get_image_file_path(name, dont_update)
|
|
||||||
if not dont_update:
|
|
||||||
self.update_frame(dont_update_when_skipping=False)
|
|
||||||
image = self.get_image()
|
|
||||||
image = image.convert(mode)
|
|
||||||
image.save(path)
|
|
||||||
|
|
||||||
def get_movie_file_path(self, name=None, extension=None):
|
|
||||||
directory = get_movie_output_directory(
|
|
||||||
self.__class__, self.camera_config, self.frame_duration
|
|
||||||
)
|
|
||||||
if extension is None:
|
|
||||||
extension = self.movie_file_extension
|
|
||||||
if name is None:
|
|
||||||
name = self.get_output_file_name()
|
|
||||||
file_path = os.path.join(directory, name)
|
|
||||||
if not file_path.endswith(extension):
|
|
||||||
file_path += extension
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def get_partial_movie_directory(self):
|
|
||||||
return get_partial_movie_output_directory(
|
|
||||||
self.__class__, self.camera_config, self.frame_duration
|
|
||||||
)
|
|
||||||
|
|
||||||
def open_movie_pipe(self):
|
|
||||||
directory = self.get_partial_movie_directory()
|
|
||||||
file_path = os.path.join(
|
|
||||||
directory, "{}{}".format(
|
|
||||||
self.num_plays,
|
|
||||||
self.movie_file_extension,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
temp_file_path = file_path.replace(".", "_temp.")
|
|
||||||
|
|
||||||
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()
|
|
||||||
width = self.camera.get_pixel_width()
|
|
||||||
|
|
||||||
command = [
|
|
||||||
FFMPEG_BIN,
|
|
||||||
'-y', # overwrite output file if it exists
|
|
||||||
'-f', 'rawvideo',
|
|
||||||
'-s', '%dx%d' % (width, height), # size of one frame
|
|
||||||
'-pix_fmt', 'rgba',
|
|
||||||
'-r', str(fps), # frames per second
|
|
||||||
'-i', '-', # The imput comes from a pipe
|
|
||||||
'-c:v', 'h264_nvenc',
|
|
||||||
'-an', # Tells FFMPEG not to expect any audio
|
|
||||||
'-loglevel', 'error',
|
|
||||||
]
|
|
||||||
if self.movie_file_extension == ".mov":
|
|
||||||
# This is if the background of the exported video
|
|
||||||
# should be transparent.
|
|
||||||
command += [
|
|
||||||
'-vcodec', 'qtrle',
|
|
||||||
# '-vcodec', 'png',
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
command += [
|
|
||||||
'-vcodec', 'libx264',
|
|
||||||
'-pix_fmt', 'yuv420p',
|
|
||||||
]
|
|
||||||
if self.livestreaming:
|
|
||||||
if self.to_twitch:
|
|
||||||
command += ['-f', 'flv']
|
|
||||||
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
|
|
||||||
else:
|
|
||||||
command += ['-f', 'mpegts']
|
|
||||||
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
|
||||||
else:
|
|
||||||
command += [temp_file_path]
|
|
||||||
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
|
||||||
|
|
||||||
def close_movie_pipe(self):
|
|
||||||
self.writing_process.stdin.close()
|
|
||||||
self.writing_process.wait()
|
|
||||||
if self.livestreaming:
|
|
||||||
return True
|
|
||||||
shutil.move(
|
|
||||||
self.temp_movie_file_path,
|
|
||||||
self.movie_file_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
def combine_movie_files(self):
|
|
||||||
# Manim renders the scene as many smaller movie files
|
|
||||||
# which are then concatenated to a larger one. The reason
|
|
||||||
# for this is that sometimes video-editing is made easier when
|
|
||||||
# one works with the broken up scene, which effectively has
|
|
||||||
# cuts at all the places you might want. But for viewing
|
|
||||||
# the scene as a whole, one of course wants to see it as a
|
|
||||||
# single piece.
|
|
||||||
partial_movie_file_directory = self.get_partial_movie_directory()
|
|
||||||
kwargs = {
|
|
||||||
"remove_non_integer_files": True,
|
|
||||||
"extension": self.movie_file_extension,
|
|
||||||
}
|
|
||||||
if self.start_at_animation_number is not None:
|
|
||||||
kwargs["min_index"] = self.start_at_animation_number
|
|
||||||
if self.end_at_animation_number is not None:
|
|
||||||
kwargs["max_index"] = self.end_at_animation_number
|
|
||||||
else:
|
|
||||||
kwargs["remove_indices_greater_than"] = self.num_plays - 1
|
|
||||||
partial_movie_files = get_sorted_integer_files(
|
|
||||||
partial_movie_file_directory,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
# Write a file partial_file_list.txt containing all
|
|
||||||
# partial movie files
|
|
||||||
file_list = os.path.join(
|
|
||||||
partial_movie_file_directory,
|
|
||||||
"partial_movie_file_list.txt"
|
|
||||||
)
|
|
||||||
with open(file_list, 'w') as fp:
|
|
||||||
for pf_path in partial_movie_files:
|
|
||||||
if os.name == 'nt':
|
|
||||||
pf_path = pf_path.replace('\\', '/')
|
|
||||||
fp.write("file \'{}\'\n".format(pf_path))
|
|
||||||
|
|
||||||
movie_file_path = self.get_movie_file_path()
|
|
||||||
commands = [
|
|
||||||
FFMPEG_BIN,
|
|
||||||
'-y', # overwrite output file if it exists
|
|
||||||
'-f', 'concat',
|
|
||||||
'-safe', '0',
|
|
||||||
'-i', file_list,
|
|
||||||
'-c', 'copy',
|
|
||||||
'-loglevel', 'error',
|
|
||||||
movie_file_path
|
|
||||||
]
|
|
||||||
if not self.includes_sound:
|
|
||||||
commands.insert(-1, '-an')
|
|
||||||
|
|
||||||
combine_process = subprocess.Popen(commands)
|
|
||||||
combine_process.wait()
|
|
||||||
# os.remove(file_list)
|
|
||||||
|
|
||||||
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
|
# TODO, this doesn't belong in Scene, but should be
|
||||||
# part of some more specialized subclass optimized
|
# part of some more specialized subclass optimized
|
||||||
# for livestreaming
|
# for livestreaming
|
||||||
|
|
327
manimlib/scene/scene_file_writer.py
Normal file
327
manimlib/scene/scene_file_writer.py
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
import numpy as np
|
||||||
|
from pydub import AudioSegment
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import _thread as thread
|
||||||
|
from time import sleep
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from manimlib.constants import FFMPEG_BIN
|
||||||
|
from manimlib.constants import STREAMING_IP
|
||||||
|
from manimlib.constants import STREAMING_PORT
|
||||||
|
from manimlib.constants import STREAMING_PROTOCOL
|
||||||
|
from manimlib.constants import VIDEO_DIR
|
||||||
|
from manimlib.utils.config_ops import digest_config
|
||||||
|
from manimlib.utils.file_ops import guarantee_existance
|
||||||
|
from manimlib.utils.file_ops import add_extension_if_not_present
|
||||||
|
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||||
|
from manimlib.utils.sounds import get_full_sound_file_path
|
||||||
|
|
||||||
|
|
||||||
|
class SceneFileWriter(object):
|
||||||
|
CONFIG = {
|
||||||
|
"write_to_movie": False,
|
||||||
|
# TODO, save_pngs is doing nothing
|
||||||
|
"save_pngs": False,
|
||||||
|
"png_mode": "RGBA",
|
||||||
|
"save_last_frame": False,
|
||||||
|
"movie_file_extension": ".mp4",
|
||||||
|
"livestreaming": False,
|
||||||
|
"to_twitch": False,
|
||||||
|
"twitch_key": None,
|
||||||
|
# Previous output_file_name
|
||||||
|
# TODO, address this in extract_scene et. al.
|
||||||
|
"file_name": None,
|
||||||
|
"output_directory": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, scene, **kwargs):
|
||||||
|
digest_config(self, kwargs)
|
||||||
|
self.scene = scene
|
||||||
|
self.stream_lock = False
|
||||||
|
self.init_output_directories()
|
||||||
|
self.init_audio()
|
||||||
|
|
||||||
|
# Output directories and files
|
||||||
|
def init_output_directories(self):
|
||||||
|
output_directory = self.output_directory or self.get_default_output_directory()
|
||||||
|
file_name = self.file_name or self.get_default_file_name()
|
||||||
|
if self.save_last_frame:
|
||||||
|
image_dir = guarantee_existance(os.path.join(
|
||||||
|
VIDEO_DIR,
|
||||||
|
output_directory,
|
||||||
|
self.get_image_directory(),
|
||||||
|
))
|
||||||
|
self.image_file_path = os.path.join(
|
||||||
|
image_dir,
|
||||||
|
add_extension_if_not_present(file_name, ".png")
|
||||||
|
)
|
||||||
|
if self.write_to_movie:
|
||||||
|
movie_dir = guarantee_existance(os.path.join(
|
||||||
|
VIDEO_DIR,
|
||||||
|
output_directory,
|
||||||
|
self.get_movie_directory(),
|
||||||
|
))
|
||||||
|
self.movie_file_path = os.path.join(
|
||||||
|
movie_dir,
|
||||||
|
add_extension_if_not_present(
|
||||||
|
file_name, self.movie_file_extension
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.partial_movie_directory = guarantee_existance(os.path.join(
|
||||||
|
movie_dir,
|
||||||
|
self.get_partial_movie_directory(),
|
||||||
|
file_name,
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_default_output_directory(self):
|
||||||
|
scene_module = self.scene.__class__.__module__
|
||||||
|
return scene_module.replace(".", os.path.sep)
|
||||||
|
|
||||||
|
def get_default_file_name(self):
|
||||||
|
return self.scene.__class__.__name__
|
||||||
|
|
||||||
|
def get_movie_directory(self):
|
||||||
|
pixel_height = self.scene.camera.pixel_height
|
||||||
|
frame_duration = self.scene.frame_duration
|
||||||
|
return "{}p{}".format(
|
||||||
|
pixel_height, int(1.0 / frame_duration)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_image_directory(self):
|
||||||
|
return "images"
|
||||||
|
|
||||||
|
def get_partial_movie_directory(self):
|
||||||
|
return "partial_movie_files"
|
||||||
|
|
||||||
|
# Directory getters
|
||||||
|
def get_image_file_path(self):
|
||||||
|
return self.image_file_path
|
||||||
|
|
||||||
|
def get_next_partial_movie_path(self):
|
||||||
|
result = os.path.join(
|
||||||
|
self.partial_movie_directory,
|
||||||
|
"{:05}{}".format(
|
||||||
|
self.scene.num_plays,
|
||||||
|
self.movie_file_extension,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_movie_file_path(self):
|
||||||
|
return self.movie_file_path
|
||||||
|
|
||||||
|
# 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=None):
|
||||||
|
if not self.includes_sound:
|
||||||
|
self.includes_sound = True
|
||||||
|
self.create_audio_segment()
|
||||||
|
segment = self.audio_segment
|
||||||
|
curr_end = segment.duration_seconds
|
||||||
|
if time is None:
|
||||||
|
time = curr_end
|
||||||
|
if time < 0:
|
||||||
|
raise Exception("Adding sound at timestamp < 0")
|
||||||
|
|
||||||
|
new_end = 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 * time)
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_sound(self, sound_file, time):
|
||||||
|
file_path = get_full_sound_file_path(sound_file)
|
||||||
|
new_segment = AudioSegment.from_file(file_path)
|
||||||
|
self.add_audio_segment(new_segment, time)
|
||||||
|
|
||||||
|
# Writers
|
||||||
|
def begin_animation(self, allow_write=False):
|
||||||
|
if self.write_to_movie and allow_write:
|
||||||
|
self.open_movie_pipe()
|
||||||
|
if self.livestreaming:
|
||||||
|
self.stream_lock = False
|
||||||
|
|
||||||
|
def end_animation(self, allow_write=False):
|
||||||
|
if self.write_to_movie and allow_write:
|
||||||
|
self.close_movie_pipe()
|
||||||
|
if self.livestreaming:
|
||||||
|
self.stream_lock = True
|
||||||
|
thread.start_new_thread(self.idle_stream, ())
|
||||||
|
|
||||||
|
def write_frame(self, frame):
|
||||||
|
if self.write_to_movie:
|
||||||
|
self.writing_process.stdin.write(frame.tostring())
|
||||||
|
|
||||||
|
def save_image(self, image):
|
||||||
|
file_path = self.get_image_file_path()
|
||||||
|
image.save(file_path)
|
||||||
|
self.print_file_ready_message(file_path)
|
||||||
|
|
||||||
|
def idle_stream(self):
|
||||||
|
while self.stream_lock:
|
||||||
|
a = datetime.datetime.now()
|
||||||
|
self.update_frame()
|
||||||
|
n_frames = 1
|
||||||
|
frame = self.get_frame()
|
||||||
|
self.add_frames(*[frame] * n_frames)
|
||||||
|
b = datetime.datetime.now()
|
||||||
|
time_diff = (b - a).total_seconds()
|
||||||
|
if time_diff < self.frame_duration:
|
||||||
|
sleep(self.frame_duration - time_diff)
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
if self.write_to_movie:
|
||||||
|
if hasattr(self, "writing_process"):
|
||||||
|
self.writing_process.terminate()
|
||||||
|
self.combine_movie_files()
|
||||||
|
if self.save_last_frame:
|
||||||
|
self.scene.update_frame(ignore_skipping=True)
|
||||||
|
self.save_image(self.scene.get_image())
|
||||||
|
|
||||||
|
def open_movie_pipe(self):
|
||||||
|
file_path = self.get_next_partial_movie_path()
|
||||||
|
temp_file_path = file_path.replace(".", "_temp.")
|
||||||
|
|
||||||
|
self.partial_movie_file_path = file_path
|
||||||
|
self.temp_partial_movie_file_path = temp_file_path
|
||||||
|
|
||||||
|
fps = int(1 / self.scene.frame_duration)
|
||||||
|
height = self.scene.camera.get_pixel_height()
|
||||||
|
width = self.scene.camera.get_pixel_width()
|
||||||
|
|
||||||
|
command = [
|
||||||
|
FFMPEG_BIN,
|
||||||
|
'-y', # overwrite output file if it exists
|
||||||
|
'-f', 'rawvideo',
|
||||||
|
'-s', '%dx%d' % (width, height), # size of one frame
|
||||||
|
'-pix_fmt', 'rgba',
|
||||||
|
'-r', str(fps), # frames per second
|
||||||
|
'-i', '-', # The imput comes from a pipe
|
||||||
|
'-c:v', 'h264_nvenc',
|
||||||
|
'-an', # Tells FFMPEG not to expect any audio
|
||||||
|
'-loglevel', 'error',
|
||||||
|
]
|
||||||
|
if self.movie_file_extension == ".mov":
|
||||||
|
# This is if the background of the exported video
|
||||||
|
# should be transparent.
|
||||||
|
command += [
|
||||||
|
'-vcodec', 'qtrle',
|
||||||
|
# '-vcodec', 'png',
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
command += [
|
||||||
|
'-vcodec', 'libx264',
|
||||||
|
'-pix_fmt', 'yuv420p',
|
||||||
|
]
|
||||||
|
if self.livestreaming:
|
||||||
|
if self.to_twitch:
|
||||||
|
command += ['-f', 'flv']
|
||||||
|
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
|
||||||
|
else:
|
||||||
|
command += ['-f', 'mpegts']
|
||||||
|
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
||||||
|
else:
|
||||||
|
command += [temp_file_path]
|
||||||
|
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
||||||
|
|
||||||
|
def close_movie_pipe(self):
|
||||||
|
self.writing_process.stdin.close()
|
||||||
|
self.writing_process.wait()
|
||||||
|
if self.livestreaming:
|
||||||
|
return True
|
||||||
|
shutil.move(
|
||||||
|
self.temp_partial_movie_file_path,
|
||||||
|
self.partial_movie_file_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
def combine_movie_files(self):
|
||||||
|
# Manim renders the scene as many smaller movie files
|
||||||
|
# which are then concatenated to a larger one. The reason
|
||||||
|
# for this is that sometimes video-editing is made easier when
|
||||||
|
# one works with the broken up scene, which effectively has
|
||||||
|
# cuts at all the places you might want. But for viewing
|
||||||
|
# the scene as a whole, one of course wants to see it as a
|
||||||
|
# single piece.
|
||||||
|
kwargs = {
|
||||||
|
"remove_non_integer_files": True,
|
||||||
|
"extension": self.movie_file_extension,
|
||||||
|
}
|
||||||
|
if self.scene.start_at_animation_number is not None:
|
||||||
|
kwargs["min_index"] = self.scene.start_at_animation_number
|
||||||
|
if self.scene.end_at_animation_number is not None:
|
||||||
|
kwargs["max_index"] = self.scene.end_at_animation_number
|
||||||
|
else:
|
||||||
|
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
|
||||||
|
partial_movie_files = get_sorted_integer_files(
|
||||||
|
self.partial_movie_directory,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
# Write a file partial_file_list.txt containing all
|
||||||
|
# partial movie files
|
||||||
|
file_list = os.path.join(
|
||||||
|
self.partial_movie_directory,
|
||||||
|
"partial_movie_file_list.txt"
|
||||||
|
)
|
||||||
|
with open(file_list, 'w') as fp:
|
||||||
|
for pf_path in partial_movie_files:
|
||||||
|
if os.name == 'nt':
|
||||||
|
pf_path = pf_path.replace('\\', '/')
|
||||||
|
fp.write("file \'{}\'\n".format(pf_path))
|
||||||
|
|
||||||
|
movie_file_path = self.get_movie_file_path()
|
||||||
|
commands = [
|
||||||
|
FFMPEG_BIN,
|
||||||
|
'-y', # overwrite output file if it exists
|
||||||
|
'-f', 'concat',
|
||||||
|
'-safe', '0',
|
||||||
|
'-i', file_list,
|
||||||
|
'-c', 'copy',
|
||||||
|
'-loglevel', 'error',
|
||||||
|
movie_file_path
|
||||||
|
]
|
||||||
|
if not self.includes_sound:
|
||||||
|
commands.insert(-1, '-an')
|
||||||
|
|
||||||
|
combine_process = subprocess.Popen(commands)
|
||||||
|
combine_process.wait()
|
||||||
|
# os.remove(file_list)
|
||||||
|
|
||||||
|
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", sound_file_path])
|
||||||
|
|
||||||
|
self.print_file_ready_message(movie_file_path)
|
||||||
|
|
||||||
|
def print_file_ready_message(self, file_path):
|
||||||
|
print("\nFile ready at {}\n".format(file_path))
|
|
@ -1,8 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.constants import VIDEO_DIR
|
|
||||||
|
|
||||||
|
|
||||||
def add_extension_if_not_present(file_name, extension):
|
def add_extension_if_not_present(file_name, extension):
|
||||||
# This could conceivably be smarter about handling existing differing extensions
|
# This could conceivably be smarter about handling existing differing extensions
|
||||||
|
@ -18,36 +16,16 @@ def guarantee_existance(path):
|
||||||
return os.path.abspath(path)
|
return os.path.abspath(path)
|
||||||
|
|
||||||
|
|
||||||
def get_scene_output_directory(scene_class):
|
def seek_full_path_from_defaults(file_name, default_dir, extensions):
|
||||||
return guarantee_existance(os.path.join(
|
possible_paths = [file_name]
|
||||||
VIDEO_DIR,
|
possible_paths += [
|
||||||
scene_class.__module__.replace(".", os.path.sep)
|
os.path.join(default_dir, file_name + extension)
|
||||||
))
|
for extension in ["", *extensions]
|
||||||
|
]
|
||||||
|
for path in possible_paths:
|
||||||
def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
if os.path.exists(path):
|
||||||
directory = get_scene_output_directory(scene_class)
|
return path
|
||||||
sub_dir = "%dp%d" % (
|
raise IOError("File {} not Found".format(file_name))
|
||||||
camera_config["pixel_height"],
|
|
||||||
int(1.0 / frame_duration)
|
|
||||||
)
|
|
||||||
return guarantee_existance(os.path.join(directory, sub_dir))
|
|
||||||
|
|
||||||
|
|
||||||
def get_partial_movie_output_directory(scene_class, camera_config, frame_duration):
|
|
||||||
directory = get_movie_output_directory(scene_class, camera_config, frame_duration)
|
|
||||||
return guarantee_existance(
|
|
||||||
os.path.join(
|
|
||||||
directory,
|
|
||||||
"partial_movie_files",
|
|
||||||
scene_class.__name__
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_image_output_directory(scene_class, sub_dir="images"):
|
|
||||||
directory = get_scene_output_directory(scene_class)
|
|
||||||
return guarantee_existance(os.path.join(directory, sub_dir))
|
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_integer_files(directory,
|
def get_sorted_integer_files(directory,
|
|
@ -4,20 +4,15 @@ import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from manimlib.constants import RASTER_IMAGE_DIR
|
from manimlib.constants import RASTER_IMAGE_DIR
|
||||||
|
from manimlib.utils.file_ops import seek_full_path_from_defaults
|
||||||
|
|
||||||
|
|
||||||
def get_full_raster_image_path(image_file_name):
|
def get_full_raster_image_path(image_file_name):
|
||||||
possible_paths = [
|
return seek_full_path_from_defaults(
|
||||||
image_file_name,
|
image_file_name,
|
||||||
os.path.join(RASTER_IMAGE_DIR, image_file_name),
|
default_dir=RASTER_IMAGE_DIR,
|
||||||
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".jpg"),
|
extensions=[".jpg", ".png", ".gif"]
|
||||||
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".png"),
|
)
|
||||||
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".gif"),
|
|
||||||
]
|
|
||||||
for path in possible_paths:
|
|
||||||
if os.path.exists(path):
|
|
||||||
return path
|
|
||||||
raise IOError("File %s not Found" % image_file_name)
|
|
||||||
|
|
||||||
|
|
||||||
def drag_pixels(frames):
|
def drag_pixels(frames):
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
from manimlib.constants import SOUND_DIR
|
||||||
|
from manimlib.utils.file_ops import seek_full_path_from_defaults
|
||||||
|
|
||||||
|
|
||||||
def play_chord(*nums):
|
def play_chord(*nums):
|
||||||
|
@ -28,3 +30,11 @@ def play_error_sound():
|
||||||
|
|
||||||
def play_finish_sound():
|
def play_finish_sound():
|
||||||
play_chord(12, 9, 5, 2)
|
play_chord(12, 9, 5, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_full_sound_file_path(sound_file_name):
|
||||||
|
return seek_full_path_from_defaults(
|
||||||
|
sound_file_name,
|
||||||
|
default_dir=SOUND_DIR,
|
||||||
|
extensions=[".wav", ".mp3"]
|
||||||
|
)
|
||||||
|
|
|
@ -2998,6 +2998,7 @@ class ShowTwoPopulations(Scene):
|
||||||
"start_num_rabbits": 20,
|
"start_num_rabbits": 20,
|
||||||
"animal_height": 0.5,
|
"animal_height": 0.5,
|
||||||
"final_wait_time": 30,
|
"final_wait_time": 30,
|
||||||
|
"count_word_scale_val": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
|
@ -3093,9 +3094,13 @@ class ShowTwoPopulations(Scene):
|
||||||
# Add counts for foxes and rabbits
|
# Add counts for foxes and rabbits
|
||||||
labels = self.get_pop_labels()
|
labels = self.get_pop_labels()
|
||||||
num_foxes = Integer(10)
|
num_foxes = Integer(10)
|
||||||
|
num_foxes.scale(self.count_word_scale_val)
|
||||||
num_foxes.next_to(labels[0], RIGHT)
|
num_foxes.next_to(labels[0], RIGHT)
|
||||||
|
num_foxes.align_to(labels[0][1], DOWN)
|
||||||
num_rabbits = Integer(10)
|
num_rabbits = Integer(10)
|
||||||
|
num_rabbits.scale(self.count_word_scale_val)
|
||||||
num_rabbits.next_to(labels[1], RIGHT)
|
num_rabbits.next_to(labels[1], RIGHT)
|
||||||
|
num_rabbits.align_to(labels[1][1], DOWN)
|
||||||
|
|
||||||
self.add(ContinualChangingDecimal(
|
self.add(ContinualChangingDecimal(
|
||||||
num_foxes, lambda a: get_num_foxes()
|
num_foxes, lambda a: get_num_foxes()
|
||||||
|
@ -3156,6 +3161,8 @@ class ShowTwoPopulations(Scene):
|
||||||
TextMobject("\\# Foxes: "),
|
TextMobject("\\# Foxes: "),
|
||||||
TextMobject("\\# Rabbits: "),
|
TextMobject("\\# Rabbits: "),
|
||||||
)
|
)
|
||||||
|
for label in labels:
|
||||||
|
label.scale(self.count_word_scale_val)
|
||||||
labels.arrange_submobjects(RIGHT, buff=2)
|
labels.arrange_submobjects(RIGHT, buff=2)
|
||||||
labels.to_edge(UP)
|
labels.to_edge(UP)
|
||||||
return labels
|
return labels
|
||||||
|
|
|
@ -8,9 +8,7 @@ from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG
|
||||||
from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
|
from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
|
||||||
from manimlib.config import get_module
|
from manimlib.config import get_module
|
||||||
from manimlib.extract_scene import is_child_scene
|
from manimlib.extract_scene import is_child_scene
|
||||||
from manimlib.utils.output_directory_getters import get_movie_output_directory
|
from manimlib.utils.file_ops import get_movie_output_directory
|
||||||
from manimlib.utils.output_directory_getters import get_partial_movie_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_sorted_integer_files
|
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_scene_classes(module_name):
|
def get_sorted_scene_classes(module_name):
|
||||||
|
@ -48,26 +46,29 @@ def stage_animations(module_name):
|
||||||
sorted_files = []
|
sorted_files = []
|
||||||
for scene_class in scene_classes:
|
for scene_class in scene_classes:
|
||||||
scene_name = scene_class.__name__
|
scene_name = scene_class.__name__
|
||||||
|
clips = [f for f in files if f.startswith(scene_name + ".")]
|
||||||
|
for clip in clips:
|
||||||
|
sorted_files.append(os.path.join(animation_dir, clip))
|
||||||
# Partial movie file directory
|
# Partial movie file directory
|
||||||
pmf_dir = get_partial_movie_output_directory(
|
# movie_dir = get_movie_output_directory(
|
||||||
scene_class, **output_directory_kwargs
|
# scene_class, **output_directory_kwargs
|
||||||
)
|
# )
|
||||||
if os.path.exists(pmf_dir):
|
# if os.path.exists(movie_dir):
|
||||||
for extension in [".mov", ".mp4"]:
|
# for extension in [".mov", ".mp4"]:
|
||||||
int_files = get_sorted_integer_files(
|
# int_files = get_sorted_integer_files(
|
||||||
pmf_dir, extension=extension
|
# pmf_dir, extension=extension
|
||||||
)
|
# )
|
||||||
for file in int_files:
|
# for file in int_files:
|
||||||
sorted_files.append(os.path.join(pmf_dir, file))
|
# sorted_files.append(os.path.join(pmf_dir, file))
|
||||||
else:
|
# else:
|
||||||
for clip in [f for f in files if f.startswith(scene_name + ".")]:
|
|
||||||
sorted_files.append(os.path.join(animation_dir, clip))
|
|
||||||
|
|
||||||
animation_subdir = os.path.dirname(animation_dir)
|
# animation_subdir = os.path.dirname(animation_dir)
|
||||||
count = 0
|
count = 0
|
||||||
while True:
|
while True:
|
||||||
staged_scenes_dir = os.path.join(
|
staged_scenes_dir = os.path.join(
|
||||||
animation_subdir, "staged_scenes_{}".format(count)
|
animation_dir,
|
||||||
|
os.pardir,
|
||||||
|
"staged_scenes_{}".format(count)
|
||||||
)
|
)
|
||||||
if not os.path.exists(staged_scenes_dir):
|
if not os.path.exists(staged_scenes_dir):
|
||||||
os.makedirs(staged_scenes_dir)
|
os.makedirs(staged_scenes_dir)
|
||||||
|
@ -82,8 +83,7 @@ def stage_animations(module_name):
|
||||||
symlink_name = os.path.join(
|
symlink_name = os.path.join(
|
||||||
staged_scenes_dir,
|
staged_scenes_dir,
|
||||||
"Scene_{:03}_{}".format(
|
"Scene_{:03}_{}".format(
|
||||||
count,
|
count, f.split(os.sep)[-1]
|
||||||
"".join(f.split(os.sep)[-2:])
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
os.symlink(f, symlink_name)
|
os.symlink(f, symlink_name)
|
||||||
|
|
Loading…
Add table
Reference in a new issue