Compare commits

...

34 Commits

Author SHA1 Message Date
ea2ce6505f Merge branch 'main' into gui-scenes 2023-05-11 10:30:08 +02:00
b3fd1d209e [pre-commit.ci] pre-commit autoupdate (#184)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.263 → v0.0.265](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.263...v0.0.265)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-09 10:27:21 +02:00
8c38db0989 chore(convert): add debug message 2023-05-08 19:15:31 +02:00
6da0c36c96 Revert "try(ci): remove cache for media"
This reverts commit 3b01efa6018dfb0902cfcd92e713ac8dd8f70e67.
2023-05-08 19:10:48 +02:00
3b01efa601 try(ci): remove cache for media 2023-05-08 18:39:14 +02:00
c9ef5e9a75 fix(pages): missing assets (#183)
* fix(pages): missing assets

This is a PR to try understanding why some assets are not present

* fix(ci): correct path

* fix(ci): fix path..

* chore(ci): add debug

* chore(ci): more and more debug
2023-05-08 17:43:58 +02:00
bfad43bd38 chore(version): bump 4.13.0 to 4.13.1 2023-05-08 10:19:30 +02:00
6f2cbc9b19 fix(convert): --use-template fixed (#182)
As described in #181, there was a mismatch between the type return by `click` and the one used by `pydantic`. Now we only use `Path` types.

Closes #181
2023-05-08 10:18:57 +02:00
5bd88c2fd5 chore(version): bump 4.12.0 to 4.13.0 2023-05-07 23:24:01 +02:00
f0c17b1e2a chore(paper): update statement of need (#176)
* chore(paper): update statement of need

Closes #171

* chore(paper): making link more time-proof
2023-05-07 23:22:47 +02:00
fce9546a9b chore(ci/deps): publish wheels and add manim/manimgl as extras (#173)
* chore(deps): add manim and manimgl as extras

* chore(ci): publish wheels too

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(ci): run tests on dep. changes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(ci): only use extras to build pages

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-07 23:01:59 +02:00
d6ad56120e chore(docs): update contributing guidelines (#177)
Add seeking for help and reporting an issue sections.

Closes #172
2023-05-07 23:01:44 +02:00
5db0261b01 chore(docs): update install documentation (#175)
* chore(docs): update install documentation

Closes #169

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(docs): update according to code quality report

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-07 23:01:32 +02:00
8ab33ef71f feat(cli): add more debugging messages (#180)
* feat(cli): add more debugging messages

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(lib): fix msg to be more correct

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(version): setup dummy version


chore(version): fix

* chore(ci): udpate version in __version__ too

* chore(version): revert changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-04 10:54:24 +02:00
4da0e2cc2d [pre-commit.ci] pre-commit autoupdate (#178)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.262 → v0.0.263](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.262...v0.0.263)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-02 09:55:05 +02:00
0e82e28313 chore(ci): add action dependabot checks (#174)
Setup dependabot to check for new actions
2023-05-01 11:52:22 +02:00
8b13106fcc chore(paper): suggestions from JOSE review (#168)
* Suggestions for paper

* A  few suggestions for the documentation
2023-05-01 11:06:55 +02:00
bce4d8188f [pre-commit.ci] pre-commit autoupdate (#167)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.260 → v0.0.262](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.260...v0.0.262)
- [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-27 00:00:14 +02:00
c420b47ad2 chore(version): bump 4.11.0 to 4.12.0 2023-04-07 10:42:57 +02:00
fad13f33dc [pre-commit.ci] pre-commit autoupdate (#166)
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.257 → v0.0.260](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.257...v0.0.260)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-07 10:40:04 +02:00
d42a7f5ff1 [pre-commit.ci] pre-commit autoupdate (#164)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.7.0 → v2.8.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.7.0...v2.8.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.255 → v0.0.257](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.255...v0.0.257)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-23 22:59:22 +01:00
88d598709a feat(cli/lib): use scene background color as default (#160)
* Use scene background color as default

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Minor changes to feature: Read scene background

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Small fix for feature "Read bg color"

* chore(ci): add typing ignore

* fix(ci): typo

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2023-03-22 13:59:04 +01:00
2ba9b734a3 chore(version): bump 4.10.0 to 4.11.0 2023-03-20 16:32:24 +01:00
49c4a10453 fix(lib): prevent calling child method (#163)
Change `pause` to not call eventual child's `next_slide` method
2023-03-20 16:24:46 +01:00
8c578d2577 fix(cli): do not terminate slides early (#162)
* fix(cli): do not terminate slides early

When a slide is replayed (either normally or reversed), its state must be reset.

Closes #161

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-20 16:24:36 +01:00
2a327c470b feat(cli): auto detect resolution (#158)
* feat(cli): auto detect resolution

The `present` command will now read by default the resolution of each presentation, and only change it if specified by the user.

This PR also fixes bugs introduced by #156 and previous PRs, where the transition between two presentation was not correct...

* fix(lib): better to test if not None
2023-03-16 15:41:31 +01:00
04dcf530f5 [pre-commit.ci] pre-commit autoupdate (#157)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.254 → v0.0.255](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.254...v0.0.255)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-14 14:48:13 +01:00
9a573f29f1 feat(cli): add start at index options (#156)
* wip: start at index

* feat(cli): add start at index options

* fix(ci): correct callback

* chore(ci): fix type hint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(cli): typo in variable name...

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-11 18:05:07 +01:00
02f425f536 feat(render): support skipping animations (#155)
* feat(render): support skipping animations

Manim Slides now supports when Manim or ManimGL skip rendering some animations. All non-public attributes or methods are now named with leading __

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-10 17:49:37 +01:00
149b12fd01 chore(lib): raise error if skipping
Temporary error before implementing this
2023-03-09 17:29:26 +01:00
6903919767 Merge branch 'main' into gui-scenes 2023-02-01 10:21:35 +01:00
f4971d3a43 Merge branch 'main' into gui-scenes 2022-12-14 08:53:45 +01:00
e62a3b05ed [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-12-09 10:09:39 +00:00
152050612e feat(cli): prompt scenes from gui 2022-12-09 11:08:26 +01:00
24 changed files with 1063 additions and 617 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.10.0
current_version = 4.13.1
commit = True
message = chore(version): bump {current_version} to {new_version}

13
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
labels:
- dependencies

View File

@ -46,22 +46,24 @@ jobs:
run: |
sudo apt-get update
sudo apt-get 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: poetry install --with docs
run: poetry install --extras=manim --with docs
- name: Restore cached media
id: cache-media-restore
uses: actions/cache/restore@v3
with:
path: media
key: ${{ runner.os }}-media
- name: Build animation and convert it into HTML slides
- name: Build animations
run: |
poetry run manim example.py ConvertExample BasicExample ThreeDExample
poetry run manim-slides convert ConvertExample docs/source/_static/slides.html -ccontrols=true
poetry run manim-slides convert BasicExample docs/source/_static/basic_example.html -ccontrols=true
poetry run manim-slides convert ThreeDExample docs/source/_static/three_d_example.html -ccontrols=true
- name: Convert animations to HTML slides
run: |
poetry run manim-slides convert -v DEBUG ConvertExample docs/source/_static/slides.html -ccontrols=true
poetry run manim-slides convert -v DEBUG BasicExample docs/source/_static/basic_example.html -ccontrols=true
poetry run manim-slides convert -v DEBUG ThreeDExample docs/source/_static/three_d_example.html -ccontrols=true
- name: Show docs/source/_static/ dir content (video only)
run: tree -L 3 docs/source/_static/ -P '*.mp4'
- name: Clear cache
run: |
gh extension install actions/gh-actions-cache
@ -82,6 +84,8 @@ jobs:
with:
# Upload docs/build/html dir
path: docs/build/html/
- name: Show docs/build/html/_static/ dir content (video only)
run: tree -L 3 docs/build/html/_static/ -P '*.mp4'
- name: Deploy to GitHub Pages
id: deployment
if: github.event_name != 'pull_request'

View File

@ -1,4 +1,3 @@
# Modified from: https://github.com/pypa/cibuildwheel
name: Upload Python Package
on:
@ -8,41 +7,28 @@ on:
types: [published]
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
build_and_release:
name: Build and release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v3
- uses: actions/setup-python@v2
- name: Install Poetry
run: pipx install poetry
- name: Install build package
run: python -m pip install -U build
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: poetry
- name: Build wheels
run: python -m build --sdist
run: poetry build
- uses: actions/upload-artifact@v2
with:
name: dist
path: dist/*.tar.*
release:
name: Release
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
runs-on: ubuntu-latest
needs: [build_wheels]
steps:
- uses: actions/download-artifact@v2
with:
name: dist
path: dist/
- name: Upload to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
run: poetry publish

View File

@ -1,6 +1,8 @@
on:
pull_request:
paths:
- pyproject.toml
- poetry.lock
- '**.py'
- .github/workflows/test_examples.yml
workflow_dispatch:

View File

@ -12,7 +12,7 @@ repos:
- id: isort
name: isort (python)
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.7.0
rev: v2.8.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
@ -20,15 +20,15 @@ repos:
exclude: poetry.lock
args: [--autofix]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.254
rev: v0.0.265
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
rev: v1.2.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

View File

@ -27,6 +27,9 @@ Tool for live presentations using either [Manim (community edition)](https://www
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
- [Contributing](#contributing)
* [Reporting an Issue](#reporting-an-issue)
* [Seeking for Help](#seeking-for-help)
* [Contact](#contact)
## Installation
@ -54,6 +57,16 @@ The recommended way to install the latest release is to use pip:
pip install manim-slides
```
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
pip install manim-slides[manim] # For Manim
# or
pip install manim-slides[manimgl] # For ManimGL
```
[^1]: NOTE: you still need to have Manim or ManimGL platform-specific dependencies installed on your computer.
### Install From Repository
An alternative way to install Manim Slides is to clone the git repository, and install from there: read the [contributing guide](https://eertmans.be/manim-slides/contributing/workflow.html) to know how.
@ -190,6 +203,55 @@ in *Settings*->*Display*.
Contributions are more than welcome! Please read through [our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
### Reporting an Issue
<!-- start reporting-an-issue -->
If you think you found a bug,
an error in the documentation,
or wish there was some feature that is currently missing,
we would love to hear from you!
The best way to reach us is via the
[GitHub issues](https://github.com/jeertmans/manim-slides/issues).
If your problem is not covered by an already existing (closed or open) issue,
then we suggest you create a
[new issue](https://github.com/jeertmans/manim-slides/issues/new/choose).
You can choose from a list of templates, or open a
[blank issue](https://github.com/jeertmans/manim-slides/issues/new)
if your issue does not fit one of the proposed topics.
The more precise you are in the description of your problem, the faster we will
be able to help you!
<!-- end reporting-an-issue -->
### Seeking for help
<!-- start seeking-for-help -->
Sometimes, you may have a question about Manim Slides,
not necessarily an issue.
There are two ways you can reach us for questions:
- via the `Question/Help/Support` topic when
[choosing an issue template](https://github.com/jeertmans/manim-slides/issues/new/choose);
- or via
[GitHub discussions](https://github.com/jeertmans/manim-slides/discussions).
<!-- end seeking-for-help -->
### Contact
<!-- start contact -->
Finally, if you do not have any GitHub account,
or just wish to contact the author of Manim Slides,
you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
<!-- end contact -->
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
[pypi-version-url]: https://pypi.org/project/manim-slides/
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/manim-slides

View File

@ -2,10 +2,14 @@
Thank you for your interest in Manim Slides! ✨
Manim Slides is an open source project, first created as a fork of [manim-presentation](https://github.com/galatolofederico/manim-presentation) (now deprecated in favor to Manim Slides), and we welcome contributions of all forms.
This section is here to help fist-time contributors know how they can help this project grow. Whether you are already familiar with Manim or GitHub, it is worth taking a few minutes to read those documents!
Manim Slides is an open source project, first created as a fork of
[manim-presentation](https://github.com/galatolofederico/manim-presentation)
(now deprecated in favor to Manim Slides),
and we welcome contributions of all forms.
This section is here to help fist-time contributors know how they can help this
project grow. Whether you are already familiar with Manim or GitHub,
it is worth taking a few minutes to read those documents!
```{toctree}
:hidden:
@ -19,3 +23,24 @@ internals
[Internals](./internals)
: how Manim Slides is built and how the various parts of it work.
## Reporting an Issue
```{include} ../../../README.md
:start-after: <!-- start reporting-an-issue -->
:end-before: <!-- end reporting-an-issue -->
```
## Seeking for Help
```{include} ../../../README.md
:start-after: <!-- start seeking-for-help -->
:end-before: <!-- end seeking-for-help -->
```
## Contact
```{include} ../../../README.md
:start-after: <!-- start contact -->
:end-before: <!-- end contact -->
```

View File

@ -11,7 +11,7 @@ This document is there to help you recreate a working environment for Manim Slid
## Forking the repository and cloning it locally
We used GitHub to host Manim Slides' repository, and we encourage contributors to use git.
We use GitHub to host Manim Slides' repository, and we encourage contributors to use git.
Useful links:
@ -30,6 +30,32 @@ With Poetry, installation becomes straightforward:
poetry install
```
This, however, only installs the minimal set of dependencies to run the package.
If you would like to install Manim or ManimGL, as documented in the [quickstart](../quickstart),
you can use the `--extras` option:
```bash
poetry install --extras manim # For Manim
# or
poetry install --extras manimgl # For ManimGL
```
Additionnally, Manim Slides comes with group dependencies for development purposes:
```bash
poetry install --with dev # For linters and formatters
# or
poetry install --with docs # To build the documentation locally
```
Another group is `test`, but it is only used for
[GitHub actions](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/test_examples.yml).
:::{note}
You can combine any number of groups or extras when installing the package locally.
:::
## Running commands
As modules were installed in a new Python environment, you cannot use them directly in the shell.

View File

@ -1,6 +1,6 @@
# Application Programming Interface
Manim Slides' API is very limited: it simply consists in two classes, `Slide` and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
Manim Slides' API is very limited: it simply consists of two classes, `Slide` and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
Thefore, we only document here the methods we think the end-user will ever use, not the methods used internally when rendering.

View File

@ -4,7 +4,7 @@ Manim Slides allows you to convert presentations into one HTML file, with
[RevealJS](https://revealjs.com/). This file can then be opened with any modern
web browser, allowing for a nice portability of your presentations.
As for every command with Manim Slides, converting slides' fragments into one
As with every command with Manim Slides, converting slides' fragments into one
HTML file (and its assets) can be done in one command:
```bash

View File

@ -67,7 +67,7 @@ and the corresponding tree:
## Without Manim Slides installed on the target machine
An alternative to `manim-slides present` is `manim-slides convert`.
Currently, only HTML conversion is available, but do not hesitate to propose
Currently, HTML and PPTX conversion are available, but do not hesitate to propose
other formats by creating a
[Feature Request](https://github.com/jeertmans/manim-slides/issues/new/choose),
or directly proposing a

View File

@ -32,6 +32,25 @@ class BasicExample(Slide):
self.next_slide() # Waits user to press continue to go to the next slide
class MultipleAnimationsInLastSlide(Slide):
"""This is used to check against solution for issue #161."""
def construct(self):
circle = Circle(color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
self.next_slide()
class TestFileTooLong(Slide):
"""This is used to check against solution for issue #123."""

View File

@ -1 +1 @@
__version__ = "4.10.0"
__version__ = "4.13.1"

View File

@ -54,7 +54,7 @@ def verbosity_option(function: F) -> F:
"-v",
"--verbosity",
type=click.Choice(
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
["PERF", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
case_sensitive=False,
),
help="Verbosity of CLI output",

View File

@ -5,9 +5,10 @@ import subprocess
import tempfile
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional, Set, Union
from typing import Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel, FilePath, root_validator, validator
from pydantic import BaseModel, FilePath, PositiveInt, root_validator, validator
from pydantic.color import Color
from PySide6.QtCore import Qt
from .defaults import FFMPEG_BIN
@ -20,7 +21,7 @@ def merge_basenames(files: List[FilePath]) -> Path:
"""
logger.info(f"Generating a new filename for animations: {files}")
dirname = files[0].parent
dirname: Path = files[0].parent
ext = files[0].suffix
basenames = (file.stem for file in files)
@ -31,7 +32,7 @@ def merge_basenames(files: List[FilePath]) -> Path:
# https://github.com/jeertmans/manim-slides/issues/123
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
return dirname / (basename + ext)
return dirname.joinpath(basename + ext)
class Key(BaseModel): # type: ignore
@ -149,13 +150,15 @@ class SlideConfig(BaseModel): # type: ignore
class PresentationConfig(BaseModel): # type: ignore
slides: List[SlideConfig]
files: List[FilePath]
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black"
@root_validator
def animation_indices_match_files(
cls, values: Dict[str, Union[List[SlideConfig], List[FilePath]]]
) -> Dict[str, Union[List[SlideConfig], List[FilePath]]]:
files = values.get("files")
slides = values.get("slides")
files: List[FilePath] = values.get("files") # type: ignore
slides: List[SlideConfig] = values.get("slides") # type: ignore
if files is None or slides is None:
return values
@ -163,7 +166,7 @@ class PresentationConfig(BaseModel): # type: ignore
n_files = len(files)
for slide in slides:
if slide.end_animation > n_files: # type: ignore
if slide.end_animation > n_files:
raise ValueError(
f"The following slide's contains animations not listed in files {files}: {slide}"
)

View File

@ -24,7 +24,7 @@ from .logger import logger
from .present import get_scenes_presentation_config
def open_with_default(file: Path):
def open_with_default(file: Path) -> None:
system = platform.system()
if system == "Darwin":
subprocess.call(("open", str(file)))
@ -54,7 +54,7 @@ def validate_config_option(
class Converter(BaseModel): # type: ignore
presentation_configs: List[PresentationConfig] = []
assets_dir: str = "{basename}_assets"
template: Optional[str] = None
template: Optional[Path] = None
def convert_to(self, dest: Path) -> None:
"""Converts self, i.e., a list of presentations, into a given format."""
@ -66,7 +66,7 @@ class Converter(BaseModel): # type: ignore
An empty string is returned if no template is used."""
return ""
def open(self, file: Path) -> bool:
def open(self, file: Path) -> Any:
"""Opens a file, generated with converter, using appropriate application."""
raise NotImplementedError
@ -314,6 +314,8 @@ class RevealJS(Converter):
file = presentation_config.files[slide_config.start_animation]
file = assets_dir / file.name
logger.debug(f"Writing video section with file {file}")
# TODO: document this
# Videos are muted because, otherwise, the first slide never plays correctly.
# This is due to a restriction in playing audio without the user doing anything.
@ -321,15 +323,14 @@ class RevealJS(Converter):
# Read more about this:
# https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_and_autoplay_blocking
if slide_config.is_loop():
yield f'<section data-background-size={self.background_size.value} data-background-video="{file}" data-background-video-muted data-background-video-loop></section>'
yield f'<section data-background-size={self.background_size.value} data-background-color="{presentation_config.background_color}" data-background-video="{file}" data-background-video-muted data-background-video-loop></section>'
else:
yield f'<section data-background-size={self.background_size.value} data-background-video="{file}" data-background-video-muted></section>'
yield f'<section data-background-size={self.background_size.value} data-background-color="{presentation_config.background_color}" data-background-video="{file}" data-background-video-muted></section>'
def load_template(self) -> str:
"""Returns the RevealJS HTML template as a string."""
if isinstance(self.template, str):
with open(self.template, "r") as f:
return f.read()
if isinstance(self.template, Path):
return self.template.read_text()
if sys.version_info < (3, 9):
return resources.read_text(data, "revealjs_template.html")
@ -350,6 +351,8 @@ class RevealJS(Converter):
)
full_assets_dir = dirname / assets_dir
logger.debug(f"Assets will be saved to: {full_assets_dir}")
os.makedirs(full_assets_dir, exist_ok=True)
for presentation_config in self.presentation_configs:
@ -376,7 +379,7 @@ class PowerPoint(Converter):
use_enum_values = True
extra = "forbid"
def open(self, file: Path) -> bool:
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
@ -389,7 +392,9 @@ class PowerPoint(Converter):
# From GitHub issue comment:
# - https://github.com/scanny/python-pptx/issues/427#issuecomment-856724440
def auto_play_media(media: pptx.shapes.picture.Movie, loop: bool = False):
def auto_play_media(
media: pptx.shapes.picture.Movie, loop: bool = False
) -> None:
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
el_cnt = xpath(
media.element.getparent().getparent().getparent(),
@ -463,7 +468,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
ctx.exit()
return click.option(
return click.option( # type: ignore
"--show-config",
is_flag=True,
help="Show supported options for given format and exit.",
@ -491,7 +496,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
ctx.exit()
return click.option(
return click.option( # type: ignore
"--show-template",
is_flag=True,
help="Show the template (currently) used for a given conversion format and exit.",

View File

@ -5,7 +5,9 @@ https://github.com/ManimCommunity/manim/blob/d5b65b844b8ce8ff5151a2f56f9dc98cebb
import logging
from rich.console import Console
from rich.logging import RichHandler
from rich.theme import Theme
__all__ = ["logger", "make_logger"]
@ -33,9 +35,12 @@ def make_logger() -> logging.Logger:
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
rich_handler = RichHandler(
show_time=True,
console=Console(theme=Theme({"logging.level.perf": "magenta"})),
)
logging.addLevelName(5, "PERF")
logger = logging.getLogger("manim-slides")
logger.addHandler(rich_handler)
return logger

View File

@ -9,10 +9,12 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import click
import cv2
import numpy as np
from click import Context, Parameter
from pydantic import ValidationError
from pydantic.color import Color
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 PySide6.QtWidgets import QApplication, QFileDialog, QGridLayout, QLabel, QWidget
from tqdm import tqdm
from .commons import config_path_option, verbosity_option
@ -70,10 +72,9 @@ class Presentation:
"""Creates presentation from a configuration object."""
def __init__(self, config: PresentationConfig) -> None:
self.slides: List[SlideConfig] = config.slides
self.files: List[str] = config.files
self.config = config
self.current_slide_index: int = 0
self.__current_slide_index: int = 0
self.current_animation: int = self.current_slide.start_animation
self.current_file: str = ""
@ -87,6 +88,71 @@ class Presentation:
self.reset()
def __len__(self) -> int:
return len(self.slides)
@property
def slides(self) -> List[SlideConfig]:
"""Returns the list of slides."""
return self.config.slides
@property
def files(self) -> List[Path]:
"""Returns the list of animation files."""
return self.config.files
@property
def resolution(self) -> Tuple[int, int]:
"""Returns the resolution."""
return self.config.resolution
@property
def background_color(self) -> Color:
"""Returns the background color."""
return self.config.background_color
@property
def current_slide_index(self) -> int:
return self.__current_slide_index
@current_slide_index.setter
def current_slide_index(self, value: Optional[int]) -> None:
if value is not None:
if -len(self) <= value < len(self):
self.__current_slide_index = value
self.current_animation = self.current_slide.start_animation
logger.debug(f"Set current slide index to {value}")
else:
logger.error(
f"Could not load slide number {value}, playing first slide instead."
)
def set_current_animation_and_update_slide_number(
self, value: Optional[int]
) -> None:
if value is not None:
n_files = len(self.files)
if -n_files <= value < n_files:
if value < 0:
value += n_files
for i, slide in enumerate(self.slides):
if value < slide.end_animation:
self.current_slide_index = i
self.current_animation = value
logger.debug(f"Playing animation {value}, at slide index {i}")
return
assert (
False
), f"An error occurred when setting the current animation to {value}, please create an issue on GitHub!"
else:
logger.error(
f"Could not load animation number {value}, playing first animation instead."
)
@property
def current_slide(self) -> SlideConfig:
"""Returns currently playing slide."""
@ -119,7 +185,7 @@ class Presentation:
self.release_cap()
file: str = self.files[animation]
file: str = str(self.files[animation])
if self.reverse:
file = "{}_reversed{}".format(*os.path.splitext(file))
@ -138,7 +204,9 @@ class Presentation:
def rewind_current_slide(self) -> None:
"""Rewinds current slide to first frame."""
logger.debug("Rewinding curring slide")
logger.debug("Rewinding current slide")
self.current_slide.terminated = False
if self.reverse:
self.current_animation = self.current_slide.end_animation - 1
else:
@ -176,9 +244,10 @@ class Presentation:
def load_previous_slide(self) -> None:
"""Loads previous slide."""
logger.debug("Loading previous slide")
logger.debug(f"Loading previous slide, current is {self.current_slide_index}")
self.cancel_reverse()
self.current_slide_index = max(0, self.current_slide_index - 1)
logger.debug(f"Loading slide index {self.current_slide_index}")
self.rewind_current_slide()
@property
@ -189,7 +258,8 @@ class Presentation:
logger.warn(
f"Something is wrong with video file {self.current_file}, as the fps returned by frame {self.current_frame_number} is 0"
)
return max(fps, 1) # TODO: understand why we sometimes get 0 fps
# TODO: understand why we sometimes get 0 fps
return max(fps, 1) # type: ignore
def reset(self) -> None:
"""Rests current presentation."""
@ -217,7 +287,7 @@ class Presentation:
return self.current_animation + 1
@property
def is_last_animation(self) -> int:
def is_last_animation(self) -> bool:
"""Returns True if current animation is the last one of current slide."""
if self.reverse:
return self.current_animation == self.current_slide.start_animation
@ -280,16 +350,20 @@ class Display(QThread): # type: ignore
change_video_signal = Signal(np.ndarray)
change_info_signal = Signal(dict)
change_presentation_sigal = Signal()
finished = Signal()
def __init__(
self,
presentations: List[PresentationConfig],
presentations: List[Presentation],
config: Config = DEFAULT_CONFIG,
start_paused: bool = False,
skip_all: bool = False,
record_to: Optional[str] = None,
exit_after_last_slide: bool = False,
start_at_scene_number: Optional[int] = None,
start_at_slide_number: Optional[int] = None,
start_at_animation_number: Optional[int] = None,
) -> None:
super().__init__()
self.presentations = presentations
@ -301,17 +375,53 @@ class Display(QThread): # type: ignore
self.state = State.PLAYING
self.lastframe: Optional[np.ndarray] = None
self.current_presentation_index = 0
self.__current_presentation_index = 0
self.current_presentation_index = start_at_scene_number # type: ignore
self.current_presentation.current_slide_index = start_at_slide_number # type: ignore
self.current_presentation.set_current_animation_and_update_slide_number(
start_at_animation_number
)
self.run_flag = True
self.key = -1
self.exit_after_last_slide = exit_after_last_slide
def __len__(self) -> int:
return len(self.presentations)
@property
def current_presentation_index(self) -> int:
return self.__current_presentation_index
@current_presentation_index.setter
def current_presentation_index(self, value: Optional[int]) -> None:
if value is not None:
if -len(self) <= value < len(self):
self.__current_presentation_index = value
self.current_presentation.release_cap()
self.change_presentation_sigal.emit()
else:
logger.error(
f"Could not load scene number {value}, playing first scene instead."
)
@property
def current_presentation(self) -> Presentation:
"""Returns the current presentation."""
return self.presentations[self.current_presentation_index]
@property
def current_resolution(self) -> Tuple[int, int]:
"""Returns the resolution of the current presentation."""
return self.current_presentation.resolution
@property
def current_background_color(self) -> Color:
"""Returns the background color of the current presentation."""
return self.current_presentation.background_color
def run(self) -> None:
"""Runs a series of presentations until end or exit."""
while self.run_flag:
@ -338,6 +448,21 @@ class Display(QThread): # type: ignore
lag = now() - last_time
sleep_time = 1 / self.current_presentation.fps
logger.log(
5,
f"Took {lag:.3f} seconds to process the current frame, that must play at a rate of one every {sleep_time:.3f} seconds.",
)
if sleep_time - lag < 0:
logger.warn(
"The FPS rate could not be matched. "
"This is normal when manually transitioning between slides.\n"
"If you feel that the FPS are too low, "
"consider checking this issue:\n"
"https://github.com/jeertmans/manim-slides/issues/179."
)
sleep_time = max(sleep_time - lag, 0)
time.sleep(sleep_time)
last_time = now()
@ -346,7 +471,7 @@ class Display(QThread): # type: ignore
if self.record_to is not None:
self.record_movie()
logger.debug("Closing video thread gracully and exiting")
logger.debug("Closing video thread gracefully and exiting")
self.finished.emit()
def record_movie(self) -> None:
@ -520,7 +645,6 @@ class App(QWidget): # type: ignore
*args: Any,
config: Config = DEFAULT_CONFIG,
fullscreen: bool = False,
resolution: Tuple[int, int] = (1980, 1080),
hide_mouse: bool = False,
aspect_ratio: AspectRatio = AspectRatio.auto,
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
@ -532,7 +656,12 @@ class App(QWidget): # type: ignore
self.setWindowTitle(WINDOW_NAME)
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.display_width, self.display_height = resolution
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
self.display_width, self.display_height = self.thread.current_resolution
self.aspect_ratio = aspect_ratio
self.resize_mode = resize_mode
self.hide_mouse = hide_mouse
@ -546,15 +675,14 @@ class App(QWidget): # type: ignore
self.label.setScaledContents(True)
self.label.setAlignment(Qt.AlignCenter)
self.label.resize(self.display_width, self.display_height)
self.label.setStyleSheet(f"background-color: {background_color}")
self.label.setStyleSheet(
f"background-color: {self.thread.current_background_color}"
)
self.pixmap = QPixmap(self.width(), self.height())
self.label.setPixmap(self.pixmap)
self.label.setMinimumSize(1, 1)
# create the video capture thread
kwargs["config"] = config
self.thread = Display(*args, **kwargs)
# create the info dialog
self.info = Info()
self.info.show()
@ -568,6 +696,7 @@ class App(QWidget): # type: ignore
# connect signals
self.thread.change_video_signal.connect(self.update_image)
self.thread.change_info_signal.connect(self.info.update_info)
self.thread.change_presentation_sigal.connect(self.update_canvas)
self.thread.finished.connect(self.closeAll)
self.send_key_signal.connect(self.thread.set_key)
@ -621,6 +750,17 @@ class App(QWidget): # type: ignore
self.label.setPixmap(QPixmap.fromImage(qt_img))
@Slot()
def update_canvas(self) -> None:
"""Update the canvas when a presentation has changed."""
logger.debug("Updating canvas")
self.display_width, self.display_height = self.thread.current_resolution
if not self.isFullScreen():
self.resize(self.display_width, self.display_height)
self.label.setStyleSheet(
f"background-color: {self.thread.current_background_color}"
)
@click.command()
@click.option(
@ -690,7 +830,7 @@ def prompt_for_scenes(folder: Path) -> List[str]:
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
return scenes
return scenes # type: ignore
except ValueError as e:
raise click.UsageError(str(e))
@ -718,6 +858,37 @@ def get_scenes_presentation_config(
return presentation_configs
def start_at_callback(
ctx: Context, param: Parameter, values: str
) -> Tuple[Optional[int], ...]:
if values == "(None, None, None)":
return (None, None, None)
def str_to_int_or_none(value: str) -> Optional[int]:
if value.lower().strip() == "":
return None
else:
try:
return int(value)
except ValueError:
raise click.BadParameter(
f"start index can only be an integer or an empty string, not `{value}`",
ctx=ctx,
param=param,
)
values_tuple = values.split(",")
n_values = len(values_tuple)
if n_values == 3:
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 3 arguments are expected but you gave {n_values}, please use commas to separate them",
ctx=ctx,
param=param,
)
@click.command()
@click.argument("scenes", nargs=-1)
@config_path_option
@ -742,9 +913,8 @@ def get_scenes_presentation_config(
"--resolution",
metavar="<WIDTH HEIGHT>",
type=(int, int),
default=(1920, 1080),
default=None,
help="Window resolution WIDTH HEIGHT used if fullscreen is not set. You may manually resize the window afterward.",
show_default=True,
)
@click.option(
"--to",
@ -785,10 +955,47 @@ def get_scenes_presentation_config(
"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)".',
default=None,
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)". If not set, it defaults to the background color configured in the Manim scene.',
show_default=True,
)
@click.option(
"--sa",
"--start-at",
"start_at",
metavar="<SCENE,SLIDE,ANIMATION>",
type=str,
callback=start_at_callback,
default=(None, None, None),
help="Start presenting at (x, y, z), equivalent to --sacn x --sasn y --saan z, and overrides values if not None.",
)
@click.option(
"--sacn",
"--start-at-scene-number",
"start_at_scene_number",
metavar="INDEX",
type=int,
default=None,
help="Start presenting at a given scene number (0 is first, -1 is last).",
)
@click.option(
"--sasn",
"--start-at-slide-number",
"start_at_slide_number",
metavar="INDEX",
type=int,
default=None,
help="Start presenting at a given slide number (0 is first, -1 is last).",
)
@click.option(
"--saan",
"--start-at-animation-number",
"start_at_animation_number",
metavar="INDEX",
type=int,
default=0,
help="Start presenting at a given animation number (0 is first, -1 is last). This conflicts with slide number since animation number is absolute to the presentation.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
@ -798,13 +1005,17 @@ def present(
start_paused: bool,
fullscreen: bool,
skip_all: bool,
resolution: Tuple[int, int],
resolution: Optional[Tuple[int, int]],
record_to: Optional[Path],
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
resize_mode: str,
background_color: str,
background_color: Optional[str],
start_at: Tuple[Optional[int], Optional[int], Optional[int]],
start_at_scene_number: Optional[int],
start_at_slide_number: Optional[int],
start_at_animation_number: Optional[int],
) -> None:
"""
Present SCENE(s), one at a time, in order.
@ -816,12 +1027,38 @@ def present(
Use `manim-slide list-scenes` to list all available scenes in a given folder.
"""
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides")
dialog = QFileDialog()
dialog.setDirectory(FOLDER_PATH if os.path.exists(FOLDER_PATH) else "")
dialog.setFileMode(QFileDialog.ExistingFiles)
dialog.setNameFilters(["JSON (*.json)", "* (*.*)"])
if dialog.exec():
filenames = dialog.selectedFiles()
print(filenames)
# TODO:
# - get files in selected order
# - kill dialog
# - add cli option (+ envvar) to prompt gui instead of cli
# - use scenes selected from gui
if skip_all:
exit_after_last_slide = True
presentation_configs = get_scenes_presentation_config(scenes, folder)
if resolution is not None:
for presentation_config in presentation_configs:
presentation_config.resolution = resolution
if background_color is not None:
for presentation_config in presentation_configs:
presentation_config.background_color = background_color
presentations = [
Presentation(presentation_config)
for presentation_config in get_scenes_presentation_config(scenes, folder)
for presentation_config in presentation_configs
]
if config_path.exists():
@ -840,6 +1077,15 @@ def present(
"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
)
if start_at[0]:
start_at_scene_number = start_at[0]
if start_at[1]:
start_at_slide_number = start_at[1]
if start_at[2]:
start_at_animation_number = start_at[2]
app = QApplication(sys.argv)
app.setApplicationName("Manim Slides")
a = App(
@ -848,13 +1094,14 @@ def present(
start_paused=start_paused,
fullscreen=fullscreen,
skip_all=skip_all,
resolution=resolution,
record_to=record_to,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio=ASPECT_RATIO_MODES[aspect_ratio],
resize_mode=RESIZE_MODES[resize_mode],
background_color=background_color,
start_at_scene_number=start_at_scene_number,
start_at_slide_number=start_at_slide_number,
start_at_animation_number=start_at_animation_number,
)
a.show()
sys.exit(app.exec_())

View File

@ -2,7 +2,7 @@ import os
import platform
import shutil
import subprocess
from typing import Any, List, Optional
from typing import Any, List, Optional, Tuple
from warnings import warn
from tqdm import tqdm
@ -47,15 +47,31 @@ class Slide(Scene): # type:ignore
super().__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides: List[SlideConfig] = []
self.current_slide = 1
self.current_animation = 0
self.loop_start_animation: Optional[int] = None
self.pause_start_animation = 0
self.__output_folder = output_folder
self.__slides: List[SlideConfig] = []
self.__current_slide = 1
self.__current_animation = 0
self.__loop_start_animation: Optional[int] = None
self.__pause_start_animation = 0
@property
def partial_movie_files(self) -> List[str]:
def __background_color(self) -> str:
"""Returns the scene's background color."""
if MANIMGL:
return self.camera_config["background_color"].hex # type: ignore
else:
return config["background_color"].hex # type: ignore
@property
def __resolution(self) -> Tuple[int, int]:
"""Returns the scene's resolution used during rendering."""
if MANIMGL:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
else:
return config["pixel_width"], config["pixel_height"]
@property
def __partial_movie_files(self) -> List[str]:
"""Returns a list of partial movie files, a.k.a animations."""
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
@ -64,34 +80,41 @@ class Slide(Scene): # type:ignore
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
return get_sorted_integer_files( # type: ignore
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
return self.renderer.file_writer.partial_movie_files # type: ignore
@property
def show_progress_bar(self) -> bool:
def __show_progress_bar(self) -> bool:
"""Returns True if progress bar should be displayed."""
if MANIMGL:
return getattr(super(Scene, self), "show_progress_bar", True)
return getattr(self, "show_progress_bar", True)
else:
return config["progress_bar"] != "none"
return config["progress_bar"] != "none" # type: ignore
@property
def leave_progress_bar(self) -> bool:
def __leave_progress_bar(self) -> bool:
"""Returns True if progress bar should be left after completed."""
if MANIMGL:
return getattr(super(Scene, self), "leave_progress_bars", False)
return getattr(self, "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
return config["progress_bar"] == "leave" # type: ignore
@property
def __start_at_animation_number(self) -> Optional[int]:
if MANIMGL:
return getattr(self, "start_at_animation_number", None)
else:
return config["from_animation_number"] # type: ignore
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overloads `self.play` and increment animation count."""
super().play(*args, **kwargs)
self.current_animation += 1
self.__current_animation += 1
def next_slide(self):
def next_slide(self) -> None:
"""
Creates a new slide with previous animations.
@ -133,19 +156,19 @@ class Slide(Scene): # type:ignore
self.play(FadeOut(text))
"""
assert (
self.loop_start_animation is None
self.__loop_start_animation is None
), "You cannot call `self.next_slide()` inside a loop"
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.slide,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
start_animation=self.__pause_start_animation,
end_animation=self.__current_animation,
number=self.__current_slide,
)
)
self.current_slide += 1
self.pause_start_animation = self.current_animation
self.__current_slide += 1
self.__pause_start_animation = self.__current_animation
def pause(self) -> None:
"""
@ -159,24 +182,24 @@ class Slide(Scene): # type:ignore
DeprecationWarning,
stacklevel=2,
)
self.next_slide()
Slide.next_slide(self)
def add_last_slide(self) -> None:
def __add_last_slide(self) -> None:
"""Adds a 'last' slide to the end of slides."""
if (
len(self.slides) > 0
and self.current_animation == self.slides[-1].end_animation
len(self.__slides) > 0
and self.__current_animation == self.__slides[-1].end_animation
):
self.slides[-1].type = SlideType.last
self.__slides[-1].type = SlideType.last
return
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.last,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
start_animation=self.__pause_start_animation,
end_animation=self.__current_animation,
number=self.__current_slide,
)
)
@ -207,42 +230,42 @@ class Slide(Scene): # type:ignore
self.end_loop()
"""
assert self.loop_start_animation is None, "You cannot nest loops"
self.loop_start_animation = self.current_animation
assert self.__loop_start_animation is None, "You cannot nest loops"
self.__loop_start_animation = self.__current_animation
def end_loop(self) -> None:
"""Ends an existing loop. See :func:`start_loop` for more details."""
assert (
self.loop_start_animation is not None
self.__loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
self.__slides.append(
SlideConfig(
type=SlideType.loop,
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
start_animation=self.__loop_start_animation,
end_animation=self.__current_animation,
number=self.__current_slide,
)
)
self.current_slide += 1
self.loop_start_animation = None
self.pause_start_animation = self.current_animation
self.__current_slide += 1
self.__loop_start_animation = None
self.__pause_start_animation = self.__current_animation
def save_slides(self, use_cache: bool = True) -> None:
def __save_slides(self, use_cache: bool = True) -> None:
"""
Saves slides, optionally using cached files.
Note that cached files only work with Manim.
"""
self.add_last_slide()
self.__add_last_slide()
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
if not os.path.exists(self.__output_folder):
os.mkdir(self.__output_folder)
files_folder = os.path.join(self.output_folder, "files")
files_folder = os.path.join(self.__output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
scene_name = type(self).__name__
scene_name = str(self)
scene_files_folder = os.path.join(files_folder, scene_name)
old_animation_files = set()
@ -257,12 +280,18 @@ class Slide(Scene): # type:ignore
files = []
for src_file in tqdm(
self.partial_movie_files,
self.__partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=self.leave_progress_bar,
leave=self.__leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self.show_progress_bar,
disable=not self.__show_progress_bar,
):
if src_file is None and not MANIMGL:
# This happens if rendering with -na,b (manim only)
# where animations not in [a,b] will be skipped
# but animations before a will have a None src_file
continue
filename = os.path.basename(src_file)
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
@ -282,14 +311,30 @@ class Slide(Scene): # type:ignore
files.append(dst_file)
if offset := self.__start_at_animation_number:
self.__slides = [
slide for slide in self.__slides if slide.end_animation > offset
]
for slide in self.__slides:
slide.start_animation -= offset
slide.end_animation -= offset
logger.info(
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
)
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
slide_path = os.path.join(self.__output_folder, "%s.json" % (scene_name,))
with open(slide_path, "w") as f:
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
f.write(
PresentationConfig(
slides=self.__slides,
files=files,
resolution=self.__resolution,
background_color=self.__background_color,
).json(indent=2)
)
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
@ -298,11 +343,11 @@ class Slide(Scene): # type:ignore
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
self.__save_slides(use_cache=False)
def render(self, *args: Any, **kwargs: Any) -> None:
"""MANIM render"""
# We need to disable the caching limit since we rely on intermidiate files
# We need to disable the caching limit since we rely on intermediate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
@ -310,7 +355,7 @@ class Slide(Scene): # type:ignore
config["max_files_cached"] = max_files_cached
self.save_slides()
self.__save_slides()
class ThreeDSlide(Slide, ThreeDScene): # type: ignore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

@ -64,13 +64,16 @@ provide new features on a regular basis.
# Easy to Use Commitment
Manim Slides is commited to be an easy-to-use tool, when minimal installation
Manim Slides is commited to be an easy-to-use tool, with minimal installation
procedure and few modifications required. It can either be used locally with its
graphical user interface (GUI), or shared via HTML thanks to the RevealJS
Javascript package [@revealjs].
graphical user interface (GUI), or shared via one of the two formats it can
convert to:
* an HTML page thanks to the RevealJS Javascript package [@revealjs];
* or a PowerPoint (`.pptx`) file.
This work has a very similar syntax to Manim and offers a comprehensive
documentation hosted on [GitHub pages](https://eertmans.be/manim-slides/), see
documentation hosted on [GitHub pages](https://jeertmans.github.io/manim-slides/), see
\autoref{fig:docs}.
![Manim Slides' documentation homepage.\label{fig:docs}](docs.png)
@ -79,10 +82,10 @@ documentation hosted on [GitHub pages](https://eertmans.be/manim-slides/), see
We have used manim-presentation for our presentation at the COST
Interact, hosted in Lyon, 2022, and
[available online](https://eertmans.be/research/cost-interact-presentation/).
[available online](https://web.archive.org/web/20230507184944/https://eertmans.be/posts/cost-interact-presentation/).
This experience highly motivated the development of Manim Slides, and our
EuCAP 2023 presentation slides are already
[available online](https://eertmans.be/research/eucap-presentation/), thanks
[available online](https://web.archive.org/web/20230507211243/https://eertmans.be/posts/eucap-presentation/), thanks
to Manim Slides' HTML feature.
Also, one of our users created a short
@ -92,8 +95,8 @@ and posted it on YouTube.
# Stability and releases
Manim Slides is continously tested on most recent Python versions, both ManimCE
and ManimGL, and on all major platforms: **Ubuntu**, **macOS** and **Windows**. As of Manim
Slide's exposed API begin very minimal, and the variaty of tests that are
and ManimGL, and on all major platforms: **Ubuntu**, **macOS** and **Windows**. Due to Manim
Slide's exposed API being very minimal, and the variety of tests that are
performed, this tool can be considered stable over time.
New releases are very frequent, as they mostly introduce enhancements or small
@ -111,12 +114,31 @@ presenting Manim content in front of an audience much easier than before,
allowing presenters to focus more on the content of their slides, rather than on
how to actually present them efficiently.
## Target Audience
Manim Slides was developed with the goal of making educational content more
accessible than ever. We believe that researchers, professors, teaching
assistants and anyone else who needs to teach scientific content can benefit
from using this tool. The ability to pace your presentation yourself is
essential, and Manim Slides gives you that ability.
## A Need for Portability
One of the major concerns with presenting content in a non-standard format
(i.e., not just a plain PDF) is the issue of portability.
Depending on the programs available, the power of the target computer,
or the access to the internet, not all solutions are equal.
From the same configuration file, Manim Slides offers a series of solutions to
share your slides, which we discuss on our
[Sharing your slides](https://jeertmans.github.io/manim-slides/reference/sharing.html)
page.
# Acknowledgements
We acknowledge the work of [@manim-presentation] that paved the initial structure
of Manim Slides with the manim-presentation Python package.
We also acknowledge Grant Sanderson for its termendous work on Manim, as well as
We also acknowledge Grant Sanderson for his tremendous work on Manim, as
well as the Manim Community contributors.
Finally, we also acknowledge contributions from the GitHub contributors on the

850
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,28 +10,10 @@ profile = "black"
py_version = 38
[tool.mypy]
check-untyped-defs = true
# Disallow dynamic typing
disallow-any-generics = true
disallow-incomplete-defs = true
disallow-subclassing-any = true
# Disallow untyped definitions and calls
disallow-untyped-defs = true
ignore-missing-imports = true
install-types = true
# None and optional handling
no-implicit-optional = true
no-warn-return-any = true
non-interactive = true
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
# Strict equality
strict-equality = true
warn-no-return = true
warn-redundant-casts = true
# Config file
warn-unused-configs = true
# Configuring warnings
warn-unused-ignores = true
strict = true
[tool.poetry]
authors = [
@ -61,12 +43,14 @@ packages = [
]
readme = "README.md"
repository = "https://github.com/jeertmans/manim-slides"
version = "4.10.0"
version = "4.13.1"
[tool.poetry.dependencies]
click = "^8.1.3"
click-default-group = "^1.2.2"
lxml = "^4.9.2"
manim = {version = "^0.17.0", optional = true}
manimgl = {version = "^1.6.1", optional = true}
numpy = "^1.19"
opencv-python = "^4.6.0.66"
pydantic = "^1.10.2"
@ -77,6 +61,10 @@ requests = "^2.28.1"
rich = "^13.3.2"
tqdm = "^4.64.1"
[tool.poetry.extras]
manim = ["manim"]
manimgl = ["manimgl"]
[tool.poetry.group.dev.dependencies]
black = "^22.10.0"
bump2version = "^1.0.1"