mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 03:05:21 +08:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
519dd47ac6 | |||
0565a99639 | |||
8dfe600656 | |||
03107867ab | |||
bf64962c46 | |||
97e7bf8cb0 | |||
1bca2683e1 | |||
d6bb82261c | |||
0c682e4ec9 | |||
2f0453c9a6 | |||
85ea9f3096 | |||
1ae8db7966 | |||
82cccc3fc2 | |||
726b0abf5a | |||
80f4f4e3f7 | |||
82eebae686 | |||
7367cc2cb5 | |||
f26541eb32 | |||
06890ceacd | |||
9aa715a0e4 | |||
a373bdb460 | |||
e3e79617c0 | |||
668de2c023 | |||
929caec018 | |||
48cc3343bd | |||
144e7dac5b | |||
8b56f42183 | |||
534bc21672 | |||
b1a8768963 | |||
422e355758 | |||
3eb9fa0b74 | |||
8f519ed134 | |||
916e2aa2ab | |||
4d5f664348 | |||
cb6a5bb35f |
1
.flake8
1
.flake8
@ -1,4 +1,5 @@
|
||||
[flake8]
|
||||
min_python_version = 3.7
|
||||
extend-ignore =
|
||||
# E501: line too long
|
||||
E501,
|
||||
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [jeertmans]
|
10
.github/workflows/pages.yml
vendored
10
.github/workflows/pages.yml
vendored
@ -30,14 +30,22 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
- name: Install Linux Dependencies
|
||||
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg
|
||||
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
|
||||
- name: Install Python dependencies
|
||||
run: pip install manim sphinx sphinx_click furo
|
||||
- name: Install local Python package
|
||||
run: pip install -e .
|
||||
- name: Build animation and convert it into HTML slides
|
||||
run: |
|
||||
manim example.py ConvertExample
|
||||
manim-slides convert ConvertExample docs/source/_static/slides.html -cembedded=true -ccontrols=true
|
||||
- name: Build docs
|
||||
run: cd docs && make html
|
||||
- name: Upload artifact
|
||||
|
15
.github/workflows/python-publish.yml
vendored
15
.github/workflows/python-publish.yml
vendored
@ -1,4 +1,4 @@
|
||||
# From: https://github.com/pypa/cibuildwheel
|
||||
# Modified from: https://github.com/pypa/cibuildwheel
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
@ -18,20 +18,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Used to host cibuildwheel
|
||||
- uses: actions/setup-python@v2
|
||||
|
||||
- name: Install deps
|
||||
run: sudo apt-get install libsdl-pango-dev
|
||||
|
||||
- name: Install packages
|
||||
run: python -m pip install -U manim tqdm
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: python -m pip install -U setuptools wheel pip
|
||||
- name: Install build package
|
||||
run: python -m pip install -U build
|
||||
|
||||
- name: Build wheels
|
||||
run: python setup.py sdist
|
||||
run: python -m build --sdist
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
122
.github/workflows/test_examples.yml
vendored
122
.github/workflows/test_examples.yml
vendored
@ -2,102 +2,116 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
- '.github/workflows/test_examples.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
name: Test Examples
|
||||
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
MANIM_SLIDES_VERBOSITY: debug
|
||||
PYTHONFAULTHANDLER: 1
|
||||
DISPLAY: ":99"
|
||||
|
||||
jobs:
|
||||
build-examples:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
manim: [manim, manimgl]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
pyversion: ['3.7', '3.8', '3.9', '3.10']
|
||||
pyversion: ['3.8', '3.9', '3.10', '3.11']
|
||||
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'
|
||||
# We only test Python 3.11 on Windows and MacOS
|
||||
- os: windows-latest
|
||||
pyversion: '3.8'
|
||||
- os: windows-latest
|
||||
pyversion: '3.9'
|
||||
- os: macos-latest
|
||||
pyversion: '3.7'
|
||||
- os: windows-latest
|
||||
pyversion: '3.10'
|
||||
manim: manim
|
||||
- os: macos-latest
|
||||
pyversion: '3.8'
|
||||
- os: macos-latest
|
||||
pyversion: '3.9'
|
||||
- os: macos-latest
|
||||
pyversion: '3.10'
|
||||
manim: manim
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Poetry
|
||||
run: pipx install poetry
|
||||
- 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'
|
||||
cache: 'poetry'
|
||||
|
||||
# Path related stuff
|
||||
- name: Append to Path on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
echo "${HOME}/.local/bin" >> $GITHUB_PATH
|
||||
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
|
||||
- name: Append to Path on Ubuntu
|
||||
if: 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)
|
||||
|
||||
# OS depedencies
|
||||
- name: Install manim dependencies on MacOs
|
||||
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 py3cairo
|
||||
- name: Install manimgl dependencies on MacOS
|
||||
if: matrix.os == 'macos-latest' && matrix.manim == 'manimgl'
|
||||
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 manim dependencies on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
|
||||
run: |
|
||||
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
|
||||
- name: Install manimgl dependencies on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
|
||||
run: |
|
||||
sudo apt-get install libpango1.0-dev ffmpeg freeglut3-dev
|
||||
- name: Install xvfb on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
|
||||
run: |
|
||||
sudo apt-get install xvfb
|
||||
nohup Xvfb $DISPLAY &
|
||||
- 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
|
||||
|
||||
# Install Manim Slides
|
||||
- name: Install Manim Slides
|
||||
run: |
|
||||
poetry config experimental.new-installer false
|
||||
poetry install
|
||||
|
||||
# Render slides
|
||||
- name: Render slides
|
||||
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
|
||||
run: poetry run manim -ql example.py Example ThreeDExample
|
||||
- name: Render slides
|
||||
if: matrix.manim == 'manimgl'
|
||||
run: poetry run -v manimgl -l example.py Example ThreeDExample
|
||||
|
||||
# Play slides
|
||||
- name: Test slides
|
||||
run: poetry run manim-slides Example ThreeDExample --skip-all
|
||||
|
||||
# Test slides to html
|
||||
- name: Test convert on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
|
||||
run: |
|
||||
poetry run manim -ql example.py ConvertExample
|
||||
poetry run manim-slides convert --to=html ConvertExample index.html
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -19,3 +19,7 @@ videos/
|
||||
images/
|
||||
|
||||
docs/build/
|
||||
|
||||
docs/source/_static/slides_assets/
|
||||
|
||||
docs/source/_static/slides.html
|
||||
|
@ -1,26 +1,23 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black"]
|
||||
args: ["--python-version", "37", "--profile", "black"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py37"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
@ -29,15 +26,15 @@ repos:
|
||||
- flake8-tidy-imports
|
||||
- flake8-typing-imports
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.982'
|
||||
rev: 'v0.991'
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-setuptools]
|
||||
args:
|
||||
- --install-types
|
||||
- --non-interactive
|
||||
- --ignore-missing-imports
|
||||
# Disallow dynamic typing
|
||||
- --disallow-any-unimported
|
||||
- --disallow-any-generics
|
||||
- --disallow-subclassing-any
|
||||
|
||||
@ -52,7 +49,7 @@ repos:
|
||||
# Configuring warnings
|
||||
- --warn-unused-ignores
|
||||
- --warn-no-return
|
||||
- --warn-return-any
|
||||
- --no-warn-return-any
|
||||
- --warn-redundant-casts
|
||||
|
||||
# Strict equality
|
||||
|
@ -24,9 +24,7 @@ Tool for live presentations using either [Manim (community edition)](https://www
|
||||
|
||||
## Installation
|
||||
|
||||
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a [virtualenv](https://docs.python.org/3/tutorial/venv.html) for a local installation.
|
||||
|
||||
> **_NOTE:_** Startin with version 4.2, Manim Slides seems to have **troubles installing on mac M1 chips**. An issue has been created [#53](https://github.com/jeertmans/manim-slides/issues/53), and we recommend following its evolution for any update.
|
||||
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a virtual environment (e.g., [venv](https://docs.python.org/3/tutorial/venv.html)) for a local installation.
|
||||
|
||||
### Dependencies
|
||||
|
||||
@ -90,7 +88,7 @@ class Example(Slide):
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
self.pause() # Waits user to press continue to go to the next slide
|
||||
|
||||
self.wait() # The presentation directly exits after last animation
|
||||
self.wait()
|
||||
```
|
||||
|
||||
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(...)`.
|
||||
|
@ -4,17 +4,25 @@
|
||||
contain the root `toctree` directive.
|
||||
|
||||
.. image:: _static/logo.png
|
||||
:width: 600
|
||||
:width: 600px
|
||||
:align: center
|
||||
:alt: Manim Slide logo
|
||||
|
||||
Welcome to Manim Slide's CLI documentation!
|
||||
===========================================
|
||||
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
|
||||
|
||||
<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" src="_static/slides.html"></iframe></div>
|
||||
|
||||
|
||||
This page contains an exhaustive list of all the commands available with `manim-slides`.
|
||||
|
||||
If you need help installing or using Manim Slide, please refer to the `GitHub README <https://github.com/jeertmans/manim-slides>`_.
|
||||
|
||||
.. click:: manim_slides.main:cli
|
||||
.. click:: manim_slides.__main__:cli
|
||||
:prog: manim-slides
|
||||
:nested: full
|
||||
|
215
example.py
215
example.py
@ -42,6 +42,221 @@ class Example(Slide):
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
|
||||
|
||||
class ConvertExample(Slide):
|
||||
"""WARNING: this example does not seem to work with ManimGL."""
|
||||
|
||||
def tinywait(self):
|
||||
self.wait(0.1)
|
||||
|
||||
def construct(self):
|
||||
|
||||
title = VGroup(
|
||||
Text("From Manim animations", t2c={"From": BLUE}),
|
||||
Text("to slides presentation", t2c={"to": BLUE}),
|
||||
Text("with Manim Slides", t2w={"[-12:]": BOLD}, t2c={"[-13:]": YELLOW}),
|
||||
).arrange(DOWN)
|
||||
|
||||
step_1 = Text("1. In your scenes file, import Manim Slides")
|
||||
step_2 = Text("2. Replace Scene with Slide")
|
||||
step_3 = Text("3. In construct, add pauses where you need")
|
||||
step_4 = Text("4. You can also create loops")
|
||||
step_5 = Text("5. Render you scene with Manim")
|
||||
step_6 = Text("6. Open your presentation with Manim Slides")
|
||||
|
||||
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
|
||||
step.scale(0.5).to_corner(UL)
|
||||
|
||||
step = step_1
|
||||
|
||||
self.play(FadeIn(title))
|
||||
|
||||
self.pause()
|
||||
|
||||
code = Code(
|
||||
code="""from manim import *
|
||||
|
||||
|
||||
class Example(Scene):
|
||||
def construct(self):
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_1 = Code(
|
||||
code="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Scene):
|
||||
def construct(self):
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_2 = Code(
|
||||
code="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
def construct(self):
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_3 = Code(
|
||||
code="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
def construct(self):
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
self.pause()
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
self.pause()
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_4 = Code(
|
||||
code="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
def construct(self):
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
self.start_loop()
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
self.end_loop()
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
self.pause()
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_5 = Code(
|
||||
code="manim example.py Example",
|
||||
language="console",
|
||||
)
|
||||
|
||||
code_step_6 = Code(
|
||||
code="manim-slides Example",
|
||||
language="console",
|
||||
)
|
||||
|
||||
or_text = Text("or generate HTML presentation").scale(0.5)
|
||||
|
||||
code_step_7 = Code(
|
||||
code="manim-slides convert Example slides.html --open",
|
||||
language="console",
|
||||
).shift(DOWN)
|
||||
|
||||
self.clear()
|
||||
|
||||
self.play(FadeIn(code))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
self.play(FadeIn(step, shift=RIGHT))
|
||||
self.play(Transform(code, code_step_1))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
self.play(Transform(step, step_2))
|
||||
self.play(Transform(code, code_step_2))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
self.play(Transform(step, step_3))
|
||||
self.play(Transform(code, code_step_3))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
self.play(Transform(step, step_4))
|
||||
self.play(Transform(code, code_step_4))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
self.play(Transform(step, step_5))
|
||||
self.play(Transform(code, code_step_5))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
self.play(Transform(step, step_6))
|
||||
self.play(Transform(code, code_step_6))
|
||||
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
|
||||
|
||||
self.start_loop()
|
||||
self.play(FadeIn(watch_text))
|
||||
self.play(FadeOut(watch_text))
|
||||
self.end_loop()
|
||||
self.clear()
|
||||
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
self.start_loop()
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
self.end_loop()
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
self.remove(dot)
|
||||
self.add(square)
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.play(Rotate(square, angle=PI / 4))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
learn_more_text = (
|
||||
VGroup(
|
||||
Text("Learn more about Manim Slides:"),
|
||||
Text("https://github.com/jeertmans/manim-slides", color=YELLOW),
|
||||
)
|
||||
.arrange(DOWN)
|
||||
.scale(0.75)
|
||||
)
|
||||
|
||||
self.play(Transform(square, learn_more_text))
|
||||
self.tinywait()
|
||||
|
||||
|
||||
# For ThreeDExample, things are different
|
||||
|
||||
if not MANIMGL:
|
||||
|
6
manim-slides.qrc
Normal file
6
manim-slides.qrc
Normal file
@ -0,0 +1,6 @@
|
||||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<qresource>
|
||||
<file alias="icon.png">static/icon.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
73
manim_slides/__main__.py
Normal file
73
manim_slides/__main__.py
Normal file
@ -0,0 +1,73 @@
|
||||
import json
|
||||
|
||||
import click
|
||||
import requests
|
||||
from click_default_group import DefaultGroup
|
||||
|
||||
from . import __version__
|
||||
from .convert import convert
|
||||
from .manim import logger
|
||||
from .present import list_scenes, present
|
||||
from .wizard import init, wizard
|
||||
|
||||
|
||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||
@click.option(
|
||||
"--notify-outdated-version/--silent",
|
||||
" /-S",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Check if a new version of Manim Slides is available.",
|
||||
)
|
||||
@click.version_option(__version__, "-v", "--version")
|
||||
@click.help_option("-h", "--help")
|
||||
def cli(notify_outdated_version: bool) -> None:
|
||||
"""
|
||||
Manim Slides command-line utilities.
|
||||
|
||||
If no command is specified, defaults to `present`.
|
||||
"""
|
||||
# Code below is mostly a copy from:
|
||||
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
|
||||
if notify_outdated_version:
|
||||
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
|
||||
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
|
||||
try:
|
||||
req_info: requests.models.Response = requests.get(
|
||||
manim_info_url, timeout=10
|
||||
)
|
||||
req_info.raise_for_status()
|
||||
stable = req_info.json()["info"]["version"]
|
||||
if stable != __version__:
|
||||
click.echo(
|
||||
"You are using Manim Slides version "
|
||||
+ click.style(f"v{__version__}", fg="red")
|
||||
+ ", but version "
|
||||
+ click.style(f"v{stable}", fg="green")
|
||||
+ " is available."
|
||||
)
|
||||
click.echo(
|
||||
"You should consider upgrading via "
|
||||
+ click.style("pip install -U manim-slides", fg="yellow")
|
||||
)
|
||||
except requests.exceptions.HTTPError:
|
||||
logger.debug(f"HTTP Error: {warn_prompt}")
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.debug(f"Connection Error: {warn_prompt}")
|
||||
except requests.exceptions.Timeout:
|
||||
logger.debug(f"Timed Out: {warn_prompt}")
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(warn_prompt)
|
||||
logger.debug(f"Error decoding JSON from {manim_info_url}")
|
||||
except Exception:
|
||||
logger.debug(f"Something went wrong: {warn_prompt}")
|
||||
|
||||
|
||||
cli.add_command(convert)
|
||||
cli.add_command(init)
|
||||
cli.add_command(list_scenes)
|
||||
cli.add_command(present)
|
||||
cli.add_command(wizard)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1 +1 @@
|
||||
__version__ = "4.3.0"
|
||||
__version__ = "4.7.1"
|
||||
|
@ -1,26 +1,31 @@
|
||||
from typing import Callable
|
||||
from typing import Any, Callable
|
||||
|
||||
import click
|
||||
from click import Context, Parameter
|
||||
|
||||
from .defaults import CONFIG_PATH
|
||||
from .defaults import CONFIG_PATH, FOLDER_PATH
|
||||
from .manim import logger
|
||||
|
||||
F = Callable[..., Any]
|
||||
Wrapper = Callable[[F], F]
|
||||
|
||||
def config_path_option(function: Callable) -> Callable:
|
||||
|
||||
def config_path_option(function: F) -> F:
|
||||
"""Wraps a function to add configuration path option."""
|
||||
return click.option(
|
||||
wrapper: Wrapper = click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_path",
|
||||
metavar="FILE",
|
||||
default=CONFIG_PATH,
|
||||
type=click.Path(dir_okay=False),
|
||||
help="Set path to configuration file.",
|
||||
show_default=True,
|
||||
)(function)
|
||||
)
|
||||
return wrapper(function)
|
||||
|
||||
|
||||
def config_options(function: Callable) -> Callable:
|
||||
def config_options(function: F) -> F:
|
||||
"""Wraps a function to add configuration options."""
|
||||
function = config_path_option(function)
|
||||
function = click.option(
|
||||
@ -35,7 +40,7 @@ def config_options(function: Callable) -> Callable:
|
||||
return function
|
||||
|
||||
|
||||
def verbosity_option(function: Callable) -> Callable:
|
||||
def verbosity_option(function: F) -> F:
|
||||
"""Wraps a function to add verbosity option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
@ -45,7 +50,7 @@ def verbosity_option(function: Callable) -> Callable:
|
||||
|
||||
logger.setLevel(value)
|
||||
|
||||
return click.option(
|
||||
wrapper: Wrapper = click.option(
|
||||
"-v",
|
||||
"--verbosity",
|
||||
type=click.Choice(
|
||||
@ -55,5 +60,23 @@ def verbosity_option(function: Callable) -> Callable:
|
||||
help="Verbosity of CLI output",
|
||||
default=None,
|
||||
expose_value=False,
|
||||
envvar="MANIM_SLIDES_VERBOSITY",
|
||||
show_envvar=True,
|
||||
callback=callback,
|
||||
)(function)
|
||||
)
|
||||
|
||||
return wrapper(function)
|
||||
|
||||
|
||||
def folder_path_option(function: F) -> F:
|
||||
"""Wraps a function to add folder path option."""
|
||||
wrapper: Wrapper = click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)
|
||||
|
||||
return wrapper(function)
|
||||
|
@ -1,14 +1,30 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Set
|
||||
from typing import Callable, Dict, List, Optional, Set, Union
|
||||
|
||||
from pydantic import BaseModel, root_validator, validator
|
||||
from PyQt5.QtCore import Qt
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from .manim import logger
|
||||
from .manim import FFMPEG_BIN, logger
|
||||
|
||||
|
||||
class Key(BaseModel):
|
||||
def merge_basenames(files: List[str]) -> str:
|
||||
"""
|
||||
Merge multiple filenames by concatenating basenames.
|
||||
"""
|
||||
|
||||
dirname = os.path.dirname(files[0])
|
||||
_, ext = os.path.splitext(files[0])
|
||||
|
||||
basename = "_".join(os.path.splitext(os.path.basename(file))[0] for file in files)
|
||||
|
||||
return os.path.join(dirname, basename + ext)
|
||||
|
||||
|
||||
class Key(BaseModel): # type: ignore
|
||||
"""Represents a list of key codes, with optionally a name."""
|
||||
|
||||
ids: Set[int]
|
||||
@ -32,7 +48,7 @@ class Key(BaseModel):
|
||||
return m
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
class Config(BaseModel): # type: ignore
|
||||
"""General Manim Slides config"""
|
||||
|
||||
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
|
||||
@ -44,8 +60,8 @@ class Config(BaseModel):
|
||||
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
|
||||
|
||||
@root_validator
|
||||
def ids_are_unique_across_keys(cls, values):
|
||||
ids = set()
|
||||
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
|
||||
ids: Set[int] = set()
|
||||
|
||||
for key in values.values():
|
||||
if len(ids.intersection(key.ids)) != 0:
|
||||
@ -71,7 +87,7 @@ class SlideType(str, Enum):
|
||||
last = "last"
|
||||
|
||||
|
||||
class SlideConfig(BaseModel):
|
||||
class SlideConfig(BaseModel): # type: ignore
|
||||
type: SlideType
|
||||
start_animation: int
|
||||
end_animation: int
|
||||
@ -79,24 +95,26 @@ class SlideConfig(BaseModel):
|
||||
terminated: bool = False
|
||||
|
||||
@validator("start_animation", "end_animation")
|
||||
def index_is_posint(cls, v: int):
|
||||
def index_is_posint(cls, v: int) -> int:
|
||||
if v < 0:
|
||||
raise ValueError("Animation index (start or end) cannot be negative")
|
||||
return v
|
||||
|
||||
@validator("number")
|
||||
def number_is_strictly_posint(cls, v: int):
|
||||
def number_is_strictly_posint(cls, v: int) -> int:
|
||||
if v <= 0:
|
||||
raise ValueError("Slide number cannot be negative or zero")
|
||||
return v
|
||||
|
||||
@root_validator
|
||||
def start_animation_is_before_end(cls, values):
|
||||
if values["start_animation"] >= values["end_animation"]:
|
||||
def start_animation_is_before_end(
|
||||
cls, values: Dict[str, Union[SlideType, int, bool]]
|
||||
) -> Dict[str, Union[SlideType, int, bool]]:
|
||||
if values["start_animation"] >= values["end_animation"]: # type: ignore
|
||||
|
||||
if values["start_animation"] == values["end_animation"] == 0:
|
||||
raise ValueError(
|
||||
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting."
|
||||
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
@ -105,22 +123,26 @@ class SlideConfig(BaseModel):
|
||||
|
||||
return values
|
||||
|
||||
def is_slide(self):
|
||||
def is_slide(self) -> bool:
|
||||
return self.type == SlideType.slide
|
||||
|
||||
def is_loop(self):
|
||||
def is_loop(self) -> bool:
|
||||
return self.type == SlideType.loop
|
||||
|
||||
def is_last(self):
|
||||
def is_last(self) -> bool:
|
||||
return self.type == SlideType.last
|
||||
|
||||
@property
|
||||
def slides_slice(self) -> slice:
|
||||
return slice(self.start_animation, self.end_animation)
|
||||
|
||||
class PresentationConfig(BaseModel):
|
||||
|
||||
class PresentationConfig(BaseModel): # type: ignore
|
||||
slides: List[SlideConfig]
|
||||
files: List[str]
|
||||
|
||||
@validator("files", pre=True, each_item=True)
|
||||
def is_file_and_exists(cls, v):
|
||||
def is_file_and_exists(cls, v: str) -> str:
|
||||
if not os.path.exists(v):
|
||||
raise ValueError(
|
||||
f"Animation file {v} does not exist. Are you in the right directory?"
|
||||
@ -132,7 +154,9 @@ class PresentationConfig(BaseModel):
|
||||
return v
|
||||
|
||||
@root_validator
|
||||
def animation_indices_match_files(cls, values):
|
||||
def animation_indices_match_files(
|
||||
cls, values: Dict[str, Union[List[SlideConfig], List[str]]]
|
||||
) -> Dict[str, Union[List[SlideConfig], List[str]]]:
|
||||
files = values.get("files")
|
||||
slides = values.get("slides")
|
||||
|
||||
@ -142,12 +166,88 @@ class PresentationConfig(BaseModel):
|
||||
n_files = len(files)
|
||||
|
||||
for slide in slides:
|
||||
if slide.end_animation > n_files:
|
||||
if slide.end_animation > n_files: # type: ignore
|
||||
raise ValueError(
|
||||
f"The following slide's contains animations not listed in files {files}: {slide}"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
def move_to(self, dest: str, copy: bool = True) -> "PresentationConfig":
|
||||
"""
|
||||
Moves (or copy) the files to a given directory.
|
||||
"""
|
||||
copy_func: Callable[[str, str], None] = shutil.copy
|
||||
move_func: Callable[[str, str], None] = shutil.move
|
||||
move = copy_func if copy else move_func
|
||||
|
||||
n = len(self.files)
|
||||
for i in range(n):
|
||||
file = self.files[i]
|
||||
basename = os.path.basename(file)
|
||||
dest_path = os.path.join(dest, basename)
|
||||
logger.debug(f"Moving / copying {file} to {dest_path}")
|
||||
move(file, dest_path)
|
||||
self.files[i] = dest_path
|
||||
|
||||
return self
|
||||
|
||||
def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
|
||||
"""
|
||||
Concatenate animations such that each slide contains one animation.
|
||||
"""
|
||||
|
||||
dest_paths = []
|
||||
|
||||
for i, slide_config in enumerate(self.slides):
|
||||
files = self.files[slide_config.slides_slice]
|
||||
|
||||
if len(files) > 1:
|
||||
dest_path = merge_basenames(files)
|
||||
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
||||
f.writelines(f"file {os.path.abspath(path)}\n" for path in files)
|
||||
f.close()
|
||||
|
||||
command = [
|
||||
FFMPEG_BIN,
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
f.name,
|
||||
"-c",
|
||||
"copy",
|
||||
dest_path,
|
||||
"-y",
|
||||
]
|
||||
logger.debug(" ".join(command))
|
||||
process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
output, error = process.communicate()
|
||||
|
||||
if output:
|
||||
logger.debug(output.decode())
|
||||
|
||||
if error:
|
||||
logger.debug(error.decode())
|
||||
|
||||
dest_paths.append(dest_path)
|
||||
|
||||
else:
|
||||
dest_paths.append(files[0])
|
||||
|
||||
slide_config.start_animation = i
|
||||
slide_config.end_animation = i + 1
|
||||
|
||||
self.files = dest_paths
|
||||
|
||||
if dest:
|
||||
return self.move_to(dest)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
DEFAULT_CONFIG = Config()
|
||||
|
216
manim_slides/convert.py
Normal file
216
manim_slides/convert.py
Normal file
@ -0,0 +1,216 @@
|
||||
import os
|
||||
import webbrowser
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Generator, List, Type
|
||||
|
||||
import click
|
||||
import pkg_resources
|
||||
from click import Context, Parameter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .commons import folder_path_option, verbosity_option
|
||||
from .config import PresentationConfig
|
||||
from .present import get_scenes_presentation_config
|
||||
|
||||
|
||||
def validate_config_option(
|
||||
ctx: Context, param: Parameter, value: Any
|
||||
) -> Dict[str, str]:
|
||||
|
||||
config = {}
|
||||
|
||||
for c_option in value:
|
||||
try:
|
||||
key, value = c_option.split("=")
|
||||
config[key] = value
|
||||
except ValueError:
|
||||
raise click.BadParameter(
|
||||
f"Configuration options `{c_option}` could not be parsed into a proper (key, value) pair. Please use an `=` sign to separate key from value."
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class Converter(BaseModel): # type: ignore
|
||||
presentation_configs: List[PresentationConfig] = []
|
||||
assets_dir: str = "{basename}_assets"
|
||||
|
||||
def convert_to(self, dest: str) -> None:
|
||||
"""Converts self, i.e., a list of presentations, into a given format."""
|
||||
raise NotImplementedError
|
||||
|
||||
def open(self, file: str) -> bool:
|
||||
"""Opens a file, generated with converter, using appropriate application."""
|
||||
return webbrowser.open(file)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s: str) -> Type["Converter"]:
|
||||
"""Returns the appropriate converter from a string name."""
|
||||
return {
|
||||
"html": RevealJS,
|
||||
}[s]
|
||||
|
||||
|
||||
class JSBool(str, Enum):
|
||||
true = "true"
|
||||
false = "false"
|
||||
|
||||
|
||||
class RevealTheme(str, Enum):
|
||||
black = "black"
|
||||
white = "white"
|
||||
league = "league"
|
||||
beige = "beige"
|
||||
sky = "sky"
|
||||
night = "night"
|
||||
serif = "serif"
|
||||
simple = "simple"
|
||||
soralized = "solarized"
|
||||
blood = "blood"
|
||||
moon = "moon"
|
||||
|
||||
|
||||
class RevealJS(Converter):
|
||||
background_color: str = "black"
|
||||
controls: JSBool = JSBool.false
|
||||
embedded: JSBool = JSBool.false
|
||||
fragments: JSBool = JSBool.false
|
||||
height: str = "100%"
|
||||
loop: JSBool = JSBool.false
|
||||
progress: JSBool = JSBool.false
|
||||
reveal_version: str = "3.7.0"
|
||||
reveal_theme: RevealTheme = RevealTheme.black
|
||||
shuffle: JSBool = JSBool.false
|
||||
title: str = "Manim Slides"
|
||||
width: str = "100%"
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
def get_sections_iter(self) -> Generator[str, None, None]:
|
||||
"""Generates a sequence of sections, one per slide, that will be included into the html template."""
|
||||
for presentation_config in self.presentation_configs:
|
||||
for slide_config in presentation_config.slides:
|
||||
file = presentation_config.files[slide_config.start_animation]
|
||||
file = os.path.join(self.assets_dir, os.path.basename(file))
|
||||
|
||||
if slide_config.is_loop():
|
||||
yield f'<section data-background-video="{file}" data-background-video-loop></section>'
|
||||
else:
|
||||
yield f'<section data-background-video="{file}"></section>'
|
||||
|
||||
def load_template(self) -> str:
|
||||
"""Returns the RevealJS HTML template as a string."""
|
||||
return pkg_resources.resource_string(
|
||||
__name__, "data/revealjs_template.html"
|
||||
).decode()
|
||||
|
||||
def convert_to(self, dest: str) -> None:
|
||||
"""Converts this configuration into a RevealJS HTML presentation, saved to DEST."""
|
||||
dirname = os.path.dirname(dest)
|
||||
basename, ext = os.path.splitext(os.path.basename(dest))
|
||||
|
||||
self.assets_dir = self.assets_dir.format(
|
||||
dirname=dirname, basename=basename, ext=ext
|
||||
)
|
||||
full_assets_dir = os.path.join(dirname, self.assets_dir)
|
||||
|
||||
os.makedirs(full_assets_dir, exist_ok=True)
|
||||
|
||||
for presentation_config in self.presentation_configs:
|
||||
presentation_config.concat_animations().move_to(full_assets_dir)
|
||||
|
||||
with open(dest, "w") as f:
|
||||
|
||||
sections = "".join(self.get_sections_iter())
|
||||
|
||||
revealjs_template = self.load_template()
|
||||
content = revealjs_template.format(sections=sections, **self.dict())
|
||||
|
||||
f.write(content)
|
||||
|
||||
|
||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wraps a function to add a `--show-config` option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
to = ctx.params.get("to")
|
||||
|
||||
if to:
|
||||
converter = Converter.from_string(to)(scenes=[])
|
||||
for key, value in converter.dict().items():
|
||||
click.echo(f"{key}: {repr(value)}")
|
||||
|
||||
ctx.exit()
|
||||
|
||||
else:
|
||||
raise click.UsageError(
|
||||
"Using --show-config option requires to first specify --to option."
|
||||
)
|
||||
|
||||
return click.option(
|
||||
"--show-config",
|
||||
is_flag=True,
|
||||
help="Show supported options for given format and exit.",
|
||||
default=None,
|
||||
expose_value=False,
|
||||
show_envvar=True,
|
||||
callback=callback,
|
||||
)(function)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("scenes", nargs=-1)
|
||||
@folder_path_option
|
||||
@click.argument("dest")
|
||||
@click.option(
|
||||
"--to",
|
||||
type=click.Choice(["html"], case_sensitive=False),
|
||||
default="html",
|
||||
show_default=True,
|
||||
help="Set the conversion format to use.",
|
||||
)
|
||||
@click.option(
|
||||
"--open",
|
||||
"open_result",
|
||||
is_flag=True,
|
||||
help="Open the newly created file using the approriate application.",
|
||||
)
|
||||
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_options",
|
||||
multiple=True,
|
||||
callback=validate_config_option,
|
||||
help="Configuration options passed to the converter. E.g., pass `-cbackground_color=red` to set the background color to red (if supported).",
|
||||
)
|
||||
@show_config_options
|
||||
@verbosity_option
|
||||
def convert(
|
||||
scenes: List[str],
|
||||
folder: str,
|
||||
dest: str,
|
||||
to: str,
|
||||
open_result: bool,
|
||||
force: bool,
|
||||
config_options: Dict[str, str],
|
||||
) -> None:
|
||||
"""
|
||||
Convert SCENE(s) into a given format and writes the result in DEST.
|
||||
"""
|
||||
|
||||
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||
|
||||
converter = Converter.from_string(to)(
|
||||
presentation_configs=presentation_configs, **config_options
|
||||
)
|
||||
|
||||
converter.convert_to(dest)
|
||||
|
||||
if open_result:
|
||||
converter.open(dest)
|
185
manim_slides/data/revealjs_template.html
Normal file
185
manim_slides/data/revealjs_template.html
Normal file
@ -0,0 +1,185 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/reveal.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/theme/{reveal_theme}.css">
|
||||
|
||||
<!-- Theme used for syntax highlighting of code -->
|
||||
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
|
||||
|
||||
<!-- Printing and PDF exports -->
|
||||
<script>
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = window.location.search.match(/print-pdf/gi) ? 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/print/pdf.css' : 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/print/paper.css';
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
</script>
|
||||
<!-- <link rel="stylesheet" href="index.css"> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="reveal">
|
||||
<div class="slides">
|
||||
{sections}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<script src="lib/js/head.min.js"></script>-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/headjs@1.0.3/dist/1.0.0/head.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/js/reveal.min.js"></script>
|
||||
|
||||
<!-- <script src="index.js"></script> -->
|
||||
<script>
|
||||
// More info about config & dependencies:
|
||||
// - https://github.com/hakimel/reveal.js#configuration
|
||||
// - https://github.com/hakimel/reveal.js#dependencies
|
||||
Reveal.initialize({{
|
||||
// Display controls in the bottom right corner
|
||||
controls: {controls},
|
||||
|
||||
width: '{width}',
|
||||
height: '{height}',
|
||||
|
||||
// Display a presentation progress bar
|
||||
progress: {progress},
|
||||
|
||||
// Set default timing of 2 minutes per slide
|
||||
defaultTiming: 120,
|
||||
|
||||
// Display the page number of the current slide
|
||||
slideNumber: true,
|
||||
|
||||
// Push each slide change to the browser history
|
||||
history: false,
|
||||
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: true,
|
||||
|
||||
// Enable the slide overview mode
|
||||
overview: true,
|
||||
|
||||
// Vertical centering of slides
|
||||
center: true,
|
||||
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: true,
|
||||
|
||||
// Loop the presentation
|
||||
loop: {loop},
|
||||
|
||||
// Change the presentation direction to be RTL
|
||||
rtl: false,
|
||||
|
||||
// Randomizes the order of slides each time the presentation loads
|
||||
shuffle: {shuffle},
|
||||
|
||||
// Turns fragments on and off globally
|
||||
fragments: {fragments},
|
||||
|
||||
// Flags if the presentation is running in an embedded mode,
|
||||
// i.e. contained within a limited portion of the screen
|
||||
embedded: {embedded},
|
||||
|
||||
// Flags if we should show a help overlay when the questionmark
|
||||
// key is pressed
|
||||
help: true,
|
||||
|
||||
// Flags if speaker notes should be visible to all viewers
|
||||
showNotes: false,
|
||||
|
||||
// Global override for autolaying embedded media (video/audio/iframe)
|
||||
// - null: Media will only autoplay if data-autoplay is present
|
||||
// - true: All media will autoplay, regardless of individual setting
|
||||
// - false: No media will autoplay, regardless of individual setting
|
||||
autoPlayMedia: null,
|
||||
|
||||
// Number of milliseconds between automatically proceeding to the
|
||||
// next slide, disabled when set to 0, this value can be overwritten
|
||||
// by using a data-autoslide attribute on your slides
|
||||
autoSlide: 0,
|
||||
|
||||
// Stop auto-sliding after user input
|
||||
autoSlideStoppable: true,
|
||||
|
||||
// Use this method for navigation when auto-sliding
|
||||
autoSlideMethod: Reveal.navigateNext,
|
||||
|
||||
// Enable slide navigation via mouse wheel
|
||||
mouseWheel: false,
|
||||
|
||||
// Hides the address bar on mobile devices
|
||||
hideAddressBar: true,
|
||||
|
||||
// Opens links in an iframe preview overlay
|
||||
previewLinks: true,
|
||||
|
||||
// Transition style
|
||||
transition: 'none', // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// Transition speed
|
||||
transitionSpeed: 'default', // default/fast/slow
|
||||
|
||||
// Transition style for full page slide backgrounds
|
||||
backgroundTransition: 'none', // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// Number of slides away from the current that are visible
|
||||
viewDistance: 3,
|
||||
|
||||
// Parallax background image
|
||||
parallaxBackgroundImage: '', // e.g. "'https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg'"
|
||||
|
||||
// Parallax background size
|
||||
parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px"
|
||||
|
||||
// Number of pixels to move the parallax background per slide
|
||||
// - Calculated automatically unless specified
|
||||
// - Set to 0 to disable movement along an axis
|
||||
parallaxBackgroundHorizontal: null,
|
||||
parallaxBackgroundVertical: null,
|
||||
|
||||
|
||||
// The display mode that will be used to show slides
|
||||
display: 'block',
|
||||
|
||||
/*
|
||||
multiplex: {{
|
||||
// Example values. To generate your own, see the socket.io server instructions.
|
||||
secret: '13652805320794272084', // Obtained from the socket.io server. Gives this (the master) control of the presentation
|
||||
id: '1ea875674b17ca76', // Obtained from socket.io server
|
||||
url: 'https://reveal-js-multiplex-ccjbegmaii.now.sh' // Location of socket.io server
|
||||
}},
|
||||
*/
|
||||
|
||||
dependencies: [
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/markdown/marked.js' }},
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/markdown/markdown.js' }},
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/notes/notes.js', async: true }},
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/highlight/highlight.js', async: true, callback: function () {{ hljs.initHighlightingOnLoad(); }} }},
|
||||
//{{ src: '//cdn.socket.io/socket.io-1.3.5.js', async: true }},
|
||||
//{{ src: 'plugin/multiplex/master.js', async: true }},
|
||||
// and if you want speaker notes
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/notes-server/client.js', async: true }}
|
||||
|
||||
],
|
||||
markdown: {{
|
||||
// renderer: myrenderer,
|
||||
smartypants: true
|
||||
}}
|
||||
}});
|
||||
Reveal.configure({{
|
||||
// PDF Configurations
|
||||
pdfMaxPagesPerSlide: 1
|
||||
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,27 +0,0 @@
|
||||
import click
|
||||
from click_default_group import DefaultGroup
|
||||
|
||||
from . import __version__
|
||||
from .present import list_scenes, present
|
||||
from .wizard import init, wizard
|
||||
|
||||
|
||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||
@click.version_option(__version__, "-v", "--version")
|
||||
@click.help_option("-h", "--help")
|
||||
def cli() -> None:
|
||||
"""
|
||||
Manim Slides command-line utilities.
|
||||
|
||||
If no command is specified, defaults to `present`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
cli.add_command(list_scenes)
|
||||
cli.add_command(present)
|
||||
cli.add_command(wizard)
|
||||
cli.add_command(init)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -3,22 +3,22 @@ import platform
|
||||
import sys
|
||||
import time
|
||||
from enum import IntEnum, auto, unique
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import click
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pydantic import ValidationError
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
|
||||
from PySide6.QtCore import Qt, QThread, Signal, Slot
|
||||
from PySide6.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent
|
||||
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
|
||||
from tqdm import tqdm
|
||||
|
||||
from .commons import config_path_option, verbosity_option
|
||||
from .config import DEFAULT_CONFIG, Config, PresentationConfig, SlideConfig, SlideType
|
||||
from .config import DEFAULT_CONFIG, Config, PresentationConfig, SlideConfig
|
||||
from .defaults import FOLDER_PATH
|
||||
from .manim import logger
|
||||
from .resources import * # noqa: F401, F403
|
||||
|
||||
os.environ.pop(
|
||||
"QT_QPA_PLATFORM_PLUGIN_PATH", None
|
||||
@ -66,7 +66,7 @@ class Presentation:
|
||||
|
||||
self.current_slide_index: int = 0
|
||||
self.current_animation: int = self.current_slide.start_animation
|
||||
self.current_file: Optional[str] = None
|
||||
self.current_file: str = ""
|
||||
|
||||
self.loaded_animation_cap: int = -1
|
||||
self.cap = None # cap = cv2.VideoCapture
|
||||
@ -77,7 +77,6 @@ class Presentation:
|
||||
self.lastframe: Optional[np.ndarray] = None
|
||||
|
||||
self.reset()
|
||||
self.add_last_slide()
|
||||
|
||||
@property
|
||||
def current_slide(self) -> SlideConfig:
|
||||
@ -184,17 +183,6 @@ class Presentation:
|
||||
)
|
||||
return max(fps, 1) # TODO: understand why we sometimes get 0 fps
|
||||
|
||||
def add_last_slide(self) -> None:
|
||||
"""Add a 'last' slide to the end of slides."""
|
||||
self.slides.append(
|
||||
SlideConfig(
|
||||
start_animation=self.last_slide.end_animation,
|
||||
end_animation=self.last_slide.end_animation + 1,
|
||||
type=SlideType.last,
|
||||
number=self.last_slide.number + 1,
|
||||
)
|
||||
)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Rests current presentation."""
|
||||
self.current_animation = 0
|
||||
@ -233,7 +221,7 @@ class Presentation:
|
||||
"""Returns current frame number."""
|
||||
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||
|
||||
def update_state(self, state) -> Tuple[np.ndarray, State]:
|
||||
def update_state(self, state: State) -> Tuple[np.ndarray, State]:
|
||||
"""
|
||||
Updates the current state given the previous one.
|
||||
|
||||
@ -247,7 +235,7 @@ class Presentation:
|
||||
still_playing, frame = self.current_cap.read()
|
||||
if still_playing:
|
||||
self.lastframe = frame
|
||||
elif state in [state.WAIT, state.PAUSED]:
|
||||
elif state == state.WAIT or state == state.PAUSED: # type: ignore
|
||||
return self.lastframe, state
|
||||
elif self.current_slide.is_last() and self.current_slide.terminated:
|
||||
return self.lastframe, State.END
|
||||
@ -279,21 +267,21 @@ class Presentation:
|
||||
return self.lastframe, state
|
||||
|
||||
|
||||
class Display(QThread):
|
||||
class Display(QThread): # type: ignore
|
||||
"""Displays one or more presentations one after each other."""
|
||||
|
||||
change_video_signal = pyqtSignal(np.ndarray)
|
||||
change_info_signal = pyqtSignal(dict)
|
||||
finished = pyqtSignal()
|
||||
change_video_signal = Signal(np.ndarray)
|
||||
change_info_signal = Signal(dict)
|
||||
finished = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
presentations,
|
||||
presentations: List[PresentationConfig],
|
||||
config: Config = DEFAULT_CONFIG,
|
||||
start_paused=False,
|
||||
skip_all=False,
|
||||
record_to=None,
|
||||
exit_after_last_slide=False,
|
||||
start_paused: bool = False,
|
||||
skip_all: bool = False,
|
||||
record_to: Optional[str] = None,
|
||||
exit_after_last_slide: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.presentations = presentations
|
||||
@ -409,7 +397,7 @@ class Display(QThread):
|
||||
}
|
||||
)
|
||||
|
||||
@pyqtSlot(int)
|
||||
@Slot(int)
|
||||
def set_key(self, key: int) -> None:
|
||||
"""Sets the next key to be handled."""
|
||||
self.key = key
|
||||
@ -455,14 +443,14 @@ class Display(QThread):
|
||||
|
||||
self.key = -1 # No more key to be handled
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
"""Stops current thread, without doing anything after."""
|
||||
self.run_flag = False
|
||||
self.wait()
|
||||
|
||||
|
||||
class Info(QWidget):
|
||||
def __init__(self):
|
||||
class Info(QWidget): # type: ignore
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle(WINDOW_INFO_NAME)
|
||||
|
||||
@ -484,8 +472,8 @@ class Info(QWidget):
|
||||
|
||||
self.update_info({})
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def update_info(self, info: dict):
|
||||
@Slot(dict)
|
||||
def update_info(self, info: Dict[str, Union[str, int]]) -> None:
|
||||
self.animationLabel.setText("Animation: {}".format(info.get("animation", "na")))
|
||||
self.stateLabel.setText("State: {}".format(info.get("state", "unknown")))
|
||||
self.slideLabel.setText(
|
||||
@ -501,38 +489,41 @@ class Info(QWidget):
|
||||
)
|
||||
|
||||
|
||||
class InfoThread(QThread):
|
||||
def __init__(self):
|
||||
class InfoThread(QThread): # type: ignore
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.dialog = Info()
|
||||
self.run_flag = True
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
super().start()
|
||||
|
||||
self.dialog.show()
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
self.dialog.deleteLater()
|
||||
|
||||
|
||||
class App(QWidget):
|
||||
send_key_signal = pyqtSignal(int)
|
||||
class App(QWidget): # type: ignore
|
||||
send_key_signal = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
*args: Any,
|
||||
config: Config = DEFAULT_CONFIG,
|
||||
fullscreen: bool = False,
|
||||
resolution: Tuple[int, int] = (1980, 1080),
|
||||
hide_mouse: bool = False,
|
||||
aspect_ratio: Qt.AspectRatioMode = Qt.IgnoreAspectRatio,
|
||||
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
|
||||
**kwargs,
|
||||
background_color: str = "black",
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle(WINDOW_NAME)
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
self.display_width, self.display_height = resolution
|
||||
self.aspect_ratio = aspect_ratio
|
||||
self.resize_mode = resize_mode
|
||||
@ -544,13 +535,15 @@ class App(QWidget):
|
||||
self.label = QLabel(self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.label.resize(self.display_width, self.display_height)
|
||||
self.label.setStyleSheet(f"background-color: {background_color}")
|
||||
|
||||
self.pixmap = QPixmap(self.width(), self.height())
|
||||
self.label.setPixmap(self.pixmap)
|
||||
self.label.setMinimumSize(1, 1)
|
||||
|
||||
# create the video capture thread
|
||||
self.thread = Display(*args, config=config, **kwargs)
|
||||
kwargs["config"] = config
|
||||
self.thread = Display(*args, **kwargs)
|
||||
# create the info dialog
|
||||
self.info = Info()
|
||||
self.info.show()
|
||||
@ -570,7 +563,7 @@ class App(QWidget):
|
||||
# start the thread
|
||||
self.thread.start()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||
|
||||
key = event.key()
|
||||
if self.config.HIDE_MOUSE.match(key):
|
||||
@ -584,54 +577,42 @@ class App(QWidget):
|
||||
self.send_key_signal.emit(key)
|
||||
event.accept()
|
||||
|
||||
def closeAll(self):
|
||||
def closeAll(self) -> None:
|
||||
logger.debug("Closing all QT windows")
|
||||
self.thread.stop()
|
||||
self.info.deleteLater()
|
||||
self.deleteLater()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
self.pixmap = self.pixmap.scaled(
|
||||
self.width(), self.height(), self.aspect_ratio, self.resize_mode
|
||||
)
|
||||
self.label.setPixmap(self.pixmap)
|
||||
self.label.resize(self.width(), self.height())
|
||||
|
||||
def closeEvent(self, event):
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
self.closeAll()
|
||||
event.accept()
|
||||
|
||||
@pyqtSlot(np.ndarray)
|
||||
def update_image(self, cv_img: dict):
|
||||
"""Updates the image_label with a new opencv image"""
|
||||
self.pixmap = self.convert_cv_qt(cv_img)
|
||||
self.label.setPixmap(self.pixmap)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def update_info(self, info: dict):
|
||||
"""Updates the image_label with a new opencv image"""
|
||||
pass
|
||||
|
||||
def convert_cv_qt(self, cv_img):
|
||||
"""Convert from an opencv image to QPixmap"""
|
||||
rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
|
||||
h, w, ch = rgb_image.shape
|
||||
@Slot(np.ndarray)
|
||||
def update_image(self, cv_img: np.ndarray) -> None:
|
||||
"""Updates the (image) label with a new opencv image"""
|
||||
h, w, ch = cv_img.shape
|
||||
bytes_per_line = ch * w
|
||||
convert_to_Qt_format = QtGui.QImage(
|
||||
rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888
|
||||
)
|
||||
p = convert_to_Qt_format.scaled(
|
||||
self.width(),
|
||||
self.height(),
|
||||
self.aspect_ratio,
|
||||
self.resize_mode,
|
||||
)
|
||||
return QPixmap.fromImage(p)
|
||||
qt_img = QImage(cv_img.data, w, h, bytes_per_line, QImage.Format_BGR888)
|
||||
|
||||
if w != self.width() or h != self.height():
|
||||
qt_img = qt_img.scaled(
|
||||
self.width(), self.height(), self.aspect_ratio, self.resize_mode
|
||||
)
|
||||
|
||||
self.label.setPixmap(QPixmap.fromImage(qt_img))
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
@ -639,14 +620,14 @@ class App(QWidget):
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def list_scenes(folder) -> None:
|
||||
def list_scenes(folder: str) -> None:
|
||||
"""List available scenes."""
|
||||
|
||||
for i, scene in enumerate(_list_scenes(folder), start=1):
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
|
||||
def _list_scenes(folder) -> List[str]:
|
||||
def _list_scenes(folder: str) -> List[str]:
|
||||
"""Lists available scenes in given directory."""
|
||||
scenes = []
|
||||
|
||||
@ -667,11 +648,69 @@ def _list_scenes(folder) -> List[str]:
|
||||
return scenes
|
||||
|
||||
|
||||
def prompt_for_scenes(folder: str) -> List[str]:
|
||||
"""Prompts the user to select scenes within a given folder."""
|
||||
|
||||
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
|
||||
|
||||
for i, scene in scene_choices.items():
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
click.echo()
|
||||
|
||||
click.echo("Choose number corresponding to desired scene/arguments.")
|
||||
click.echo("(Use comma separated list for multiple entries)")
|
||||
|
||||
def value_proc(value: Optional[str]) -> List[str]:
|
||||
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
|
||||
|
||||
if not all(0 < i <= len(scene_choices) for i in indices):
|
||||
raise click.UsageError("Please only enter numbers displayed on the screen.")
|
||||
|
||||
return [scene_choices[i] for i in indices]
|
||||
|
||||
if len(scene_choices) == 0:
|
||||
raise click.UsageError(
|
||||
"No scenes were found, are you in the correct directory?"
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||
return scenes
|
||||
except ValueError as e:
|
||||
raise click.UsageError(str(e))
|
||||
|
||||
|
||||
def get_scenes_presentation_config(
|
||||
scenes: List[str], folder: str
|
||||
) -> List[PresentationConfig]:
|
||||
"""Returns a list of presentation configurations based on the user input."""
|
||||
|
||||
if len(scenes) == 0:
|
||||
scenes = prompt_for_scenes(folder)
|
||||
|
||||
presentation_configs = []
|
||||
for scene in scenes:
|
||||
config_file = os.path.join(folder, f"{scene}.json")
|
||||
if not os.path.exists(config_file):
|
||||
raise click.UsageError(
|
||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||
)
|
||||
try:
|
||||
presentation_configs.append(PresentationConfig.parse_file(config_file))
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
|
||||
return presentation_configs
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("scenes", nargs=-1)
|
||||
@config_path_option
|
||||
@click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
@ -688,6 +727,7 @@ def _list_scenes(folder) -> List[str]:
|
||||
@click.option(
|
||||
"-r",
|
||||
"--resolution",
|
||||
metavar="<WIDTH HEIGHT>",
|
||||
type=(int, int),
|
||||
default=(1920, 1080),
|
||||
help="Window resolution WIDTH HEIGHT used if fullscreen is not set. You may manually resize the window afterward.",
|
||||
@ -697,6 +737,7 @@ def _list_scenes(folder) -> List[str]:
|
||||
"--to",
|
||||
"--record-to",
|
||||
"record_to",
|
||||
metavar="FILE",
|
||||
type=click.Path(dir_okay=False),
|
||||
default=None,
|
||||
help="If set, the presentation will be recorded into a AVI video file with given name.",
|
||||
@ -725,21 +766,32 @@ def _list_scenes(folder) -> List[str]:
|
||||
help="Set the resize (i.e., transformation) mode to be used when rescaling video.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--background-color",
|
||||
"--bgcolor",
|
||||
"background_color",
|
||||
metavar="COLOR",
|
||||
type=str,
|
||||
default="black",
|
||||
help='Set the background color for borders when using "keep" resize mode. Can be any valid CSS color, e.g., "green", "#FF6500" or "rgba(255, 255, 0, .5)".',
|
||||
show_default=True,
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def present(
|
||||
scenes,
|
||||
config_path,
|
||||
folder,
|
||||
start_paused,
|
||||
fullscreen,
|
||||
skip_all,
|
||||
resolution,
|
||||
record_to,
|
||||
exit_after_last_slide,
|
||||
hide_mouse,
|
||||
aspect_ratio,
|
||||
resize_mode,
|
||||
scenes: List[str],
|
||||
config_path: str,
|
||||
folder: str,
|
||||
start_paused: bool,
|
||||
fullscreen: bool,
|
||||
skip_all: bool,
|
||||
resolution: Tuple[int, int],
|
||||
record_to: Optional[str],
|
||||
exit_after_last_slide: bool,
|
||||
hide_mouse: bool,
|
||||
aspect_ratio: str,
|
||||
resize_mode: str,
|
||||
background_color: str,
|
||||
) -> None:
|
||||
"""
|
||||
Present SCENE(s), one at a time, in order.
|
||||
@ -754,53 +806,10 @@ def present(
|
||||
if skip_all:
|
||||
exit_after_last_slide = True
|
||||
|
||||
if len(scenes) == 0:
|
||||
scene_choices = _list_scenes(folder)
|
||||
|
||||
scene_choices = dict(enumerate(scene_choices, start=1))
|
||||
|
||||
for i, scene in scene_choices.items():
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
click.echo()
|
||||
|
||||
click.echo("Choose number corresponding to desired scene/arguments.")
|
||||
click.echo("(Use comma separated list for multiple entries)")
|
||||
|
||||
def value_proc(value: str) -> List[str]:
|
||||
indices = list(map(int, value.strip().replace(" ", "").split(",")))
|
||||
|
||||
if not all(0 < i <= len(scene_choices) for i in indices):
|
||||
raise click.UsageError(
|
||||
"Please only enter numbers displayed on the screen."
|
||||
)
|
||||
|
||||
return [scene_choices[i] for i in indices]
|
||||
|
||||
if len(scene_choices) == 0:
|
||||
raise click.UsageError(
|
||||
"No scenes were found, are you in the correct directory?"
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||
break
|
||||
except ValueError as e:
|
||||
raise click.UsageError(e)
|
||||
|
||||
presentations = []
|
||||
for scene in scenes:
|
||||
config_file = os.path.join(folder, f"{scene}.json")
|
||||
if not os.path.exists(config_file):
|
||||
raise click.UsageError(
|
||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||
)
|
||||
try:
|
||||
pres_config = PresentationConfig.parse_file(config_file)
|
||||
presentations.append(Presentation(pres_config))
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
presentations = [
|
||||
Presentation(presentation_config)
|
||||
for presentation_config in get_scenes_presentation_config(scenes, folder)
|
||||
]
|
||||
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
@ -832,6 +841,7 @@ def present(
|
||||
hide_mouse=hide_mouse,
|
||||
aspect_ratio=ASPECT_RATIO_MODES[aspect_ratio],
|
||||
resize_mode=RESIZE_MODES[resize_mode],
|
||||
background_color=background_color,
|
||||
)
|
||||
a.show()
|
||||
sys.exit(app.exec_())
|
||||
|
171
manim_slides/resources.py
Normal file
171
manim_slides/resources.py
Normal file
@ -0,0 +1,171 @@
|
||||
# type: ignore
|
||||
# Resource object code (Python 3)
|
||||
# Created by: object code
|
||||
# Created by: The Resource Compiler for Qt version 6.4.0
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
qt_resource_data = b"\
|
||||
\x00\x00\x08\x1c\
|
||||
\x89\
|
||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||
\x00\x01\x00\x00\x00\x01\x00\x08\x06\x00\x00\x00\x5cr\xa8f\
|
||||
\x00\x00\x01\x84iCCPICC prof\
|
||||
ile\x00\x00(\x91}\x91=H\xc3@\x1c\xc5_\
|
||||
S\xa5R+\x0e\xed \xe2\x90\xa1:Y\x10-\xe2\xa8\
|
||||
U(B\x85P+\xb4\xea`r\xfd\x84&\x0dI\x8a\
|
||||
\x8b\xa3\xe0Zp\xf0c\xb1\xea\xe0\xe2\xac\xab\x83\xab \
|
||||
\x08~\x808:9)\xbaH\x89\xffK\x0a-b<\
|
||||
8\xee\xc7\xbb{\x8f\xbbw\x80\xd0\xac2\xd5\xec\x99\x00\
|
||||
T\xcd2\xd2\xc9\x84\x98\xcd\xad\x8a\x81W\x04\x11F?\
|
||||
\xe2\x88\xcb\xcc\xd4\xe7$)\x05\xcf\xf1u\x0f\x1f_\xef\
|
||||
b<\xcb\xfb\xdc\x9fc _0\x19\xe0\x13\x89g\x99\
|
||||
nX\xc4\x1b\xc4\xd3\x9b\x96\xcey\x9f8\xc2\xcar\x9e\
|
||||
\xf8\x9cx\xdc\xa0\x0b\x12?r]q\xf9\x8ds\xc9a\
|
||||
\x81gF\x8cLz\x9e8B,\x96\xbaX\xe9bV\
|
||||
6T\xe28q4\xafj\x94/d]\xces\xde\xe2\
|
||||
\xacV\xeb\xac}O\xfe\xc2PA[Y\xe6:\xcd\x11\
|
||||
$\xb1\x88%H\x10\xa1\xa0\x8e\x0a\xaa\xb0\x10\xa3U#\
|
||||
\xc5D\x9a\xf6\x13\x1e\xfea\xc7/\x91K!W\x05\x8c\
|
||||
\x1c\x0b\xa8A\x85\xec\xf8\xc1\xff\xe0w\xb7fqj\xd2\
|
||||
M\x0a%\x80\xde\x17\xdb\xfe\x18\x05\x02\xbb@\xaba\xdb\
|
||||
\xdf\xc7\xb6\xdd:\x01\xfc\xcf\xc0\x95\xd6\xf1\xd7\x9a\xc0\xcc\
|
||||
'\xe9\x8d\x8e\x16=\x02\x06\xb7\x81\x8b\xeb\x8e\xa6\xec\x01\
|
||||
\x97;\xc0\xd0\x93.\x1b\xb2#\xf9i\x0a\xc5\x22\xf0~\
|
||||
F\xdf\x94\x03\xc2\xb7@p\xcd\xed\xad\xbd\x8f\xd3\x07 \
|
||||
C]\xa5n\x80\x83C`\xacD\xd9\xeb\x1e\xef\xee\xeb\
|
||||
\xee\xed\xdf3\xed\xfe~\x00\xd6\xd3r\xcf+\xa2\xc1_\
|
||||
\x00\x00\x00\x06bKGD\x004\x004\x004\xaf4\
|
||||
\x1c\xc0\x00\x00\x00\x09pHYs\x00\x00.#\x00\x00\
|
||||
.#\x01x\xa5?v\x00\x00\x00\x07tIME\x07\
|
||||
\xe6\x0a\x13\x0c\x0f\x03\x13^\x06\xfe\x00\x00\x00\x19tE\
|
||||
XtComment\x00Create\
|
||||
d with GIMPW\x81\x0e\x17\x00\
|
||||
\x00\x05\xf4IDATx\xda\xed\xddA\x92\x9b:\x18\
|
||||
\x85QK\xe5%\xc1\xfe\x17\x00{rF\x19\xa4\xcb\xee\
|
||||
\x801X\xd2=g\x98z\x956 }\xfe\xd5\xddy\
|
||||
\xbe\xdd\x00\x00\x00\x00\x00\x801\x95\xab\xbe\xd04M\x0f\
|
||||
\xb7\x1b\xb6[\xd7\xb5t\x1d\x00\x9b\x1e\xda\x8eA\xb1\xf1\
|
||||
!7\x04\xc5\xc6\x87\xdc\x10\x14\x1b\x1fr#Pl~\
|
||||
\xc8\x0dA\xb1\xf9!7\x02\xc5\xe6\x87\xdc\x08\x14\x9b\x1f\
|
||||
r#Pl~\xc8\x8d@\xb1\xf9!7\x02\xc5\xe6\x87\
|
||||
\xdc\x08T\x9b\x1frU\xb7\x00\xc6\xb4\xe5\x8d\xbb|\xe2\
|
||||
/\x01\xfa<\x0aT\x9b\x1f\x1c\x01\x80\xc0\xa3@\xf5\xee\
|
||||
\x0f&\x00 p\x0a\xa8\xde\xfd\xc1\x04\x00\x04N\x01\x02\
|
||||
\x00&\x00@\x00\x9c\xff!\xca\xbd\x85\x17\xb1,\x8b'\
|
||||
A\x9cy\x9e\xb3\x03`\xe3\x93\xec\xef\xfa\xfff\x08\xaa\
|
||||
\xcd\x0f\xb9\x13p\xb5\xf9!7\x02~\x0a\x00\xc1.\x0f\
|
||||
\x80w\x7fhgo\x98\x00\xc0\x04\x00\x08\x00 \x00\x80\
|
||||
\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\x00 \
|
||||
\x00\x80\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\
|
||||
\x00 \x00\x80\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\
|
||||
\x00|\xd2\xbd\xb7\x17\xfc\xdbG)\x8f\xfc\xb1c-^\
|
||||
\xb7g\xd1\xffu\x97\x9f\x7f0M\xd3\xe3\xcc/x\xe4\
|
||||
\x06m\xfd\x1c\xf5\x91\x16_\x8b\xd7\xbc\xe7\xf3\xec\x13\x9f\
|
||||
\xc5\x91\xeb\xde\xf35\xde\xb1\xaek\xe9\xf2\x08\xb0\xe7\xc6\
|
||||
\x9c}\x13\x93\xafy\xef\xd7I|\x16=]w\x1d\xf1\
|
||||
\xe6\x8f\xb0\xf0Z\xbc\xe6w\xff\xfe\xc4g\xd1\x8b\xea\xe6\
|
||||
\xb7w\xbdG\xae\xb9\xd5\xfb\x95\xf6\x1c{\xb9\xe6\xea\x01\
|
||||
x\xbd\xae/\xf7\x99\xf81\xa0\x85v\xe9\xeb;:\xe1\
|
||||
\x006\xbfkF\x00l\x04\xd7\x8e\x00t\xbc\xf8m\
|
||||
\x00\x11\x10\x00\x8b\xde\xfd\x10C\x01\xb0\xf9qo\x04\xc0\
|
||||
\x02w\x8f\xdc\x04\x01\xb0\xb0\xdd+\x04\xc0\xf9V\x04\x10\
|
||||
\x00\x8bX<\x11\x00\x9b\xdf\xbdD\x00,X\xf7\x14\x01\
|
||||
\xb0P\xdd[\x04\xc0yU\x04\x10\x00\x8bRl\x11\x00\
|
||||
\x9b\xdf\xbdG\x00,@\xcf\x00\x01\xb0\xf0<\x0b\x04\xc0\
|
||||
\xf9S\x04\x10\x00\x8b\xcc\xf3A\x00,.\x13\x1a\x02`\
|
||||
\xf3{f\x08\x80\x85\xe4\xd9\x09\x00FI\x11\x10\x00,\
|
||||
\x1a1\x17\x00\x8b\x05\xcfV\x00,\x10<c\x01\xb00\
|
||||
\xf0\xac\x05\xc0\xf9\x10\x11\x10\x00\x8b\x00\xf1\x17\x00\x9b\x1f\
|
||||
kA\x00<p\xac\x09\x01\xf0\xa0\xb16\x04\xc0y\x0f\
|
||||
\x11\x10\x00\x0f\x15\xeb\xa5\x1bw\x0f\x13r\xd7\x8d_\x04\
|
||||
\x82`\x02\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\
|
||||
\x00 \x00\x80\x00\x00\x02\xd0\xbeeY<e\xac\x9d\xe4\
|
||||
\x09@\x04\xb0f\xc2\x8f\x00\xcb\xb2\x08\x016\x7f\xfa\xf7\
|
||||
\x00D\x00o\x12\xc1\x01\x10\x01\xac\x89\xf0\x00\x88\x00\xd6\
|
||||
Bx\x00D\x00k\xc0\xef\x01\xf8\xe6\xa0\xcd/\x00X\
|
||||
\x0c\x9e\xb7\x00X\x14\x16\x85\x89O\x00,\x10<[\x01\
|
||||
\xb0P\xf0L\x05\xc0\xa8\x88\xcd/\x00\x16\x0f\x9e\x9f\x00\
|
||||
XD\x98\xe0\x04@\x04\xf0\xac\x04\xc0\xc2\xc23\x12\x00\
|
||||
\xa3%6\xbf\x00Xlx\x1e\x02`\xd1a\x22\x13\x00\
|
||||
\x11\xc0\xbd\x17\x00\x0b\x11\xf7\x5c\x00\x8c\xa2\xd8\xfc\x02`\
|
||||
q\xe2\xfe\x0a\x80E\x8a\x09K\x00D\x00\xf7R\x00,\
|
||||
\x5c\xdcC\x010\xbab\xf3\x0b\x80\xc5\x8c\xfb%\x00\x16\
|
||||
\xb5\xfb\x84\x00X\xdc\x8eK\x08\x80\x08\xb8'\x08\x80w\
|
||||
;\x9b\x1f\x01\xb0\xf8]?\x02`\x13\x98\x80\x10\x00\x11\
|
||||
p\xad\x08\xc0\x08\x0b\xad\xe5\xd7\xfb\xa9\xd7f\xf3\x0b\x80\
|
||||
\x05s\xe2h\x9c\x10\x11\xaf9l\x02x\xe7!\xf4\xfe\
|
||||
\xe0Z\xbc\xe6Q\xc3t\xd6k\xef\xe1\x9a\xeb\x88\x0fa\
|
||||
\x94j\xb7x\xcd{\xbfN\xea7\xfbz\xb9\xe6:\xd2\
|
||||
M\x1dq\xb1m\xb9\xa6\xab\xafy\xeb\xd7\x1b\xe9Y\xec\
|
||||
Y[=]w\xf9\xf9\x07\xd34=\x94\x91\xad\xe6y\
|
||||
\x8e|\xc6g]\xf7\xb3\xbf\xf7\x93\xd6u\xfdg\xcf\xdf\
|
||||
-a\x04=\xf7\xba\xfd\x1e\x00\x04\x13\x00\x10\x00@\x00\
|
||||
\x00\x01\x00\x04\x00\x10\x00@\x00\x00\x01\x00\x04\x00\x10\x00\
|
||||
@\x00\x00\x01\x00\x04\x00\x10\x00@\x00\x00\x01\x00\x04\x00\
|
||||
\x10\x00@\x00\x00\x01\x00\x04\x00\x18=\x00g\xff\x7f\xcf\
|
||||
\xa1W\xdf\xd8\x1b5\xe5B\xc1\xe6o\xe8\x08 \x02\xf0\
|
||||
\xfd\xbdPS/\x1c\xd27\xff\xed\xf6\xe4\xb3\x01o\xb7\
|
||||
\xf3?\x1f\xf0\x15\x9f\x1b\x88M\x7f\xae\xa6?\x1b\xd0D\
|
||||
0V<=\xcf\xf6\xf91\xe0@\xef*6\x1c\x02 \
|
||||
\x04n\x02\x9b\xc6\xff\x97\x01x\xf6\x1f\x22\x02\x98\x00p\
|
||||
$ !\x00\xa6\x00\xd3\x00c\x8f\xff&\x00\x11 \xdc\
|
||||
\x7f\xdf\xe5\xbf\xf5;\x01\x9c\xe3\xca\x1f\x17\x0aO\xdb\xef\
|
||||
\xfe\x9b&\x00G\x01\xd3\x00cn~G\x00\x11\xc0\x11\
|
||||
`\x1bG\x01G\x02\xa1\x19\xeb\xdd\x7f\xd7\x04\xe0(`\
|
||||
\x1a`\xac\xcd\xbf\xfb\x08 \x02\x22\xc08\x9b\x7f\xd7\x11\
|
||||
\xc0q\xc0\x91@X\xc6\xda\xfc\xbb'\x00\x93\x80i\x80\
|
||||
q6\xff\xdb\x01\x10\x01\x11\xa0\xff\xcd\xff\xf6\x11\xc0q\
|
||||
\xc0\x91@H\xfa\xdf\xfc\x87&\x00\x93\x80i\x80\xbe7\
|
||||
\xffG&\x00\xd3\x80I@<\xfa\xdb\xf8\xa7\x04@\x08\
|
||||
\x84@\x00\xfa\xd8\xf8\xa7\x06@\x08D@\x00\xda\xde\xf8\
|
||||
\x97\x04@\x08\xb2# \x00\xedn\xfcK\x03 \x08\x99\
|
||||
\x11\x10\x80\xf66|\x13\x01`\xach\xbe\x0a\xc1\xd9\x01\
|
||||
\xf0\x13\xa8\xe3\xfcs`\xef\x0c\x87y\xa7\x17\x00D@\
|
||||
\x08\x04\x80+\x22`\x1a@\x00\x84@\x048\xcc7Q\
|
||||
:\x97\xfc\x13\x15\xdf\x044\x01\x98\x04l\x02L\x00$\
|
||||
N\x03\xe2g\x02\xc0\x86@\x00\x10\x01\x1c\x01\x88:\x12\
|
||||
\x88\x9d\x09\x00\x1b\x04\x01@\x04p\x04 \xeaH n\
|
||||
&\x00l\x18\x04\x00\x11\xc0\x11\x80\xa8#\x81\x98\x99\x00\
|
||||
\xb0\x81\x10\x00D\x00G\x00\xa2\x8e\x04\xe2e\x02\xc0\x86\
|
||||
B\x00\x10\x01\x1c\x01\x88:\x12\x88\x95\x09\x00\x1b\x0c\x01\
|
||||
@\x04p\x04 \xeaH N&\x00l8\x04\x00\x11\
|
||||
\xc0\x11\x80\xa8#\x81\x18\x99\x00\xb0\x01\x11\x00D\x00G\
|
||||
\x00\xa2\x8e\x04\xe2c\x02\xc04\x80\x09\x80\xc4i@p\
|
||||
L\x00\x98\x06\x10\x00D\x00\x01@\x04\xf0=\x002\xbe\
|
||||
/ .&\x00L\x03\x08\x00\x22\x80#\x00QG\x02\
|
||||
11\x01`\x1a@\x00\x10\x01\x1c\x01\x88:\x12\x88\x87\
|
||||
\x00\x10\x1a\x01\x9b_\x00\x08\x8d\x80\xcd\x0fA1\xe8\xfd\
|
||||
S\x8c\x01\x00\xda\xf2\x07\xc0\xb4\x09d\x9d\x1fRw\x00\
|
||||
\x00\x00\x00IEND\xaeB`\x82\
|
||||
"
|
||||
|
||||
qt_resource_name = b"\
|
||||
\x00\x08\
|
||||
\x0aaZ\xa7\
|
||||
\x00i\
|
||||
\x00c\x00o\x00n\x00.\x00p\x00n\x00g\
|
||||
"
|
||||
|
||||
qt_resource_struct = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x01\x847\x9eu\x9f\
|
||||
"
|
||||
|
||||
|
||||
def qInitResources():
|
||||
QtCore.qRegisterResourceData(
|
||||
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
|
||||
)
|
||||
|
||||
|
||||
def qCleanupResources():
|
||||
QtCore.qUnregisterResourceData(
|
||||
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
|
||||
)
|
||||
|
||||
|
||||
qInitResources()
|
@ -14,11 +14,18 @@ from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
||||
def reverse_video_file(src: str, dst: str) -> None:
|
||||
"""Reverses a video file, writting the result to `dst`."""
|
||||
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
||||
logger.debug(" ".join(command))
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process.communicate()
|
||||
output, error = process.communicate()
|
||||
|
||||
if output:
|
||||
logger.debug(output.decode())
|
||||
|
||||
if error:
|
||||
logger.debug(error.decode())
|
||||
|
||||
|
||||
class Slide(Scene):
|
||||
class Slide(Scene): # type:ignore
|
||||
"""
|
||||
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
|
||||
"""
|
||||
@ -96,6 +103,17 @@ class Slide(Scene):
|
||||
self.current_slide += 1
|
||||
self.pause_start_animation = self.current_animation
|
||||
|
||||
def add_last_slide(self) -> None:
|
||||
"""Adds a 'last' slide to the end of slides."""
|
||||
self.slides.append(
|
||||
SlideConfig(
|
||||
type=SlideType.last,
|
||||
start_animation=self.pause_start_animation,
|
||||
end_animation=self.current_animation,
|
||||
number=self.current_slide,
|
||||
)
|
||||
)
|
||||
|
||||
def start_loop(self) -> None:
|
||||
"""Starts a loop."""
|
||||
assert self.loop_start_animation is None, "You cannot nest loops"
|
||||
@ -124,6 +142,8 @@ class Slide(Scene):
|
||||
|
||||
Note that cached files only work with Manim.
|
||||
"""
|
||||
self.add_last_slide()
|
||||
|
||||
if not os.path.exists(self.output_folder):
|
||||
os.mkdir(self.output_folder)
|
||||
|
||||
@ -202,7 +222,7 @@ class Slide(Scene):
|
||||
self.save_slides()
|
||||
|
||||
|
||||
class ThreeDSlide(Slide, ThreeDScene):
|
||||
class ThreeDSlide(Slide, ThreeDScene): # type: ignore
|
||||
"""
|
||||
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
|
||||
|
||||
|
@ -4,8 +4,9 @@ from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon, QKeyEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
@ -21,16 +22,16 @@ from .commons import config_options, verbosity_option
|
||||
from .config import Config, Key
|
||||
from .defaults import CONFIG_PATH
|
||||
from .manim import logger
|
||||
from .resources import * # noqa: F401, F403
|
||||
|
||||
WINDOW_NAME: str = "Configuration Wizard"
|
||||
|
||||
keymap = {}
|
||||
for key, value in vars(Qt).items():
|
||||
if isinstance(value, Qt.Key):
|
||||
keymap[value] = key.partition("_")[2]
|
||||
for key in Qt.Key:
|
||||
keymap[key.value] = key.name.partition("_")[2]
|
||||
|
||||
|
||||
class KeyInput(QDialog):
|
||||
class KeyInput(QDialog): # type: ignore
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.key = None
|
||||
@ -42,19 +43,21 @@ class KeyInput(QDialog):
|
||||
self.layout.addWidget(self.label)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def keyPressEvent(self, event: Any) -> None:
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||
self.key = event.key()
|
||||
self.deleteLater()
|
||||
event.accept()
|
||||
|
||||
|
||||
class Wizard(QWidget):
|
||||
class Wizard(QWidget): # type: ignore
|
||||
def __init__(self, config: Config):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle(WINDOW_NAME)
|
||||
self.config = config
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
QBtn = QDialogButtonBox.Save | QDialogButtonBox.Cancel
|
||||
|
||||
@ -128,7 +131,7 @@ class Wizard(QWidget):
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def wizard(config_path, force, merge):
|
||||
def wizard(config_path: str, force: bool, merge: bool) -> None:
|
||||
"""Launch configuration wizard."""
|
||||
return _init(config_path, force, merge, skip_interactive=False)
|
||||
|
||||
@ -137,12 +140,16 @@ def wizard(config_path, force, merge):
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def init(config_path, force, merge, skip_interactive=False):
|
||||
def init(
|
||||
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""Initialize a new default configuration file."""
|
||||
return _init(config_path, force, merge, skip_interactive=True)
|
||||
|
||||
|
||||
def _init(config_path, force, merge, skip_interactive=False):
|
||||
def _init(
|
||||
config_path: str, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""Actual initialization code for configuration file, with optional interactive mode."""
|
||||
|
||||
if os.path.exists(config_path):
|
||||
|
1975
poetry.lock
generated
Normal file
1975
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,56 @@
|
||||
[tool.vulture]
|
||||
paths = ["manim_slides"]
|
||||
[tool.poetry]
|
||||
name = "manim-slides"
|
||||
description = "Tool for live presentations using manim"
|
||||
authors = [
|
||||
"Jérome Eertmans <jeertmans@icloud.com>"
|
||||
]
|
||||
version = "4.7.1"
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/jeertmans/manim-slides"
|
||||
documentation = "https://eertmans.be/manim-slides"
|
||||
repository = "https://github.com/jeertmans/manim-slides"
|
||||
|
||||
keywords = ["manim", "slides", "plugin", "manimgl"]
|
||||
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
"Topic :: Scientific/Engineering",
|
||||
]
|
||||
|
||||
exclude = ["docs/","static/"]
|
||||
packages = [
|
||||
{ include = "manim_slides" },
|
||||
]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8,<3.12"
|
||||
click = ">=8.0"
|
||||
click-default-group = ">=1.2"
|
||||
numpy = ">=1.19"
|
||||
opencv-python = ">=4.6"
|
||||
pydantic = ">=1.9"
|
||||
pyside6 = ">=6"
|
||||
requests = ">=2.26"
|
||||
tqdm = ">=4.62"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
manim = "^0.17.0"
|
||||
manimgl = "^1.6.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools","poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.plugins]
|
||||
[tool.poetry.plugins."console_scripts"]
|
||||
manim-slides = "manim_slides.__main__:cli"
|
||||
|
55
setup.py
55
setup.py
@ -1,55 +0,0 @@
|
||||
# type: ignore
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
import setuptools
|
||||
|
||||
if sys.version_info < (3, 7):
|
||||
raise RuntimeError("This package requires Python 3.7+")
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"__version__", os.path.join("manim_slides", "__version__.py")
|
||||
)
|
||||
version = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(version)
|
||||
|
||||
|
||||
with open("README.md", "r") as f:
|
||||
long_description = f.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="manim-slides",
|
||||
version=version.__version__,
|
||||
author="Jérome Eertmans (previously, Federico A. Galatolo)",
|
||||
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
|
||||
description="Tool for live presentations using manim",
|
||||
url="https://github.com/jeertmans/manim-slides",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
packages=setuptools.find_packages(),
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"manim-slides=manim_slides.main:cli",
|
||||
],
|
||||
},
|
||||
python_requires=">=3.7",
|
||||
install_requires=[
|
||||
"click>=8.0",
|
||||
"click-default-group>=1.2",
|
||||
"numpy>=1.19.3",
|
||||
"pydantic>=1.9.1",
|
||||
"pyqt5>=5.15",
|
||||
"opencv-python>=4.6",
|
||||
"tqdm>=4.62.3",
|
||||
],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
)
|
BIN
static/icon.png
Normal file
BIN
static/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Reference in New Issue
Block a user