mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 11:05:54 +08:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
2169938df7 | |||
a207248deb | |||
1c859991b8 | |||
a24915665d | |||
5cce117050 | |||
bd76fbdfd9 | |||
5b026919ea | |||
05c1a16ca3 | |||
3bd8c386b1 | |||
628c8da832 | |||
3b62e6b788 | |||
ff9aac49d7 | |||
16d9d32d33 | |||
988011ff7d | |||
3dbe12b480 | |||
6ba657c0d5 | |||
6c8ab61f9d | |||
91e6e139e3 | |||
d8acbae165 | |||
75af26e601 | |||
a8903b809d | |||
d813aaf313 | |||
d5679924b9 | |||
fb562d88ac |
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -29,7 +29,7 @@ body:
|
||||
options:
|
||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||
required: true
|
||||
- label: Checked the [frequently qsked questions]](https://manim-slides.eertmans.be/latest/faq.html);
|
||||
- label: Checked the [frequently asked questions]](https://manim-slides.eertmans.be/latest/faq.html);
|
||||
required: true
|
||||
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -18,7 +18,7 @@ body:
|
||||
label: Terms
|
||||
description: 'By submitting this issue, I have:'
|
||||
options:
|
||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Aenhancement+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
|
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
@ -36,7 +36,6 @@ jobs:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@ -67,7 +66,7 @@ jobs:
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
|
||||
tags: |
|
||||
ghcr.io/jeertmans/manim-slides:latest
|
||||
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
|
||||
|
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
@ -70,13 +70,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup Python ${{ matrix.pyversion }}
|
||||
run: uv python install ${{ matrix.pyversion }}
|
||||
|
||||
- name: Install manim dependencies on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
||||
@ -96,14 +93,11 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: ssciwr/setup-mesa-dist-win@v2
|
||||
|
||||
- name: Install Manim Slides
|
||||
run: uv sync --locked --extra tests
|
||||
|
||||
- name: Run pytest
|
||||
run: uv run pytest
|
||||
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
|
@ -6,7 +6,7 @@ ci:
|
||||
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
@ -21,13 +21,13 @@ repos:
|
||||
exclude: poetry.lock
|
||||
args: [--autofix, --trailing-commas]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.8
|
||||
rev: v0.8.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.11.2
|
||||
rev: v1.14.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-setuptools]
|
||||
|
@ -6,9 +6,6 @@ build:
|
||||
apt_packages:
|
||||
- libpango1.0-dev
|
||||
- ffmpeg
|
||||
jobs:
|
||||
post_install:
|
||||
- ipython kernel install --name "manim-slides" --user
|
||||
sphinx:
|
||||
builder: html
|
||||
configuration: docs/source/conf.py
|
||||
|
70
CHANGELOG.md
70
CHANGELOG.md
@ -8,9 +8,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
<!-- start changelog -->
|
||||
|
||||
(unreleased)=
|
||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.7...HEAD)
|
||||
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.2.0...HEAD)
|
||||
|
||||
(unreleased-added)=
|
||||
(v5.2.0)=
|
||||
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
|
||||
|
||||
(v5.2.0-changed)=
|
||||
### Changed
|
||||
|
||||
- The info window is now only shown in presentations when there
|
||||
are multiple monitors. However, the `--show-info-window` option
|
||||
was added to `manim-slides present` to force the info window.
|
||||
When there are multiple monitors, the info window will no longer
|
||||
be on the same monitor as the main window, unless overridden.
|
||||
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
|
||||
[#482](https://github.com/jeertmans/manim-slides/pull/482)
|
||||
|
||||
(v5.2.0-chore)=
|
||||
### Chore
|
||||
|
||||
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies
|
||||
with Manim's.
|
||||
[#499](https://github.com/jeertmans/manim-slides/pull/499)
|
||||
|
||||
(v5.1.10)=
|
||||
## [v5.1.10](https://github.com/jeertmans/manim-slides/compare/v5.1.9...v5.1.10)
|
||||
|
||||
(v5.1.10-added)=
|
||||
### Added
|
||||
|
||||
- Added `--offline` option to `manim-slides convert` for offline
|
||||
HTML presentations.
|
||||
[#440](https://github.com/jeertmans/manim-slides/pull/440)
|
||||
|
||||
(v5.1.10-changed)=
|
||||
### Changed
|
||||
|
||||
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
|
||||
[#488](https://github.com/jeertmans/manim-slides/pull/488)
|
||||
|
||||
(v5.1.10-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fixed PyAV issue by pinning its version to `<14`.
|
||||
A future release will contain a fix that supports both `av>=14`
|
||||
and `av<14`, as their syntax differ, but the former doesn't
|
||||
provide binary wheels for Python 3.9.
|
||||
[#494](https://github.com/jeertmans/manim-slides/pull/494)
|
||||
- Fixed blank web page when converting multiple slides into HTML.
|
||||
[#497](https://github.com/jeertmans/manim-slides/pull/497)
|
||||
|
||||
(v5.1.9)=
|
||||
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)
|
||||
|
||||
(v5.1.9-fixed)=
|
||||
## Chore
|
||||
|
||||
- Fixed failing docker builds.
|
||||
[#481](https://github.com/jeertmans/manim-slides/pull/481)
|
||||
|
||||
(v5.1.8)=
|
||||
## [v5.1.8](https://github.com/jeertmans/manim-slides/compare/v5.1.7...v5.1.8)
|
||||
|
||||
(v5.1.8-added)=
|
||||
### Added
|
||||
|
||||
- Added `manim-slides checkhealth` command to easily obtain important information
|
||||
@ -23,7 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
and asset files.
|
||||
[#470](https://github.com/jeertmans/manim-slides/pull/470)
|
||||
|
||||
(unreleased-chore)=
|
||||
(v5.1.8-chore)=
|
||||
### Chore
|
||||
|
||||
- Pin `rtoml==0.9.0` on Windows platforms,
|
||||
@ -49,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Changed project manager from Rye to uv.
|
||||
[#476](https://github.com/jeertmans/manim-slides/pull/476)
|
||||
|
||||
(unreleased-fixed)=
|
||||
(v5.1.8-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fix combining assets from multiple scenes to avoid filename collision.
|
||||
@ -60,7 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
|
||||
[#465](https://github.com/jeertmans/manim-slides/pull/465)
|
||||
|
||||
(unreleased-removed)=
|
||||
(v5.1.8-removed)=
|
||||
### Removed
|
||||
|
||||
- Removed `full-gl` extra, because it does not make sense to ship both
|
||||
|
@ -26,7 +26,7 @@ keywords:
|
||||
- PowerPoint
|
||||
- Python
|
||||
license: MIT
|
||||
version: v5.1.7
|
||||
version: v5.2.0
|
||||
preferred-citation:
|
||||
publisher:
|
||||
name: The Open Journal
|
||||
|
@ -1,9 +1,8 @@
|
||||
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
|
||||
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
ffmpeg \
|
||||
build-essential \
|
||||
gcc \
|
||||
cmake \
|
||||
@ -24,21 +23,23 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
|
||||
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
|
||||
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
||||
&& tlmgr install \
|
||||
amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \
|
||||
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype ms physics preview ragged2e relsize rsfs \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||
|
||||
# clone and build manim-slides
|
||||
COPY . /opt/manim-slides
|
||||
WORKDIR /opt/manim-slides
|
||||
RUN pip install --no-cache manim[jupyterlab] .[sphinx-directive]
|
||||
ENV UV_PYTHON=/usr/local/bin/python
|
||||
RUN pip install --no-cache-dir uv
|
||||
RUN uv pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
|
||||
|
||||
ARG NB_USER=manimslidesuser
|
||||
ARG NB_UID=1000
|
||||
ENV USER ${NB_USER}
|
||||
ENV NB_UID ${NB_UID}
|
||||
ENV HOME /manim-slides
|
||||
ENV USER=${NB_USER}
|
||||
ENV NB_UID=${NB_UID}
|
||||
ENV HOME=/manim-slides
|
||||
|
||||
RUN adduser --disabled-password \
|
||||
--gecos "Default user" \
|
||||
|
@ -29,13 +29,8 @@ ManimGL support is only guaranteed to work
|
||||
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
||||
and its development is not very active.
|
||||
|
||||
The typical issues are that (1) ManimGL needs an outdated NumPy version
|
||||
and (2) ManimGL **should not** be installed from the GitHub repository,
|
||||
at least not from the `main` branch, but from a version released to PyPI.
|
||||
|
||||
To solve the NumPy issue, you can safely downgrade NumPy to a version supported
|
||||
by ManimGL,
|
||||
while ignoring the possible *conflicting dependencies* messages from `pip` (or else).
|
||||
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
|
||||
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
|
||||
|
||||
### Presenting
|
||||
|
||||
|
@ -29,19 +29,6 @@ please refer to their specific installation guidelines:
|
||||
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
||||
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
||||
|
||||
:::{warning}
|
||||
If you install Manim from its git repository, as suggested by ManimGL,
|
||||
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
|
||||
for ManimGL), otherwise it might install an unsupported version of Manim!
|
||||
See [#314](https://github.com/jeertmans/manim-slides/issues/314).
|
||||
|
||||
Also, note that ManimGL uses outdated dependencies, and may
|
||||
not work out-of-the-box. One example is NumPy: ManimGL
|
||||
does not specify any restriction on this package, but
|
||||
only `numpy<1.25` will work, see
|
||||
[#2053](https://github.com/3b1b/manim/issues/2053).
|
||||
:::
|
||||
|
||||
<!-- end deps -->
|
||||
|
||||
## Pip Install
|
||||
@ -137,12 +124,12 @@ Manim Slides is distributed under Nixpkgs >=24.05.
|
||||
If you are using Nix or NixOS, you can find Manim Slides under:
|
||||
|
||||
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
|
||||
includes pyqt6 (see above);
|
||||
includes PyQt6 (see above);
|
||||
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
||||
module (for notebook magics), and includes IPython but not does not include
|
||||
any Qt bindings.
|
||||
module (for notebook magics), and includes IPython but does not include
|
||||
any Qt binding.
|
||||
|
||||
You can try out the Manim Slides package with
|
||||
You can try out the Manim Slides package with:
|
||||
|
||||
```sh
|
||||
nix-shell -p manim ffmpeg manim-slides
|
||||
@ -160,7 +147,7 @@ nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...
|
||||
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
|
||||
|
||||
:::{note}
|
||||
Nix current does not support `manimgl`.
|
||||
Nix does not currently support `manimgl`.
|
||||
:::
|
||||
|
||||
## When you need a Qt backend
|
||||
|
@ -78,9 +78,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "manim-slides",
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "manim-slides"
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
@ -92,7 +92,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.6"
|
||||
"version": "3.11.8"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "5.1.7"
|
||||
__version__ = "5.2.0"
|
||||
|
@ -15,6 +15,8 @@ from typing import Any, Callable, Optional, Union
|
||||
import av
|
||||
import click
|
||||
import pptx
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from click import Context, Parameter
|
||||
from jinja2 import Template
|
||||
from lxml import etree
|
||||
@ -287,8 +289,11 @@ class RevealTheme(str, StrEnum):
|
||||
|
||||
|
||||
class RevealJS(Converter):
|
||||
# Export option: use data-uri
|
||||
# Export option:
|
||||
data_uri: bool = False
|
||||
offline: bool = Field(
|
||||
False, description="Download remote assets for offline presentation."
|
||||
)
|
||||
# Presentation size options from RevealJS
|
||||
width: Union[Str, int] = Str("100%")
|
||||
height: Union[Str, int] = Str("100%")
|
||||
@ -385,27 +390,25 @@ class RevealJS(Converter):
|
||||
def open(self, file: Path) -> None:
|
||||
webbrowser.open(file.absolute().as_uri())
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
def convert_to(self, dest: Path) -> None: # noqa: C901
|
||||
"""
|
||||
Convert this configuration into a RevealJS HTML presentation, saved to
|
||||
DEST.
|
||||
"""
|
||||
if self.data_uri:
|
||||
assets_dir = Path("") # Actually we won't care.
|
||||
else:
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
|
||||
if not self.data_uri or self.offline:
|
||||
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
||||
|
||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not self.data_uri:
|
||||
num_presentation_configs = len(self.presentation_configs)
|
||||
|
||||
if num_presentation_configs > 1:
|
||||
@ -435,7 +438,9 @@ class RevealJS(Converter):
|
||||
revealjs_template = Template(self.load_template())
|
||||
|
||||
options = self.model_dump()
|
||||
options["assets_dir"] = assets_dir
|
||||
|
||||
if assets_dir is not None:
|
||||
options["assets_dir"] = assets_dir
|
||||
|
||||
has_notes = any(
|
||||
slide_config.notes != ""
|
||||
@ -448,9 +453,28 @@ class RevealJS(Converter):
|
||||
get_duration_ms=get_duration_ms,
|
||||
has_notes=has_notes,
|
||||
env=os.environ,
|
||||
prefix=prefix if not self.data_uri else None,
|
||||
**options,
|
||||
)
|
||||
|
||||
if self.offline:
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
session = requests.Session()
|
||||
|
||||
for tag, inner in [("link", "href"), ("script", "src")]:
|
||||
for item in soup.find_all(tag):
|
||||
if item.has_attr(inner) and (link := item[inner]).startswith(
|
||||
"http"
|
||||
):
|
||||
asset_name = link.rsplit("/", 1)[1]
|
||||
asset = session.get(link)
|
||||
with open(full_assets_dir / asset_name, "wb") as asset_file:
|
||||
asset_file.write(asset.content)
|
||||
|
||||
item[inner] = str(assets_dir / asset_name)
|
||||
|
||||
content = str(soup)
|
||||
|
||||
f.write(content)
|
||||
|
||||
|
||||
@ -590,7 +614,7 @@ class PowerPoint(Converter):
|
||||
|
||||
|
||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wrap a function to add a `--show-config` option."""
|
||||
"""Wrap a function to add a '--show-config' option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
@ -621,7 +645,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
|
||||
|
||||
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wrap a function to add a `--show-template` option."""
|
||||
"""Wrap a function to add a '--show-template' option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
@ -666,7 +690,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
is_flag=True,
|
||||
help="Open the newly created file using the appropriate application.",
|
||||
)
|
||||
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
@ -674,7 +697,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
multiple=True,
|
||||
callback=validate_config_option,
|
||||
help="Configuration options passed to the converter. "
|
||||
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
|
||||
"E.g., pass '-cslide_number=true' to display slide numbers.",
|
||||
)
|
||||
@click.option(
|
||||
"--use-template",
|
||||
@ -682,7 +705,13 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
metavar="FILE",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Use the template given by FILE instead of default one. "
|
||||
"To echo the default template, use ``--show-template``.",
|
||||
"To echo the default template, use '--show-template'.",
|
||||
)
|
||||
@click.option(
|
||||
"--offline",
|
||||
is_flag=True,
|
||||
help="Download any remote content and store it in the assets folder. "
|
||||
"The is a convenient alias to '-coffline=true'.",
|
||||
)
|
||||
@show_template_option
|
||||
@show_config_options
|
||||
@ -693,9 +722,9 @@ def convert(
|
||||
dest: Path,
|
||||
to: str,
|
||||
open_result: bool,
|
||||
force: bool,
|
||||
config_options: dict[str, str],
|
||||
template: Optional[Path],
|
||||
offline: bool,
|
||||
) -> None:
|
||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||
@ -713,6 +742,13 @@ def convert(
|
||||
else:
|
||||
cls = Converter.from_string(to)
|
||||
|
||||
if (
|
||||
offline
|
||||
and issubclass(cls, (RevealJS, HtmlZip))
|
||||
and "offline" not in config_options
|
||||
):
|
||||
config_options["offline"] = "true"
|
||||
|
||||
converter = cls(
|
||||
presentation_configs=presentation_configs,
|
||||
template=template,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Literal, Optional
|
||||
|
||||
import click
|
||||
from click import Context, Parameter
|
||||
@ -222,8 +222,14 @@ def start_at_callback(
|
||||
)
|
||||
@click.option(
|
||||
"--hide-info-window",
|
||||
is_flag=True,
|
||||
help="Hide info window.",
|
||||
flag_value="always",
|
||||
help="Hide info window. By default, hide the info window if there is only one screen.",
|
||||
)
|
||||
@click.option(
|
||||
"--show-info-window",
|
||||
"hide_info_window",
|
||||
flag_value="never",
|
||||
help="Force to show info window.",
|
||||
)
|
||||
@click.option(
|
||||
"--info-window-screen",
|
||||
@ -231,11 +237,13 @@ def start_at_callback(
|
||||
metavar="NUMBER",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Put info window on the given screen (a.k.a. display).",
|
||||
help="Put info window on the given screen (a.k.a. display). "
|
||||
"If there is more than one screen, it will by default put the info window "
|
||||
"on a different screen than the main player.",
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def present(
|
||||
def present( # noqa: C901
|
||||
scenes: list[str],
|
||||
config_path: Path,
|
||||
folder: Path,
|
||||
@ -251,7 +259,7 @@ def present(
|
||||
screen_number: Optional[int],
|
||||
playback_rate: float,
|
||||
next_terminates_loop: bool,
|
||||
hide_info_window: bool,
|
||||
hide_info_window: Optional[Literal["always", "never"]],
|
||||
info_window_screen_number: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
@ -294,22 +302,36 @@ def present(
|
||||
app = qapp()
|
||||
app.setApplicationName("Manim Slides")
|
||||
|
||||
screens = app.screens()
|
||||
|
||||
def get_screen(number: int) -> Optional[QScreen]:
|
||||
try:
|
||||
return app.screens()[number]
|
||||
return screens[number]
|
||||
except IndexError:
|
||||
logger.error(
|
||||
f"Invalid screen number {number}, "
|
||||
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
|
||||
f"allowed values are from 0 to {len(screens)-1} (incl.)"
|
||||
)
|
||||
return None
|
||||
|
||||
should_hide_info_window = False
|
||||
|
||||
if hide_info_window is None:
|
||||
should_hide_info_window = len(screens) == 1
|
||||
elif hide_info_window == "always":
|
||||
should_hide_info_window = True
|
||||
|
||||
if should_hide_info_window and info_window_screen_number is not None:
|
||||
logger.warning(
|
||||
f"Ignoring `--info-window-screen` because `--hide-info-window` is set to `{hide_info_window}`."
|
||||
)
|
||||
|
||||
if screen_number is not None:
|
||||
screen = get_screen(screen_number)
|
||||
else:
|
||||
screen = None
|
||||
|
||||
if info_window_screen_number is not None:
|
||||
if info_window_screen_number is not None and not should_hide_info_window:
|
||||
info_window_screen = get_screen(info_window_screen_number)
|
||||
else:
|
||||
info_window_screen = None
|
||||
@ -333,11 +355,11 @@ def present(
|
||||
screen=screen,
|
||||
playback_rate=playback_rate,
|
||||
next_terminates_loop=next_terminates_loop,
|
||||
hide_info_window=hide_info_window,
|
||||
hide_info_window=should_hide_info_window,
|
||||
info_window_screen=info_window_screen,
|
||||
)
|
||||
|
||||
player.show()
|
||||
player.show(screens)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
sys.exit(app.exec())
|
||||
|
@ -28,7 +28,6 @@ class Info(QWidget): # type: ignore[misc]
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
full_screen: bool,
|
||||
aspect_ratio_mode: Qt.AspectRatioMode,
|
||||
screen: Optional[QScreen],
|
||||
) -> None:
|
||||
@ -38,9 +37,6 @@ class Info(QWidget): # type: ignore[misc]
|
||||
self.setScreen(screen)
|
||||
self.move(screen.geometry().topLeft())
|
||||
|
||||
if full_screen:
|
||||
self.setWindowState(Qt.WindowFullScreen)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
|
||||
# Current slide view
|
||||
@ -243,7 +239,6 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
self.slide_changed.connect(self.slide_changed_callback)
|
||||
|
||||
self.info = Info(
|
||||
full_screen=full_screen,
|
||||
aspect_ratio_mode=aspect_ratio_mode,
|
||||
screen=info_window_screen,
|
||||
)
|
||||
@ -484,11 +479,28 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
self.info.next_media_player.setSource(url)
|
||||
self.info.next_media_player.play()
|
||||
|
||||
def show(self) -> None:
|
||||
def show(self, screens: list[QScreen]) -> None:
|
||||
"""Screens is necessary to prevent the info window from being shown on the same screen as the main window (especially in full screen mode)."""
|
||||
super().show()
|
||||
|
||||
if not self.hide_info_window:
|
||||
self.info.show()
|
||||
if len(screens) > 1 and self.isFullScreen():
|
||||
self.ensure_different_screens(screens)
|
||||
|
||||
if self.isFullScreen():
|
||||
self.info.showFullScreen()
|
||||
else:
|
||||
self.info.show()
|
||||
|
||||
if (
|
||||
len(screens) > 1 and self.info.screen() == self.screen()
|
||||
): # It is better when Qt assigns the location, but if it fails to, this is a fallback
|
||||
self.ensure_different_screens(screens)
|
||||
|
||||
def ensure_different_screens(self, screens: list[QScreen]) -> None:
|
||||
target_screen = screens[1] if self.screen() == screens[0] else screens[0]
|
||||
self.info.setScreen(target_screen)
|
||||
self.info.move(target_screen.geometry().topLeft())
|
||||
|
||||
@Slot()
|
||||
def close(self) -> None:
|
||||
@ -515,6 +527,9 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
|
||||
@Slot()
|
||||
def reverse(self) -> None:
|
||||
if self.playing_reversed_slide and self.current_slide_index >= 1:
|
||||
self.current_slide_index -= 1
|
||||
|
||||
self.load_reversed_slide()
|
||||
self.preview_next_slide()
|
||||
|
||||
@ -535,8 +550,10 @@ class Player(QMainWindow): # type: ignore[misc]
|
||||
def full_screen(self) -> None:
|
||||
if self.windowState() == Qt.WindowFullScreen:
|
||||
self.setWindowState(Qt.WindowNoState)
|
||||
self.info.setWindowState(Qt.WindowNoState)
|
||||
else:
|
||||
self.setWindowState(Qt.WindowFullScreen)
|
||||
self.info.setWindowState(Qt.WindowFullScreen)
|
||||
|
||||
@Slot()
|
||||
def hide_mouse(self) -> None:
|
||||
|
@ -1,7 +1,7 @@
|
||||
__all__ = [
|
||||
"API_NAME",
|
||||
"MANIM",
|
||||
"MANIMGL",
|
||||
"API_NAME",
|
||||
"Slide",
|
||||
"ThreeDSlide",
|
||||
]
|
||||
|
@ -10,29 +10,39 @@ from .base import BaseSlide
|
||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
kwargs.setdefault("file_writer_config", {}).update(
|
||||
skip_animations=True,
|
||||
break_into_partial_movies=True,
|
||||
write_to_movie=True,
|
||||
)
|
||||
# See: https://github.com/3b1b/manim/issues/2261
|
||||
if kwargs["file_writer_config"].setdefault("output_directory", ".") == "":
|
||||
kwargs["file_writer_config"]["output_directory"] = "."
|
||||
|
||||
kwargs["preview"] = False # Avoid opening a preview window
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def _frame_height(self) -> float:
|
||||
return self.camera.frame.get_height() # type: ignore
|
||||
return float(self.camera.get_frame_height())
|
||||
|
||||
@property
|
||||
def _frame_width(self) -> float:
|
||||
return self.camera.frame.get_width() # type: ignore
|
||||
return float(self.camera.get_frame_width())
|
||||
|
||||
@property
|
||||
def _background_color(self) -> str:
|
||||
return self.camera_config["background_color"].hex # type: ignore
|
||||
rgba = self.camera.background_rgba
|
||||
r = int(255 * rgba[0])
|
||||
g = int(255 * rgba[1])
|
||||
b = int(255 * rgba[2])
|
||||
if rgba[3] == 1.0:
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
a = int(255 * rgba[3])
|
||||
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
|
||||
|
||||
@property
|
||||
def _resolution(self) -> tuple[int, int]:
|
||||
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
|
||||
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
|
||||
|
||||
@property
|
||||
def _partial_movie_files(self) -> list[Path]:
|
||||
|
@ -19,31 +19,32 @@
|
||||
<body>
|
||||
<div class="reveal">
|
||||
<div class="slides">
|
||||
{%- for presentation_config in presentation_configs -%}
|
||||
{% for presentation_config in presentation_configs -%}
|
||||
{% set outer_loop = loop %}
|
||||
{%- for slide_config in presentation_config.slides -%}
|
||||
{%- if data_uri -%}
|
||||
{% set file = file_to_data_uri(slide_config.file) %}
|
||||
{%- else -%}
|
||||
{% set file = assets_dir / slide_config.file.name %}
|
||||
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
|
||||
{%- endif -%}
|
||||
<section
|
||||
data-background-size={{ background_size }}
|
||||
data-background-color="{{ presentation_config.background_color }}"
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||
data-background-video-muted
|
||||
{%- endif %}
|
||||
{% if slide_config.loop -%}
|
||||
data-background-video-loop
|
||||
{%- endif -%}
|
||||
{% if slide_config.auto_next -%}
|
||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||
{%- endif -%}>
|
||||
{% if slide_config.notes != "" -%}
|
||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||
{%- endif %}
|
||||
</section>
|
||||
<section
|
||||
data-background-size={{ background_size }}
|
||||
data-background-color="{{ presentation_config.background_color }}"
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 -%}
|
||||
data-background-video-muted
|
||||
{%- endif -%}
|
||||
{% if slide_config.loop -%}
|
||||
data-background-video-loop
|
||||
{%- endif -%}
|
||||
{% if slide_config.auto_next -%}
|
||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||
{%- endif %}
|
||||
>
|
||||
{%- if slide_config.notes != "" -%}
|
||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||
{%- endif %}
|
||||
</section>
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
|
@ -17,7 +17,8 @@ classifiers = [
|
||||
"Topic :: Scientific/Engineering",
|
||||
]
|
||||
dependencies = [
|
||||
"av>=9.0.0",
|
||||
"av>=9.0.0,<14",
|
||||
"beautifulsoup4>=4.12.3",
|
||||
"click>=8.1.3",
|
||||
"click-default-group>=1.2.2",
|
||||
"jinja2>=3.1.2",
|
||||
@ -58,7 +59,7 @@ full = [
|
||||
]
|
||||
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
|
||||
manim = ["manim>=0.18.0"]
|
||||
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
|
||||
manimgl = ["manimgl>=1.7.1", "pyrr"]
|
||||
pyqt6 = ["pyqt6>=6.7.0"]
|
||||
pyqt6-full = ["manim-slides[full,pyqt6]"]
|
||||
pyside6 = ["pyside6>=6.6.1"]
|
||||
@ -87,7 +88,7 @@ Repository = "https://github.com/jeertmans/manim-slides"
|
||||
allow_dirty = false
|
||||
commit = true
|
||||
commit_args = ""
|
||||
current_version = "5.1.7"
|
||||
current_version = "5.2.0"
|
||||
ignore_missing_version = false
|
||||
message = "chore(deps): bump version from {current_version} to {new_version}"
|
||||
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
|
||||
@ -189,7 +190,8 @@ filterwarnings = [
|
||||
'''ignore:'audioop' is deprecated:DeprecationWarning''',
|
||||
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
|
||||
'ignore::DeprecationWarning:pkg_resources.*:',
|
||||
'ignore::DeprecationWarning:pydub.*:',
|
||||
'ignore:invalid escape sequence.*:DeprecationWarning',
|
||||
'ignore:invalid escape sequence.*:SyntaxWarning',
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
@ -226,9 +228,3 @@ dev-dependencies = [
|
||||
"pre-commit>=3.5.0",
|
||||
"setuptools>=73.0.1",
|
||||
]
|
||||
override-dependencies = [
|
||||
# Bypass constraints from ManimGL
|
||||
"manimpango>=0.5.0,<1.0.0",
|
||||
"numpy<=1.24;python_version < '3.12'",
|
||||
"numpy>=1.26;python_version >= '3.12'",
|
||||
]
|
||||
|
@ -39,6 +39,13 @@ def test_checkhealth(
|
||||
del sys.modules["qtpy"] # Avoid using cached module
|
||||
|
||||
with missing_modules(*names):
|
||||
if (
|
||||
not manimlib_missing
|
||||
and not MANIMGL_NOT_INSTALLED
|
||||
and sys.version_info < (3, 10)
|
||||
):
|
||||
pytest.skip("See https://github.com/3b1b/manim/issues/2263")
|
||||
|
||||
result = runner.invoke(
|
||||
checkhealth,
|
||||
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
|
||||
|
@ -156,6 +156,24 @@ class TestConverter:
|
||||
file_contents = out_file.read_text()
|
||||
assert "manim" in file_contents.casefold()
|
||||
|
||||
def test_revealjs_offline_converter(
|
||||
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||
) -> None:
|
||||
out_file = tmp_path / "slides.html"
|
||||
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
|
||||
out_file
|
||||
)
|
||||
assert out_file.exists()
|
||||
assets_dir = Path(tmp_path / "slides_assets")
|
||||
assert assets_dir.is_dir()
|
||||
for file in [
|
||||
"black.min.css",
|
||||
"reveal.min.css",
|
||||
"reveal.min.js",
|
||||
"zenburn.min.css",
|
||||
]:
|
||||
assert (assets_dir / file).exists()
|
||||
|
||||
def test_htmlzip_converter(
|
||||
self, tmp_path: Path, presentation_config: PresentationConfig
|
||||
) -> None:
|
||||
|
@ -6,6 +6,11 @@ import pytest
|
||||
|
||||
import manim_slides.slide as slide
|
||||
|
||||
skip_if_py39 = pytest.mark.skipif(
|
||||
sys.version_info < (3, 10),
|
||||
reason="See https://github.com/3b1b/manim/issues/2263",
|
||||
)
|
||||
|
||||
|
||||
def assert_import(
|
||||
*,
|
||||
@ -20,6 +25,7 @@ def assert_import(
|
||||
assert slide.MANIMGL == manimgl
|
||||
|
||||
|
||||
@skip_if_py39
|
||||
def test_force_api() -> None:
|
||||
pytest.importorskip("manimlib")
|
||||
import manim # noqa: F401
|
||||
@ -53,6 +59,7 @@ def test_invalid_api() -> None:
|
||||
del os.environ[slide.MANIM_API]
|
||||
|
||||
|
||||
@skip_if_py39
|
||||
@pytest.mark.filterwarnings("ignore:assert_import")
|
||||
def test_manim_and_manimgl_imported() -> None:
|
||||
pytest.importorskip("manimlib")
|
||||
@ -79,6 +86,7 @@ def test_manim_imported() -> None:
|
||||
)
|
||||
|
||||
|
||||
@skip_if_py39
|
||||
def test_manimgl_imported() -> None:
|
||||
pytest.importorskip("manimlib")
|
||||
import manimlib # noqa: F401
|
||||
|
@ -26,23 +26,32 @@ from manim_slides.defaults import FOLDER_PATH
|
||||
from manim_slides.render import render
|
||||
from manim_slides.slide.manim import Slide as CESlide
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
|
||||
class _GLSlide:
|
||||
def construct(self) -> None:
|
||||
pass
|
||||
|
||||
def render(self) -> None:
|
||||
pass
|
||||
|
||||
GLSlide = pytest.param(
|
||||
_GLSlide,
|
||||
marks=pytest.mark.skip(reason="See https://github.com/3b1b/manim/issues/2263"),
|
||||
)
|
||||
else:
|
||||
from manim_slides.slide.manimlib import Slide as GLSlide
|
||||
|
||||
_GLSlide = GLSlide
|
||||
|
||||
|
||||
class CEGLSlide(CESlide):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
|
||||
class _GLSlide:
|
||||
pass
|
||||
|
||||
GLSlide = pytest.param(_GLSlide, marks=pytest.mark.skip())
|
||||
else:
|
||||
from manim_slides.slide.manimlib import Slide as GLSlide
|
||||
|
||||
SlideType = Union[type[CESlide], type[GLSlide], type[CEGLSlide]]
|
||||
Slide = Union[CESlide, GLSlide, CEGLSlide]
|
||||
SlideType = Union[type[CESlide], type[_GLSlide], type[CEGLSlide]]
|
||||
Slide = Union[CESlide, _GLSlide, CEGLSlide]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -52,8 +61,8 @@ Slide = Union[CESlide, GLSlide, CEGLSlide]
|
||||
pytest.param(
|
||||
"--GL",
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 12),
|
||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
||||
sys.version_info < (3, 10),
|
||||
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -161,8 +170,8 @@ def test_clear_cache(
|
||||
pytest.param(
|
||||
"--GL",
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info >= (3, 12),
|
||||
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
|
||||
sys.version_info < (3, 10),
|
||||
reason="See https://github.com/3b1b/manim/issues/2263.",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
Reference in New Issue
Block a user