jeertmans/manim-slides

[FEATURE] Integrate with `marimo` notebook

Opened this issue · 3 comments

Terms

Description

Marimo is an open-source reactive notebook for Python — reproducible, git-friendly, executable as a script, and shareable as an app. As an alternative to jupyter/ipython.

There is ipython_magic, but not work for marimo(need invoke and ipython).So I make a monkey-patch, but It's better to add support natively.

The monkey-patch add Slide._repr_html_ like ipython_magic. Document about this api

Monkey patch

from manim_slides import Slide

import logging
import mimetypes
from pathlib import Path

from manim import config, logger
from manim.constants import RendererType
from manim.renderer.shader import shader_program_cache

from manim_slides.convert import (
    RevealJS,
    Template,
    file_to_data_uri,
    get_duration_ms,
    os,
)
from manim_slides.present import get_scenes_presentation_config


class PatchedRevealJS(RevealJS):
    def convert(self) -> str:
        if self.data_uri:
            assets_dir = Path("")  # Actually we won't care.
        else:
            raise NotImplementedError()

        revealjs_template = Template(self.load_template())

        options = self.model_dump()
        options["assets_dir"] = assets_dir

        has_notes = any(
            slide_config.notes != ""
            for presentation_config in self.presentation_configs
            for slide_config in presentation_config.slides
        )

        return revealjs_template.render(
            file_to_data_uri=file_to_data_uri,
            get_duration_ms=get_duration_ms,
            has_notes=has_notes,
            env=os.environ,
            **options,
        )


def _extend_mime(cls):
    logging.getLogger("manim-slides").setLevel(logging.getLogger("manim").level)

    renderer = None
    if config.renderer == RendererType.OPENGL:
        from manim.renderer.opengl_renderer import OpenGLRenderer

        renderer = OpenGLRenderer()

    try:
        scene = cls(renderer=renderer)
        scene.render()
    finally:
        # Shader cache becomes invalid as the context is destroyed
        shader_program_cache.clear()

        # Close OpenGL window here instead of waiting for the main thread to
        # finish causing the window to stay open and freeze
        if renderer is not None and renderer.window is not None:
            renderer.window.close()

    if config["output_file"] is None:
        logger.info("No output file produced")
        return

    file_type = mimetypes.guess_type(config["output_file"])[0] or "video/mp4"

    if not file_type.startswith("video"):
        raise ValueError(f"Manim Slides only supports video files, not {file_type}")

    presentation_configs = get_scenes_presentation_config(
        [cls.__name__], Path("./slides")
    )
    out_data = PatchedRevealJS(
        presentation_configs=presentation_configs, data_uri=True
    ).convert()

    return """<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" srcdoc="{srcdoc}"></iframe></div>""".format(
        srcdoc=out_data.replace('"', "'")
    )


Slide._repr_html_ = classmethod(_extend_mime)

Usage:

from manim import Scene, Square, Circle, BLUE, GREEN, PINK, Create, Transform
from manim_slides.slide import Slide

import helper

blue_circle = Circle(color=BLUE, fill_opacity=0.5)
green_square = Square(color=GREEN, fill_opacity=0.8)
class CircleToSquare(Slide):
    def construct(self):
        self.play(Create(blue_circle))
        self.next_slide()

        self.play(Transform(blue_circle, green_square))

CircleToSquare

Screenshots

image

Additional information

No response

Hello @way-zer, this looks very interesting!

I have a small question: how do you pass rendering arguments?

Maybe it would be great if Slide had a render_html method that returns RevealJS class instance, and add __repr_html__ to RevealJS. The render_html could accept *args: list (or args: Sequence[str]) and use them to control the rendering.

Hello @way-zer, this looks very interesting!

I have a small question: how do you pass rendering arguments?

Maybe it would be great if Slide had a render_html method that returns RevealJS class instance, and add __repr_html__ to RevealJS. The render_html could accept *args: list (or args: Sequence[str]) and use them to control the rendering.

Good question, This is what I thought before, but it doesn't work.

# not work because render is delayed
with tempconfig({"frame_height": 100.0}):
    CircleToSquare

I plan to discuss with marimo that it should provide a way to render with contextmanager.

Maybe, wrapper with config, and config is optional.

from manim import tempconfig
class withConfig:
    def __init__(self, s, cfg):
        self.s = s
        self.cfg = cfg
    def _repr_html_(self):
        with tempconfig(self.cfg):
            return self.s._repr_html_()

withConfig(CircleToSquare,{})

This new feature doesn't seem to be unique to marimo, and will likely work on Jupyter Notebook too. I think adding new methods to both Slide and RevealJS can be good enough.

Something like this:

class Slide:
    def render_html(args: list[str]) -> RevealJS:
        # render slides with optional arguments and return RevealJS class instance

    def _repr_html_(self) -> HTML:
        self.render_html([])._repr_html_()

class RevealJS:
    def _repr_html_(self) -> HTML:
        # convert to HTML and generate output cell