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.
|
||||
|
||||
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 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)
|
||||
if self.label_text is None:
|
||||
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)
|
||||
self.label = self.get_label()
|
||||
self.add(self.label)
|
||||
|
@ -130,8 +132,8 @@ class SlidingBlocks(VGroup):
|
|||
|
||||
def update_blocks_from_phase_space_point_tracker(self):
|
||||
block1, block2 = self.block1, self.block2
|
||||
|
||||
ps_point = self.phase_space_point_tracker.get_location()
|
||||
|
||||
theta = np.arctan(np.sqrt(self.mass_ratio))
|
||||
ps_point_angle = angle_of_vector(ps_point)
|
||||
n_clacks = int(ps_point_angle / theta)
|
||||
|
@ -274,7 +276,6 @@ class BlocksAndWallScene(Scene):
|
|||
"collision_sound": "clack.wav",
|
||||
"show_flash_animations": True,
|
||||
"min_time_between_sounds": 0.004,
|
||||
"allow_sound": True,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
|
@ -345,10 +346,7 @@ class BlocksAndWallScene(Scene):
|
|||
self.counter_mob.set_value(n_clacks)
|
||||
|
||||
def create_sound_file(self, clack_data):
|
||||
directory = get_scene_output_directory(BlocksAndWallScene)
|
||||
clack_file = os.path.join(
|
||||
directory, 'sounds', self.collision_sound,
|
||||
)
|
||||
clack_file = os.path.join(SOUND_DIR, self.collision_sound)
|
||||
output_file = self.get_movie_file_path(extension='.wav')
|
||||
times = [
|
||||
time
|
||||
|
@ -377,6 +375,8 @@ class BlocksAndWallScene(Scene):
|
|||
clacks.export(output_file, format="wav")
|
||||
return output_file
|
||||
|
||||
# TODO, this no longer works
|
||||
# should use Scene.add_sound instead
|
||||
def combine_movie_files(self):
|
||||
Scene.combine_movie_files(self)
|
||||
if self.include_sound:
|
||||
|
@ -848,9 +848,14 @@ class BlocksAndWallExampleMass1e10(BlocksAndWallExample):
|
|||
|
||||
|
||||
class DigitsOfPi(Scene):
|
||||
CONFIG = {"n_digits": 9}
|
||||
|
||||
def construct(self):
|
||||
nd = self.n_digits
|
||||
pow10 = int(10**nd)
|
||||
rounded_pi = int(pow10 * PI) / pow10
|
||||
equation = TexMobject(
|
||||
"\\pi = 3.14159265..."
|
||||
("\\pi = {:." + str(nd) + "f}...").format(rounded_pi)
|
||||
)
|
||||
equation.set_color(YELLOW)
|
||||
pi_creature = Randolph(color=YELLOW)
|
||||
|
@ -858,9 +863,11 @@ class DigitsOfPi(Scene):
|
|||
pi_creature.scale(1.4)
|
||||
pi_creature.move_to(equation[0], DOWN)
|
||||
self.add(pi_creature, equation[1])
|
||||
for digit in equation[2:]:
|
||||
self.add(digit)
|
||||
self.wait(0.1)
|
||||
self.play(ShowIncreasingSubsets(
|
||||
equation[2:],
|
||||
rate_func=None,
|
||||
run_time=1,
|
||||
))
|
||||
self.play(Blink(pi_creature))
|
||||
self.wait()
|
||||
|
||||
|
@ -1553,7 +1560,7 @@ class EndScreen(Scene):
|
|||
)
|
||||
|
||||
|
||||
class Thumbnail(BlocksAndWallExample):
|
||||
class Thumbnail(BlocksAndWallExample, MovingCameraScene):
|
||||
CONFIG = {
|
||||
"sliding_blocks_config": {
|
||||
"block1_config": {
|
||||
|
@ -1565,19 +1572,34 @@ class Thumbnail(BlocksAndWallExample):
|
|||
"wait_time": 0,
|
||||
"count_clacks": False,
|
||||
"show_flash_animations": False,
|
||||
"floor_y": -3,
|
||||
"floor_y": -3.0,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
MovingCameraScene.setup(self)
|
||||
BlocksAndWallExample.setup(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.wall.set_stroke(WHITE, 10)
|
||||
self.wall[1:].set_stroke(WHITE, 4)
|
||||
|
||||
def grow_labels(self):
|
||||
blocks = self.blocks
|
||||
for block in blocks.block1, blocks.block2:
|
||||
block.remove(block.label)
|
||||
block.label.scale(2.5, about_point=block.get_top())
|
||||
self.add(block.label)
|
||||
|
||||
def add_vector(self):
|
||||
blocks = self.blocks
|
||||
arrow = Vector(
|
||||
2.5 * LEFT,
|
||||
color=RED,
|
||||
|
@ -1590,9 +1612,10 @@ class Thumbnail(BlocksAndWallExample):
|
|||
)
|
||||
self.add(arrow)
|
||||
|
||||
def add_text(self):
|
||||
question = TextMobject("How many\\\\collisions?")
|
||||
question.scale(2.5)
|
||||
question.to_edge(UP)
|
||||
question.set_color(YELLOW)
|
||||
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.images 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.rate_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
|
||||
# quality.
|
||||
# Use -s to skip to the end and just show the final frame
|
||||
# Use the -p to have the animation pop up once done.
|
||||
# Use -s to skip to the end and just save the final frame
|
||||
# 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.
|
||||
|
||||
|
||||
|
|
|
@ -246,11 +246,10 @@ class GrowFromPoint(Transform):
|
|||
def __init__(self, mobject, point, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
target = mobject.copy()
|
||||
point_mob = VectorizedPoint(point)
|
||||
mobject.scale(0)
|
||||
mobject.move_to(point)
|
||||
if self.point_color:
|
||||
point_mob.set_color(self.point_color)
|
||||
mobject.replace(point_mob)
|
||||
mobject.set_color(point_mob.get_color())
|
||||
mobject.set_color(self.point_color)
|
||||
Transform.__init__(self, mobject, target, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -191,10 +191,7 @@ class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle):
|
|||
|
||||
class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
|
||||
CONFIG = {
|
||||
"rect_to_animation": lambda rect: Succession(
|
||||
ShowCreation, rect,
|
||||
FadeOut, rect,
|
||||
)
|
||||
"rect_to_animation": ShowCreationThenFadeOut
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ def parse_cli():
|
|||
parser.add_argument(
|
||||
"-p", "--preview",
|
||||
action="store_true",
|
||||
help="Automatically open movie file once its done",
|
||||
help="Automatically open the saved file once its done",
|
||||
),
|
||||
parser.add_argument(
|
||||
"-w", "--write_to_movie",
|
||||
|
@ -33,9 +33,9 @@ def parse_cli():
|
|||
help="Render the scene as a movie file",
|
||||
),
|
||||
parser.add_argument(
|
||||
"-s", "--show_last_frame",
|
||||
"-s", "--save_last_frame",
|
||||
action="store_true",
|
||||
help="Save the last frame and open the image file",
|
||||
help="Save the last frame",
|
||||
),
|
||||
parser.add_argument(
|
||||
"-l", "--low_quality",
|
||||
|
@ -73,8 +73,7 @@ def parse_cli():
|
|||
help="Write all the scenes from a file",
|
||||
),
|
||||
parser.add_argument(
|
||||
"-o", "--output_file_name",
|
||||
nargs=1,
|
||||
"-o", "--file_name",
|
||||
help="Specify the name of the output file, if"
|
||||
"it should be different from the scene class name",
|
||||
)
|
||||
|
@ -156,36 +155,25 @@ def get_module(file_name):
|
|||
|
||||
|
||||
def get_configuration(args):
|
||||
if args.output_file_name is not None:
|
||||
output_file_name_root, output_file_name_ext = os.path.splitext(
|
||||
args.output_file_name)
|
||||
expected_ext = '.png' if args.show_last_frame else '.mp4'
|
||||
if output_file_name_ext not in ['', expected_ext]:
|
||||
print("WARNING: The output will be to (doubly-dotted) %s%s" %
|
||||
output_file_name_root % expected_ext)
|
||||
output_file_name = args.output_file_name
|
||||
else:
|
||||
# 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
|
||||
|
||||
file_writer_config = {
|
||||
# By default, write to file
|
||||
"write_to_movie": args.write_to_movie or not args.save_last_frame,
|
||||
"save_last_frame": args.save_last_frame,
|
||||
"save_pngs": args.save_pngs,
|
||||
# If -t is passed in (for transparent), this will be RGBA
|
||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
||||
"file_name": args.file_name,
|
||||
}
|
||||
config = {
|
||||
"module": get_module(args.file),
|
||||
"scene_names": args.scene_names,
|
||||
"open_video_upon_completion": args.preview,
|
||||
"show_file_in_finder": args.show_file_in_finder,
|
||||
# By default, write to file
|
||||
"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",
|
||||
"file_writer_config": file_writer_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"ignore_waits": args.preview,
|
||||
"write_all": args.write_all,
|
||||
"output_file_name": output_file_name,
|
||||
"start_at_animation_number": args.start_at_animation_number,
|
||||
"end_at_animation_number": None,
|
||||
"sound": args.sound,
|
||||
|
@ -242,7 +230,7 @@ def get_configuration(args):
|
|||
config["start_at_animation_number"] = int(stan)
|
||||
|
||||
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"],
|
||||
])
|
||||
return config
|
||||
|
|
|
@ -25,6 +25,7 @@ with open("media_dir.txt", 'w') as media_file:
|
|||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_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__))
|
||||
FILE_DIR = os.path.join(THIS_DIR, "files")
|
||||
|
|
|
@ -12,46 +12,43 @@ from manimlib.utils.sounds import play_finish_sound
|
|||
import manimlib.constants
|
||||
|
||||
|
||||
def handle_scene(scene, **config):
|
||||
def open_file_if_needed(file_writer, **config):
|
||||
if config["quiet"]:
|
||||
curr_stdout = sys.stdout
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if config["show_last_frame"]:
|
||||
scene.save_image(mode=config["saved_image_mode"])
|
||||
open_file = any([
|
||||
config["show_last_frame"],
|
||||
config["open_video_upon_completion"],
|
||||
config["show_file_in_finder"]
|
||||
])
|
||||
if open_file:
|
||||
current_os = platform.system()
|
||||
file_path = None
|
||||
file_paths = []
|
||||
|
||||
if config["show_last_frame"]:
|
||||
file_path = scene.get_image_file_path()
|
||||
else:
|
||||
file_path = scene.get_movie_file_path()
|
||||
if config["file_writer_config"]["save_last_frame"]:
|
||||
file_paths.append(file_writer.get_image_file_path())
|
||||
if config["file_writer_config"]["write_to_movie"]:
|
||||
file_paths.append(file_writer.get_movie_file_path())
|
||||
|
||||
if current_os == "Windows":
|
||||
os.startfile(file_path)
|
||||
else:
|
||||
commands = []
|
||||
for file_path in file_paths:
|
||||
if current_os == "Windows":
|
||||
os.startfile(file_path)
|
||||
else:
|
||||
commands = []
|
||||
if (current_os == "Linux"):
|
||||
commands.append("xdg-open")
|
||||
else: # Assume macOS
|
||||
commands.append("open")
|
||||
|
||||
if (current_os == "Linux"):
|
||||
commands.append("xdg-open")
|
||||
else: # Assume macOS
|
||||
commands.append("open")
|
||||
if config["show_file_in_finder"]:
|
||||
commands.append("-R")
|
||||
|
||||
if config["show_file_in_finder"]:
|
||||
commands.append("-R")
|
||||
commands.append(file_path)
|
||||
|
||||
commands.append(file_path)
|
||||
|
||||
# commands.append("-g")
|
||||
FNULL = open(os.devnull, 'w')
|
||||
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
||||
FNULL.close()
|
||||
# commands.append("-g")
|
||||
FNULL = open(os.devnull, 'w')
|
||||
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
||||
FNULL.close()
|
||||
|
||||
if config["quiet"]:
|
||||
sys.stdout.close()
|
||||
|
@ -128,23 +125,18 @@ def main(config):
|
|||
"camera_config",
|
||||
"frame_duration",
|
||||
"skip_animations",
|
||||
"write_to_movie",
|
||||
"save_pngs",
|
||||
"movie_file_extension",
|
||||
"file_writer_config",
|
||||
"start_at_animation_number",
|
||||
"end_at_animation_number",
|
||||
"output_file_name",
|
||||
"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):
|
||||
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"]:
|
||||
play_finish_sound()
|
||||
except Exception:
|
||||
|
|
|
@ -293,3 +293,82 @@ class TODOStub(Scene):
|
|||
def construct(self):
|
||||
self.add(TextMobject("TODO: %s" % self.message))
|
||||
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
|
||||
|
||||
# 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):
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
from time import sleep
|
||||
import _thread as thread
|
||||
import datetime
|
||||
import inspect
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
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
|
||||
|
@ -21,12 +14,8 @@ from manimlib.container.container import Container
|
|||
from manimlib.continual_animation.continual_animation import ContinualAnimation
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
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.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):
|
||||
|
@ -34,67 +23,42 @@ class Scene(Container):
|
|||
"camera_class": Camera,
|
||||
"camera_config": {},
|
||||
"frame_duration": LOW_QUALITY_FRAME_DURATION,
|
||||
"construct_args": [],
|
||||
"file_writer_config": {},
|
||||
"skip_animations": False,
|
||||
"write_to_movie": False,
|
||||
"save_pngs": False,
|
||||
"pngs_mode": "RGBA",
|
||||
"movie_file_extension": ".mp4",
|
||||
"always_continually_update": False,
|
||||
"random_seed": 0,
|
||||
"start_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,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Perhaps allow passing in a non-empty *mobjects parameter?
|
||||
Container.__init__(self, **kwargs)
|
||||
self.camera = self.camera_class(**self.camera_config)
|
||||
self.file_writer = SceneFileWriter(
|
||||
self, **self.file_writer_config,
|
||||
)
|
||||
|
||||
self.mobjects = []
|
||||
self.continual_animations = []
|
||||
# TODO, remove need for foreground mobjects
|
||||
self.foreground_mobjects = []
|
||||
self.num_plays = 0
|
||||
self.frame_num = 0
|
||||
self.time = 0
|
||||
self.original_skipping_status = self.skip_animations
|
||||
self.stream_lock = False
|
||||
if self.random_seed is not None:
|
||||
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)
|
||||
self.construct()
|
||||
except EndSceneEarlyException:
|
||||
if hasattr(self, "writing_process"):
|
||||
self.writing_process.terminate()
|
||||
pass
|
||||
self.tear_down()
|
||||
|
||||
if self.write_to_movie:
|
||||
self.combine_movie_files()
|
||||
self.file_writer.finish()
|
||||
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):
|
||||
"""
|
||||
This is meant to be implement by any scenes which
|
||||
|
@ -116,11 +80,6 @@ class Scene(Container):
|
|||
def __str__(self):
|
||||
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):
|
||||
print("Played {} animations".format(self.num_plays))
|
||||
|
||||
|
@ -142,40 +101,7 @@ 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):
|
||||
self.camera = camera
|
||||
|
||||
|
@ -202,9 +128,9 @@ class Scene(Container):
|
|||
mobjects=None,
|
||||
background=None,
|
||||
include_submobjects=True,
|
||||
dont_update_when_skipping=True,
|
||||
ignore_skipping=True,
|
||||
**kwargs):
|
||||
if self.skip_animations and dont_update_when_skipping:
|
||||
if self.skip_animations and not ignore_skipping:
|
||||
return
|
||||
if mobjects is None:
|
||||
mobjects = list_update(
|
||||
|
@ -511,7 +437,7 @@ class Scene(Container):
|
|||
compile_method(state)
|
||||
return animations
|
||||
|
||||
def handle_animation_skipping(self):
|
||||
def update_skipping_status(self):
|
||||
if self.start_at_animation_number:
|
||||
if self.num_plays == self.start_at_animation_number:
|
||||
self.skip_animations = False
|
||||
|
@ -520,10 +446,18 @@ class Scene(Container):
|
|||
self.skip_animations = True
|
||||
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
|
||||
def play(self, *args, **kwargs):
|
||||
if self.livestreaming:
|
||||
self.stream_lock = False
|
||||
if len(args) == 0:
|
||||
warnings.warn("Called Scene.play with no animations")
|
||||
return
|
||||
|
@ -558,22 +492,10 @@ class Scene(Container):
|
|||
else:
|
||||
self.continual_update(0)
|
||||
|
||||
if self.livestreaming:
|
||||
self.stream_lock = True
|
||||
thread.start_new_thread(self.idle_stream, ())
|
||||
return self
|
||||
|
||||
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)
|
||||
self.file_writer.idle_stream()
|
||||
|
||||
def clean_up_animations(self, *animations):
|
||||
for animation in animations:
|
||||
|
@ -638,202 +560,20 @@ class Scene(Container):
|
|||
return self
|
||||
|
||||
def add_frames(self, *frames):
|
||||
self.increment_time(len(frames) * self.frame_duration)
|
||||
if self.skip_animations:
|
||||
return
|
||||
self.increment_time(len(frames) * self.frame_duration)
|
||||
if self.write_to_movie:
|
||||
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())
|
||||
for frame in frames:
|
||||
self.file_writer.write_frame(frame)
|
||||
|
||||
# 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):
|
||||
self.update_frame(dont_update_when_skipping=False)
|
||||
self.update_frame(ignore_skipping=True)
|
||||
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
|
||||
# part of some more specialized subclass optimized
|
||||
# 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 numpy as np
|
||||
|
||||
from manimlib.constants import VIDEO_DIR
|
||||
|
||||
|
||||
def add_extension_if_not_present(file_name, extension):
|
||||
# This could conceivably be smarter about handling existing differing extensions
|
||||
|
@ -18,36 +16,16 @@ def guarantee_existance(path):
|
|||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def get_scene_output_directory(scene_class):
|
||||
return guarantee_existance(os.path.join(
|
||||
VIDEO_DIR,
|
||||
scene_class.__module__.replace(".", os.path.sep)
|
||||
))
|
||||
|
||||
|
||||
def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
||||
directory = get_scene_output_directory(scene_class)
|
||||
sub_dir = "%dp%d" % (
|
||||
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 seek_full_path_from_defaults(file_name, default_dir, extensions):
|
||||
possible_paths = [file_name]
|
||||
possible_paths += [
|
||||
os.path.join(default_dir, file_name + extension)
|
||||
for extension in ["", *extensions]
|
||||
]
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
raise IOError("File {} not Found".format(file_name))
|
||||
|
||||
|
||||
def get_sorted_integer_files(directory,
|
|
@ -4,20 +4,15 @@ import os
|
|||
from PIL import Image
|
||||
|
||||
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):
|
||||
possible_paths = [
|
||||
return seek_full_path_from_defaults(
|
||||
image_file_name,
|
||||
os.path.join(RASTER_IMAGE_DIR, image_file_name),
|
||||
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".jpg"),
|
||||
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)
|
||||
default_dir=RASTER_IMAGE_DIR,
|
||||
extensions=[".jpg", ".png", ".gif"]
|
||||
)
|
||||
|
||||
|
||||
def drag_pixels(frames):
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import os
|
||||
from manimlib.constants import SOUND_DIR
|
||||
from manimlib.utils.file_ops import seek_full_path_from_defaults
|
||||
|
||||
|
||||
def play_chord(*nums):
|
||||
|
@ -28,3 +30,11 @@ def play_error_sound():
|
|||
|
||||
def play_finish_sound():
|
||||
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,
|
||||
"animal_height": 0.5,
|
||||
"final_wait_time": 30,
|
||||
"count_word_scale_val": 1,
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
|
@ -3093,9 +3094,13 @@ class ShowTwoPopulations(Scene):
|
|||
# Add counts for foxes and rabbits
|
||||
labels = self.get_pop_labels()
|
||||
num_foxes = Integer(10)
|
||||
num_foxes.scale(self.count_word_scale_val)
|
||||
num_foxes.next_to(labels[0], RIGHT)
|
||||
num_foxes.align_to(labels[0][1], DOWN)
|
||||
num_rabbits = Integer(10)
|
||||
num_rabbits.scale(self.count_word_scale_val)
|
||||
num_rabbits.next_to(labels[1], RIGHT)
|
||||
num_rabbits.align_to(labels[1][1], DOWN)
|
||||
|
||||
self.add(ContinualChangingDecimal(
|
||||
num_foxes, lambda a: get_num_foxes()
|
||||
|
@ -3156,6 +3161,8 @@ class ShowTwoPopulations(Scene):
|
|||
TextMobject("\\# Foxes: "),
|
||||
TextMobject("\\# Rabbits: "),
|
||||
)
|
||||
for label in labels:
|
||||
label.scale(self.count_word_scale_val)
|
||||
labels.arrange_submobjects(RIGHT, buff=2)
|
||||
labels.to_edge(UP)
|
||||
return labels
|
||||
|
|
|
@ -8,9 +8,7 @@ from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG
|
|||
from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
|
||||
from manimlib.config import get_module
|
||||
from manimlib.extract_scene import is_child_scene
|
||||
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
|
||||
from manimlib.utils.file_ops import get_movie_output_directory
|
||||
|
||||
|
||||
def get_sorted_scene_classes(module_name):
|
||||
|
@ -48,26 +46,29 @@ def stage_animations(module_name):
|
|||
sorted_files = []
|
||||
for scene_class in scene_classes:
|
||||
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
|
||||
pmf_dir = get_partial_movie_output_directory(
|
||||
scene_class, **output_directory_kwargs
|
||||
)
|
||||
if os.path.exists(pmf_dir):
|
||||
for extension in [".mov", ".mp4"]:
|
||||
int_files = get_sorted_integer_files(
|
||||
pmf_dir, extension=extension
|
||||
)
|
||||
for file in int_files:
|
||||
sorted_files.append(os.path.join(pmf_dir, file))
|
||||
else:
|
||||
for clip in [f for f in files if f.startswith(scene_name + ".")]:
|
||||
sorted_files.append(os.path.join(animation_dir, clip))
|
||||
# movie_dir = get_movie_output_directory(
|
||||
# scene_class, **output_directory_kwargs
|
||||
# )
|
||||
# if os.path.exists(movie_dir):
|
||||
# for extension in [".mov", ".mp4"]:
|
||||
# int_files = get_sorted_integer_files(
|
||||
# pmf_dir, extension=extension
|
||||
# )
|
||||
# for file in int_files:
|
||||
# sorted_files.append(os.path.join(pmf_dir, file))
|
||||
# else:
|
||||
|
||||
animation_subdir = os.path.dirname(animation_dir)
|
||||
# animation_subdir = os.path.dirname(animation_dir)
|
||||
count = 0
|
||||
while True:
|
||||
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):
|
||||
os.makedirs(staged_scenes_dir)
|
||||
|
@ -82,8 +83,7 @@ def stage_animations(module_name):
|
|||
symlink_name = os.path.join(
|
||||
staged_scenes_dir,
|
||||
"Scene_{:03}_{}".format(
|
||||
count,
|
||||
"".join(f.split(os.sep)[-2:])
|
||||
count, f.split(os.sep)[-1]
|
||||
)
|
||||
)
|
||||
os.symlink(f, symlink_name)
|
||||
|
|
Loading…
Add table
Reference in a new issue