def add_background_music(self, path: str) -> CodeScene: """ Adds background music for the video. Can be combined with `wait_util_beat` or `wait_until_measure` to automatically time animations Args: path: The file path of the music file, usually an mp3 file. """ self.music = BackgroundMusic(path) return self
def add_background_music(self, path: str): self.music = BackgroundMusic(path)
class CodeScene(MovingCameraScene): CONFIG = { "code_font": "Ubuntu Mono", "text_font": "Helvetica", "code_theme": "fruity", } def __init__( self, *args, **kwargs, ): super().__init__(*args, **kwargs) self.caption = None self.col_width = self.camera_frame.get_width() / 3 self.music: Optional[BackgroundMusic] = None self.pauses = [] def add_background_music(self, path: str): self.music = BackgroundMusic(path) def tear_down(self): super().tear_down() if self.music: self.time = 0 file = fit_audio(self.music.file, self.renderer.time + 2) self.add_sound(file) os.remove(file) if self.pauses: config[ "slide_videos"] = self.renderer.file_writer.partial_movie_files[:] config["slide_stops"].extend(self.pauses) config[ "movie_file_path"] = self.renderer.file_writer.movie_file_path def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): if config.get("show_slides"): print("In slide mode, skipping wait") self.pauses.append( len(self.renderer.file_writer.partial_movie_files) - 1) else: super().wait(duration, stop_condition) def wait_until_beat(self, wait_time: Union[float, int]): if self.music: adjusted_delay = self.music.next_beat( self.renderer.time + wait_time) - self.renderer.time self.wait(adjusted_delay) else: self.wait(wait_time) def wait_until_measure(self, wait_time: Union[float, int], post: Union[float, int] = 0): if self.music: adjusted_delay = self.music.next_measure( self.renderer.time + wait_time) - self.renderer.time adjusted_delay += post self.wait(adjusted_delay) else: self.wait(wait_time) def add_background(self, path: str) -> ImageMobject: background = ImageMobject(path, height=self.camera_frame.get_height()) background.stretch_to_fit_width(self.camera_frame.get_width()) self.add(background) return background def animate_code_comments( self, path: str, title: str = None, keep_comments: bool = False, start_line: int = 1, end_line: Optional[int] = None, reset_at_end: bool = True, ) -> Code: parent = None if title: title = PangoText(title, font=self.CONFIG["text_font"]).to_edge(edge=UP) self.add(title) code_group = VGroup().next_to(title, direction=DOWN) self.add(code_group) parent = code_group code, comments = comment_parser.parse(path, keep_comments=keep_comments, start_line=start_line, end_line=end_line) with NamedTemporaryFile(suffix=f".{path.split('.')[-1]}") as f: f.writelines([line.encode() for line in code]) f.flush() tex = self.create_code(f.name, line_no_from=start_line) if parent: parent.add(tex) self.play(ShowCreation(tex)) self.wait() for comment in comments: self.highlight_lines(tex, comment.start, comment.end, comment.caption) if reset_at_end: self.highlight_none(tex) return tex def highlight_lines(self, tex: Code, start: int = 1, end: int = -1, caption: Optional[str] = None): if end == -1: end = len(tex.line_numbers) + 1 if hasattr(tex, "line_no_from"): start -= tex.line_no_from - 1 end -= tex.line_no_from - 1 def in_range(number: int): return start <= number <= end pre_actions = [] actions = [] post_actions = [] if caption: caption = "\n".join(wrap(caption, 25)) if self.caption: pre_actions.append(FadeOut(self.caption)) else: self.play(ApplyMethod(tex.to_edge)) self.caption = PangoText( caption, font=self.CONFIG["text_font"], size=self.col_width / 10 * 0.9).add_background_rectangle(buff=MED_SMALL_BUFF) self.caption.next_to(tex, RIGHT) self.caption.align_to(tex.line_numbers[start - 1], UP) actions.append(FadeIn(self.caption)) elif self.caption: actions.append(FadeOut(self.caption)) post_actions += [ApplyMethod(tex.center)] self.caption = None # highlight code lines actions += [ ApplyMethod( tex.code[line_no].set_opacity, 1 if in_range(line_no + 1) else 0.3, ) for line_no in range(len(tex.code)) ] # highlight line numbers actions += [ ApplyMethod( tex.line_numbers[line_no].set_opacity, 1 if in_range(line_no + 1) else 0.3, ) for line_no in range(len(tex.code)) ] if pre_actions: self.play(*pre_actions) if actions: self.play(*actions) if caption: wait_time = len(caption) / (200 * 5 / 60) self.wait_until_measure(wait_time, -1.5) if post_actions: self.play(*post_actions) def highlight_line(self, tex: Code, number: int = -1, caption: Optional[str] = None): return self.highlight_lines(tex, number, number, caption=caption) def highlight_none(self, tex: Code): start_line = tex.line_no_from return self.highlight_lines(tex, start_line, len(tex.code) + start_line, caption=None) def create_code(self, path: str, **kwargs) -> Code: tex = Code(path, font=self.CONFIG["code_font"], style=self.CONFIG["code_theme"], **kwargs) x_scale = (self.col_width * 2) / tex.get_width() y_scale = self.camera_frame.get_height() * 0.95 / tex.get_height() tex.scale(min(x_scale, y_scale)) return tex
class CodeScene(MovingCameraScene): """ This class serves as a convenience class for animating code walkthroughs in as little work as possible. For more control, use `Code` or `PartialCode` with the `HighlightLines` and `HighlightNone` transitions directly. """ def __init__( self, *args, code_font="Ubuntu Mono", text_font="Helvetica", code_theme="fruity", **kwargs, ): super().__init__(*args, **kwargs) self.caption = None self.code_font = code_font self.text_font = text_font self.code_theme = code_theme self.col_width = None self.music: Optional[BackgroundMusic] = None self.pauses = {} def setup(self): super().setup() self.col_width = self.camera_frame.get_width() / 3 def add_background_music(self, path: str) -> CodeScene: """ Adds background music for the video. Can be combined with `wait_util_beat` or `wait_until_measure` to automatically time animations Args: path: The file path of the music file, usually an mp3 file. """ self.music = BackgroundMusic(path) return self def tear_down(self): super().tear_down() if self.music: self.time = 0 file = fit_audio(self.music.file, self.renderer.time + 2) self.add_sound(file) os.remove(file) if self.pauses: config[ "slide_videos"] = self.renderer.file_writer.partial_movie_files[:] config["slide_stops"].update(self.pauses) config[ "movie_file_path"] = self.renderer.file_writer.movie_file_path def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): """ Either waits like normal or if the codevidgen script is used and the "--slides" flag is used, it will treat these calls as breaks between slides """ if config.get("show_slides"): print("In slide mode, skipping wait") super().wait(0.5) index = len(self.renderer.file_writer.partial_movie_files) - 1 self.pauses[index] = [] else: super().wait(duration, stop_condition) def play_movie(self, path: str): if config.get("show_slides"): index = len(self.renderer.file_writer.partial_movie_files) - 1 self.pauses[index].append(path) def wait_until_beat(self, wait_time: Union[float, int]): """ Waits until the next music beat, only works with `add_background_music` """ if self.music: adjusted_delay = self.music.next_beat( self.renderer.time + wait_time) - self.renderer.time self.wait(adjusted_delay) else: self.wait(wait_time) def wait_until_measure(self, wait_time: Union[float, int], post: Union[float, int] = 0): """ Waits until the next music measure, only works with `add_background_music` """ if self.music: adjusted_delay = self.music.next_measure( self.renderer.time + wait_time) - self.renderer.time adjusted_delay += post self.wait(adjusted_delay) else: self.wait(wait_time) def add_background(self, path: str) -> ImageMobject: """ Adds a full screen background image. The image will be stretched to the full width. Args: path: The file path of the image file """ background = ImageMobject(path, height=self.camera_frame.get_height()) background.stretch_to_fit_width(self.camera_frame.get_width()) self.add(background) return background def animate_code_comments( self, path: str, title: str = None, keep_comments: bool = False, start_line: int = 1, end_line: Optional[int] = None, reset_at_end: bool = True, ) -> Code: """ Parses a code file, displays it or a section of it, and animates comments Args: path: The source code file path title: The title or file path if not provided keep_comments: Whether to keep comments or strip them when displaying start_line: The start line number, used for displaying only a partial file end_line: The end line number, defaults to the end of the file reset_at_end: Whether to reset the code to full screen at the end or not """ code, comments = comment_parser.parse(path, keep_comments=keep_comments, start_line=start_line, end_line=end_line) tex = AutoScaled( PartialCode(code=code, start_line=start_line, style=self.code_theme)) if title is None: title = path title = Text(title, color=WHITE).to_edge(edge=UP) self.add(title) tex.next_to(title, DOWN) self.play(ShowCreation(tex)) self.wait() for comment in comments: self.highlight_lines(tex, comment.start, comment.end, comment.caption) if self.caption: self.play(FadeOut(self.caption)) self.caption = None if reset_at_end: self.play(HighlightNone(tex)) self.play(ApplyMethod(tex.full_size)) return tex def highlight_lines(self, code: Code, start: int = 1, end: int = -1, caption: Optional[str] = None): """ Convenience method for animating a code object. Args: code: The code object, must be wrapped in `AutoScaled` start: The start line number end: The end line number, defaults to the end of the file caption: The text to display with the highlight """ if end == -1: end = len(code.line_numbers) + code.line_no_from layout = ColumnLayout(columns=3) actions = [] if caption and not self.caption: self.play( ApplyMethod( code.fill_between_x, layout.get_x(1, span=2, direction=LEFT), layout.get_x(1, span=2, direction=RIGHT), )) if self.caption: actions.append(FadeOut(self.caption)) self.caption = None if not caption: self.play(ApplyMethod(code.full_size)) else: callout = TextBox(caption, text_attrs=dict(size=0.4, font=DEFAULT_FONT)) callout.align_to(code.line_numbers[start - code.line_no_from], UP) callout.set_x(layout.get_x(3), LEFT) actions += [HighlightLines(code, start, end), FadeIn(callout)] self.caption = callout self.play(*actions) if not self.caption: self.play(ApplyMethod(code.full_size)) else: wait_time = len(self.caption.text) / (200 * 5 / 60) self.wait_until_measure(wait_time, -1.5) def highlight_line(self, code: Code, number: int = -1, caption: Optional[str] = None): """ Convenience method for highlighting a single line Args: code: The code object, must be wrapped in `AutoScaled` number: The line number caption: The text to display with the highlight """ return self.highlight_lines(code, number, number, caption=caption) def highlight_none(self, code: Code): """ Convenience method for resetting any existing highlighting. Args: code: The code object, must be wrapped in `AutoScaled` """ if self.caption: self.play(FadeOut(self.caption), HighlightNone(code)) self.caption = None self.play(ApplyMethod(code.full_size)) def create_code(self, path: str, **kwargs) -> Code: """ Convenience method for creating an autoscaled code object. Args: path: The source code file path """ return AutoScaled( Code(path, font=self.code_font, style=self.code_theme, **kwargs))