diff --git a/.github/workflows/test_examples.yml b/.github/workflows/test_examples.yml new file mode 100644 index 00000000..acd5c4aa --- /dev/null +++ b/.github/workflows/test_examples.yml @@ -0,0 +1,100 @@ +on: + pull_request: + paths: + - '**.py' + workflow_dispatch: + +name: Test Examples + +jobs: + build-examples: + strategy: + matrix: + manim: [manim, manimgl] + os: [macos-latest, ubuntu-latest, windows-latest] + pyversion: ['3.7', '3.8', '3.9', '3.10'] + exclude: + # excludes manimgl on Windows because if throws errors + # related to OpenGL, which seems hard to fix: + # Your graphics drivers do not support OpenGL 2.0. + - os: windows-latest + manim: manimgl + # manimgl actually requires Python >= 3.8, see: + # https://github.com/3b1b/manim/issues/1808 + - manim: manimgl + pyversion: '3.7' + # We only test Python 3.10 on Windows and MacOS + - os: windows-latest + pyversion: '3.7' + - os: windows-latest + pyversion: '3.8' + - os: windows-latest + pyversion: '3.9' + - os: macos-latest + pyversion: '3.7' + - os: macos-latest + pyversion: '3.8' + - os: macos-latest + pyversion: '3.9' + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.pyversion }} + - name: Append to Path on MacOS and Ubuntu + if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' + run: echo "${HOME}/.local/bin" >> $GITHUB_PATH + - name: Append to Path on Windows + if: matrix.os == 'windows-latest' + run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install MacOS dependencies (manim only) + if: matrix.os == 'macos-latest' && matrix.manim == 'manim' + run: brew install py3cairo + - name: Install MacOS dependencies + if: matrix.os == 'macos-latest' + run: brew install ffmpeg + - name: Install Ubuntu dependencies + if: matrix.os == 'ubuntu-latest' + run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb + - name: Install Windows dependencies + if: matrix.os == 'windows-latest' + run: choco install ffmpeg + - name: Install manim on MacOs + if: matrix.manim == 'manim' && matrix.os == 'macos-latest' + run: pip3 install --user manim + - name: Install manim on Ubuntu and Windows + if: matrix.manim == 'manim' && (matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest') + run: python -m pip install --user manim + - name: Install manimgl on MacOs + if: matrix.manim == 'manimgl' && matrix.os == 'macos-latest' + run: pip3 install --user manimgl + - name: Install manimgl on Ubuntu and Windows + if: matrix.manim == 'manimgl' && matrix.os != 'macos-latest' + run: python -m pip install --user manimgl + - name: Install manim-slides on MacOS + if: matrix.os == 'macos-latest' + run: pip3 install --user . + - name: Install manim-slides on Ubuntu + if: matrix.os == 'ubuntu-latest' + run: xvfb-run -a -s "-screen 0 1400x900x24" python -m pip install --user . + - name: Install manim-slides on Windows + if: matrix.os == 'windows-latest' + run: pip3 install -e . + - name: Build slides with manim + if: matrix.manim == 'manim' + run: python -m manim -ql example.py Example ThreeDExample + - name: Build slides with manimgl on Ubuntu + if: matrix.manim == 'manimgl' && matrix.os == 'ubuntu-latest' + run: xvfb-run -a -s "-screen 0 1400x900x24" manim-render -l example.py Example ThreeDExample + - name: Build slides with manimgl on MacOS or Windows + if: matrix.manim == 'manimgl' && (matrix.os == 'macos-latest' || matrix.os == 'windows-latest') + run: manimgl -l example.py Example ThreeDExample + - name: Test slides on Ubuntu + if: matrix.os == 'ubuntu-latest' + run: xvfb-run -a -s "-screen 0 1400x900x24" manim-slides Example ThreeDExample --skip-all + - name: Test slides on MacOS or Windows + if: matrix.os == 'macos-latest' || matrix.os == 'windows-latest' + run: manim-slides Example ThreeDExample --skip-all diff --git a/example.py b/example.py index 2938047d..f0e8d8eb 100644 --- a/example.py +++ b/example.py @@ -1,6 +1,15 @@ -# If you want to use manimgl, uncomment change -# manim to manimlib -from manimlib import * +import sys + +if "manim" in sys.modules: + from manim import * + + MANIMGL = False +elif "manimlib" in sys.modules: + from manimlib import * + + MANIMGL = True +else: + raise ImportError("This script must be run with either `manim` or `manimgl`") from manim_slides import Slide, ThreeDSlide @@ -31,37 +40,88 @@ def construct(self): self.play(dot.animate.move_to(ORIGIN)) -class ThreeDExample(ThreeDSlide): - def construct(self): - axes = ThreeDAxes() - circle = Circle(radius=3, color=BLUE) - dot = Dot(color=RED) +# For ThreeDExample, things are different - self.add(axes) +if not MANIMGL: - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + class ThreeDExample(ThreeDSlide): + def construct(self): + axes = ThreeDAxes() + circle = Circle(radius=3, color=BLUE) + dot = Dot(color=RED) - self.play(GrowFromCenter(circle)) - self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4) + self.add(axes) - self.pause() + self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) - self.start_loop() - self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear) - self.end_loop() + self.play(GrowFromCenter(circle)) + self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4) - self.stop_ambient_camera_rotation() - self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) + self.pause() - self.play(dot.animate.move_to(ORIGIN)) - self.pause() + self.start_loop() + self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear) + self.end_loop() - self.play(dot.animate.move_to(RIGHT * 3)) - self.pause() + self.stop_ambient_camera_rotation() + self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) - self.start_loop() - self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) - self.end_loop() + self.play(dot.animate.move_to(ORIGIN)) + self.pause() - # Each slide MUST end with an animation (a self.wait is considered an animation) - self.play(dot.animate.move_to(ORIGIN)) + self.play(dot.animate.move_to(RIGHT * 3)) + self.pause() + + self.start_loop() + self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) + self.end_loop() + + # Each slide MUST end with an animation (a self.wait is considered an animation) + self.play(dot.animate.move_to(ORIGIN)) + +else: + # WARNING: 3b1b's manim change how ThreeDScene work, + # this is why things have to be managed differently. + class ThreeDExample(Slide): + CONFIG = { + "camera_class": ThreeDCamera, + } + + def construct(self): + axes = ThreeDAxes() + circle = Circle(radius=3, color=BLUE) + dot = Dot(color=RED) + + self.add(axes) + + frame = self.camera.frame + frame.set_euler_angles( + theta=30 * DEGREES, + phi=75 * DEGREES, + gamma=0, + ) + + self.play(GrowFromCenter(circle)) + updater = lambda m, dt: m.increment_theta((75 * DEGREES / 4) * dt) + frame.add_updater(updater) + + self.pause() + + self.start_loop() + self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear) + self.end_loop() + + frame.remove_updater(updater) + self.play(frame.animate.set_theta(30 * DEGREES)) + self.play(dot.animate.move_to(ORIGIN)) + self.pause() + + self.play(dot.animate.move_to(RIGHT * 3)) + self.pause() + + self.start_loop() + self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) + self.end_loop() + + # Each slide MUST end with an animation (a self.wait is considered an animation) + self.play(dot.animate.move_to(ORIGIN)) diff --git a/manim_slides/__version__.py b/manim_slides/__version__.py index 1e3bed4c..3348d7f9 100644 --- a/manim_slides/__version__.py +++ b/manim_slides/__version__.py @@ -1 +1 @@ -__version__ = "3.2.2" +__version__ = "3.2.3" diff --git a/manim_slides/present.py b/manim_slides/present.py index 81909516..ac648dcd 100644 --- a/manim_slides/present.py +++ b/manim_slides/present.py @@ -174,10 +174,18 @@ def update_state(self, state): class Display: - def __init__(self, presentations, config, start_paused=False, fullscreen=False): + def __init__( + self, + presentations, + config, + start_paused=False, + fullscreen=False, + skip_all=False, + ): self.presentations = presentations self.start_paused = start_paused self.config = config + self.skip_all = skip_all self.state = State.PLAYING self.lastframe = None @@ -206,7 +214,7 @@ def resize_frame_to_screen(self, frame: np.ndarray): scale = min(scale_height, scale_width) - return cv2.resize(frame, (int(scale * frame_height, scale * frame_width))) + return cv2.resize(frame, (int(scale * frame_height), int(scale * frame_width))) @property def current_presentation(self): @@ -293,7 +301,9 @@ def handle_key(self): ): self.current_presentation.next() self.state = State.PLAYING - elif self.state == State.PLAYING and self.config.CONTINUE.match(key): + elif ( + self.state == State.PLAYING and self.config.CONTINUE.match(key) + ) or self.skip_all: self.current_presentation.next() elif self.config.BACK.match(key): if self.current_presentation.current_slide_i == 0: @@ -356,8 +366,15 @@ def _list_scenes(folder): is_flag=True, help="Show the next animation first frame as last frame (hack).", ) +@click.option( + "--skip-all", + is_flag=True, + help="Skip all slides, useful the test if slides are working.", +) @click.help_option("-h", "--help") -def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next): +def present( + scenes, config_path, folder, start_paused, fullscreen, last_frame_next, skip_all +): """Present the different scenes.""" if len(scenes) == 0: @@ -411,6 +428,10 @@ def value_proc(value: str): config = Config() display = Display( - presentations, config=config, start_paused=start_paused, fullscreen=fullscreen + presentations, + config=config, + start_paused=start_paused, + fullscreen=fullscreen, + skip_all=skip_all, ) display.run() diff --git a/setup.py b/setup.py index 9000aabe..0de42140 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ "numpy>=1.19.3", "pydantic>=1.9.1", "opencv-python>=4.6", - "tqdm", + "tqdm>=4.62.3", ], classifiers=[ "Programming Language :: Python :: 3",