示例#1
0
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
示例#2
0
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))