Compare commits

...

2 Commits

Author SHA1 Message Date
5734d0dc18 chore(gh-pages): remove hidden files 2024-01-31 14:13:22 +01:00
b5620650ba chore(gh-pages): remove everything 2024-01-31 14:11:18 +01:00
133 changed files with 4 additions and 13878 deletions

View File

@ -1,16 +0,0 @@
[bumpversion]
current_version = 5.1.0
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?
serialize =
{major}.{minor}.{patch}-rc{release}
{major}.{minor}.{patch}
commit = True
message = chore(version): bump {current_version} to {new_version}
[bumpversion:file:manim_slides/__version__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
[bumpversion:file:CITATION.cff]
search = version: v{current_version}
replace = version: v{new_version}

3
.github/FUNDING.yml vendored
View File

@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [jeertmans]

View File

@ -1,40 +0,0 @@
name: Bug
description: Report an issue to help improve the project.
labels: bug
title: '[BUG] <description>'
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
validations:
required: true
- type: textarea
id: platform
attributes:
label: Platform
description: What is your platform. Linux, macOS, or Windows?
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this bug?
validations:
required: false

View File

@ -1,59 +0,0 @@
name: Documentation
description: Ask / Report an issue related to the documentation.
title: 'DOC: <description>'
labels: [bug, docs]
body:
- type: markdown
attributes:
value: >
**Thank you for wanting to report a problem with manim-slides docs!**
If the problem seems straightforward, feel free to submit a PR instead!
Verify first that your issue is not already reported on GitHub [Issues].
[Issues]:
https://github.com/jeertmans/manim-slides/issues
- type: textarea
attributes:
label: Describe the Issue
description: A clear and concise description of the issue you encountered.
validations:
required: true
- type: input
attributes:
label: Affected Page
description: Add a link to page with the problem.
validations:
required: true
- type: dropdown
attributes:
label: Issue Type
description: >
Please select the option in the drop-down.
<details>
<summary>
<em>Issue?</em>
</summary>
</details>
options:
- Documentation Enhancement
- Documentation Report
validations:
required: true
- type: textarea
attributes:
label: Recommended fix or suggestions
description: A clear and concise description of how you want to update it.
validations:
required: false

View File

@ -1,26 +0,0 @@
name: Feature Request
description: Have a new idea/feature? Please suggest!
labels: enhancement
title: '[FEATURE] <description>'
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the enhancement you propose, also include what you tried and what worked.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this idea?
validations:
required: false

View File

@ -1,14 +0,0 @@
name: Question/Help/Support
description: Ask us about Manim Slides
title: 'Support: Ask us anything'
labels: [help, question]
body:
- type: textarea
attributes:
label: "Please explain the issue you're experiencing (with as much detail as possible):"
description: >
Please make sure to leave a reference to the document/code you're
referring to.
validations:
required: true

View File

@ -1,13 +0,0 @@
# 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

@ -1,26 +0,0 @@
<!-- If your PR fixes an open issue, use `Closes #999` to link your PR with the issue. #999 stands for the issue number you are fixing -->
## Fixes Issue
<!-- Remove this section if not applicable -->
<!-- Example: Closes #31 -->
## Description
<!-- Describe all the proposed changes in your PR -->
## Check List (Check all the applicable boxes)
- [ ] I understand that my contributions needs to pass the checks.
- [ ] If I created new functions / methods, I documented them and add type hints.
- [ ] If I modified already existing code, I updated the documentation accordingly.
- [ ] The title of my pull request is a short description of the requested changes.
## Screenshots
<!-- Add all the screenshots which support your changes -->
## Note to reviewers
<!-- Add notes to reviewers if applicable -->

View File

@ -1,34 +0,0 @@
# From: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup caches by a branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,72 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: CodeQL
on:
push:
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule:
- cron: 45 3 * * 2
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [python]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@ -1,33 +0,0 @@
# Simple workflow for deploying static content to GitHub Pages
name: Create JOSE Paper
on:
# Runs on pushes targeting the default branch
push:
paths:
- paper/*
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
paper:
runs-on: ubuntu-latest
name: Paper Draft
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build draft PDF
uses: openjournals/openjournals-draft-action@master
with:
journal: jose
# This should be the path to the paper within your repo.
paper-path: paper/paper.md
- name: Upload
uses: actions/upload-artifact@v4
with:
name: paper
# This is the output path where Pandoc will write the compiled
# PDF. Note, this should be the same directory as the input
# paper.md
path: paper/paper.pdf

View File

@ -1,15 +0,0 @@
on:
pull_request:
workflow_dispatch:
name: LanguageTool check
jobs:
languagetool_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: reviewdog/action-languagetool@v1
with:
reporter: github-pr-review
level: warning

View File

@ -1,13 +0,0 @@
name: Keep the versions up-to-date
on:
release:
types: [published, edited]
jobs:
actions-tagger:
runs-on: windows-latest
steps:
- uses: Actions-R-Us/actions-tagger@latest
with:
publish_latest_tag: true

View File

@ -1,71 +0,0 @@
name: Upload Packages
on:
push:
release:
types: [published]
jobs:
publish-python:
name: Publish Python package
runs-on: ubuntu-latest
environment: release
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: '3.10'
cache: true
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
run: pdm publish
publish-docker:
name: Publish Docker image
runs-on: ubuntu-latest
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
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: create_release
shell: python
env:
tag_act: ${{ github.ref }}
run: |
import os
ref_tag = os.getenv('tag_act').split('/')[-1]
with open(os.getenv('GITHUB_OUTPUT'), 'w') as f:
print(f"tag_name={ref_tag}", file=f)
- name: Build and push
uses: docker/build-push-action@v5
with:
platforms: linux/arm64,linux/amd64
file: docker/Dockerfile
push: true
tags: |
ghcr.io/jeertmans/manim-slides:latest
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}

View File

@ -1,88 +0,0 @@
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
name: Tests
jobs:
pytest:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.9', '3.10', '3.11', '3.12']
runs-on: ${{ matrix.os }}
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: error
PYTHONFAULTHANDLER: 1
DISPLAY: :99
GITHUB_WORKFLOWS: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.pyversion }}
cache: true
# Path related stuff
- name: Append to Path on MacOS
if: matrix.os == 'macos-latest'
run: |
echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
- name: Append to Path on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
- name: Append to Path on Windows
if: matrix.os == 'windows-latest'
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
# OS depedencies
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
run: brew install ffmpeg py3cairo
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
sudo apt-get install xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
- name: Install Mesa
if: matrix.os == 'windows-latest'
uses: ssciwr/setup-mesa-dist-win@v2
- name: Install Manim Slides
run: |
pdm sync -Ggithub-action -Gtest
- name: Run pytest
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
run: pdm run pytest
- name: Run pytest and coverage
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
run: pdm run pytest --cov-report xml --cov=manim_slides tests/
- name: Upload to codecov.io
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
fail_ci_if_error: true

50
.gitignore vendored
View File

@ -1,50 +0,0 @@
# Python files
__pycache__/
/env
/build
/dist
*.egg-info/
.pdm-python
# Manim files
images/
/media
tests/data/media/
docs/source/media/
# ManimGL files
videos/
# Manim Slides files
.manim-slides.toml
slides/
!tests/data/slides/
slides_assets/
# Docs
docs/build/
slides.html
docs/source/reference/.ipynb_checkpoints/
docs/source/_static/basic_example_assets/
docs/source/_static/basic_example.html
docs/source/_static/three_d_example.html
docs/source/_static/three_d_example_assets/
docs/source/reference/media/
# JOSE Paper
paper/paper.pdf
paper/media/
# Others
coverage.xml
rendering_times.csv

View File

@ -1,31 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.12.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
- id: pretty-format-toml
exclude: poetry.lock
args: [--autofix, --trailing-commas]
- repo: https://github.com/keewis/blackdoc
rev: v0.3.9
hooks:
- id: blackdoc
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.14
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

View File

@ -1,21 +0,0 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: '3.10'
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_install:
- ipython kernel install --name "manim-slides" --user
sphinx:
configuration: docs/source/conf.py
python:
install:
- method: pip
path: .
extra_requirements:
- docs
- magic
- sphinx-directive

View File

@ -1,185 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.0...HEAD)
(v5.1)=
## [v5.1](https://github.com/jeertmans/manim-slides/compare/v5.0.0...v5.1.0)
(v5.1-added)=
### Added
- Added the `--hide-info-window` option to `manim-slides present`.
[#313](https://github.com/jeertmans/manim-slides/pull/313)
- Added the `manim-slides render` command
to render slides using correct Manim installation.
[#317](https://github.com/jeertmans/manim-slides/pull/317)
- Added the `playback-rate` and `reversed-playback-rate` options
to slide config.
[#320](https://github.com/jeertmans/manim-slides/pull/320)
- Added the speaker notes option.
[#322](https://github.com/jeertmans/manim-slides/pull/322)
- Added `auto` option for conversion format, which is the default.
This is somewhat a **breaking change**, but changes to the CLI
API are not considered to be very important.
[#325](https://github.com/jeertmans/manim-slides/pull/325)
- Added `return_animation` option to slide animations `self.wipe`
and `self.zoom`.
[#331](https://github.com/jeertmans/manim-slides/pull/331)
- Created a Docker image, published on GitHub.
[#355](https://github.com/jeertmans/manim-slides/pull/355)
- Added `:template:` and `:config_options` options to
the Sphinx directive.
[#357](https://github.com/jeertmans/manim-slides/pull/357)
(v5.1-modified)=
### Modified
- Modified the internal logic to simplify adding configuration options.
[#321](https://github.com/jeertmans/manim-slides/pull/321)
- Remove `reversed` file assets when exporting to HTML, as it was not used.
[#336](https://github.com/jeertmans/manim-slides/pull/336)
(v5.1-chore)=
### Chore
- Removed subrocess calls to FFMPEG with direct `libav` bindings using
the `av` Python module. This should enhance rendering speed and security.
[#335](https://github.com/jeertmans/manim-slides/pull/335)
- Changed build backend to PDM and reflected on docs.
[#354](https://github.com/jeertmans/manim-slides/pull/354)
- Dropped Python 3.8 support.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Made Qt backend optional and support PyQt6 too.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Documentated how to create and use a custom HTML template.
[#357](https://github.com/jeertmans/manim-slides/pull/357)
## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0)
Prior to v5, there was no real CHANGELOG other than the GitHub releases,
with most of the content automatically generated by GitHub from merged
pull requests.
In an effort to better document changes, this CHANGELOG document is now created.
(v5-added)=
### Added
- Added the following option aliases to `manim-slides present`:
`-F` and `--full-screen` for `fullscreen`,
`-H` for `--hide-mouse`,
and `-S` for `--screen-number`.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Added a full screen key binding (defaults to <kbd>F</kbd>) in the
presenter.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Added support for including code from a file in Manim Slides
Sphinx directive.
[#261](https://github.com/jeertmans/manim-slides/pull/261)
- Added the `manim_slides.slide.animation` module and created the
`Wipe` and `Zoom` classes, that return a new animation.
[#285](https://github.com/jeertmans/manim-slides/pull/285)
- Added two environ variables, `MANIM_API` and `FORCE_MANIM_API`,
to specify the `MANIM_API` to be used: `manim` and `manimce` will
import `manim`, while `manimgl` and `manimlib` will import `manimlib`.
If one of the two APIs is already imported, use `FORCE_MANIM_API=1` to
override this.
[#285](https://github.com/jeertmans/manim-slides/pull/285)
- Added a working `ThreeDSlide` class compatible with `manimlib`.
[#285](https://github.com/jeertmans/manim-slides/pull/285)
- Added `loop` option to `Slide`'s `next_slide` method.
Calling `next_slide` will never fail anymore.
[#294](https://github.com/jeertmans/manim-slides/pull/294)
- Added `Slide.next_section` for compatibility with `manim`'s
`Scene.next_section` method.
[#295](https://github.com/jeertmans/manim-slides/pull/295)
- Added `--next-terminates-loop` option to `manim-slides present` for turn a
looping slide into a normal one, so that it ends nicely. This is useful to
have a smooth transition with the next slide.
[#299](https://github.com/jeertmans/manim-slides/pull/299)
- Added `--playback-rate` option to `manim-slides present` for testing purposes.
[#300](https://github.com/jeertmans/manim-slides/pull/300)
- Added `auto_next` option to `Slide`'s `next_slide` method to automatically
play the next slide upon terminating. Supported by `present` and
`convert --to=html` commands.
[#304](https://github.com/jeertmans/manim-slides/pull/304)
(v5-changed)=
### Changed
- Automatically concatenate all animations from a slide into one.
This is a **breaking change** because the config file format is
different from the previous one. For migration help, see associated PR.
[#242](https://github.com/jeertmans/manim-slides/pull/242)
- Changed the player interface to only use PySide6, and not a combination of
PySide6 and OpenCV. A few features have been removed (see removed section),
but the new player should be much easier to maintain and more performant,
than its predecessor.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Changed the slide config format to exclude unecessary information.
`StypeType` is removed in favor to one boolean `loop` field. This is
a **breaking change** and one should re-render the slides to apply changes.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Renamed key bindings in the config. This is a **breaking change** and one
should either manually rename them (see list below) or re-init a config.
List of changes: `CONTINUE` to `NEXT`, `BACK` to `PREVIOUS`, and
`REWIND` to `REPLAY`.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Conversion to HTML now uses Jinja2 templating. The template file has
been modified accordingly, and old templates will not work anymore.
This is a **breaking change**.
[#271](https://github.com/jeertmans/manim-slides/pull/271)
- Bumped RevealJS' default version to v4.6.1, and added three new themes.
[#272](https://github.com/jeertmans/manim-slides/pull/272)
- Changed the logger such that `make_logger` is called at module import,
and we do not use Manim's logger anymore.
[#285](https://github.com/jeertmans/manim-slides/pull/285)
- Changed `Slide.wipe` and `Slide.zoom` to automatically call `self.play`.
This is a **breaking change** as calling `self.play(self.wipe(...))` now
raises an error (because `None` is not an animation).
[#285](https://github.com/jeertmans/manim-slides/pull/285)
- Changed the `manim_slides.slide` module to contain submodules, i.e.,
`slide.manim`, `slide.manimlib`, `slide.animation`.
Only `slide.animation` is part of the public API.
Rules for choosing the Manim API (either `manim` or `manimlib`) has changed,
and defaults to the currently imported module, with a preference for `manim`.
[#285](https://github.com/jeertmans/manim-slides/pull/285)
(v5-fixed)=
### Fixed
- Patched enums in `manim_slides/convert.py` to correctly call `str`'s
`__str__` method, and not the `Enum` one.
This bug was discovered by
[@alexanderskulikov](https://github.com/alexanderskulikov) in
[#253](https://github.com/jeertmans/manim-slides/discussions/253), caused by
Python 3.11's change in how `Enum` work.
[#257](https://github.com/jeertmans/manim-slides/pull/257).
- Fixed potential non-existing parent path issue in
`manim convert`'s destination path.
[#262](https://github.com/jeertmans/manim-slides/pull/262)
(v5-removed)=
### Removed
- Removed `--start-at-animation-number` option from `manim-slides present`.
[#242](https://github.com/jeertmans/manim-slides/pull/242)
- Removed the following options from `manim-slides present`:
`--resolution`, `--record-to`, `--resize-mode`, and `--background-color`.
[#243](https://github.com/jeertmans/manim-slides/pull/243)
- Removed `PERF` verbosity level because not used anymore.
[#245](https://github.com/jeertmans/manim-slides/pull/245)
- Remove `Slide`'s method `start_loop` and `self.end_loop`
in favor to `self.next_slide(loop=True)`.
This is a **breaking change**.
[#294](https://github.com/jeertmans/manim-slides/pull/294)
<!-- end changelog -->

View File

@ -1,32 +0,0 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: Manim Slides
message: A Python package for presenting Manim content anywhere
type: software
authors:
- name: Jérome Eertmans
orcid: 'https://orcid.org/0000-0002-5579-5360'
website: 'https://eertmans.be'
identifiers:
- type: doi
value: 10.21105/jose.00206
description: The paper presenting the software.
repository-code: 'https://github.com/jeertmans/manim-slides'
url: 'https://eertmans.be/manim-slides'
abstract: >-
Manim Slides is a Python package that makes presenting
Manim animations straightforward. With minimal changes
required to pre-existing code, one can slide through
animations in a PowerPoint-like manner, or share its
slides online using ReavealJS power.
keywords:
- Education
- Math Animations
- Presentation Tool
- PowerPoint
- Python
license: MIT
version: v5.1.0

1
CNAME Normal file
View File

@ -0,0 +1 @@
manim-slides.readthedocs.io

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022-2024 Jérome Eertmans
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

239
README.md
View File

@ -1,237 +1,4 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
</picture>
# GitHub Pages branch
[![Latest Release][pypi-version-badge]][pypi-version-url]
[![Python version][pypi-python-version-badge]][pypi-version-url]
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
[![Documentation][documentation-badge]][documentation-url]
[![DOI][doi-badge]][doi-url]
[![JOSE Paper][jose-badge]][jose-url]
[![codecov][codecov-badge]][codecov-url]
[![Binder][binder-badge]][binder-url]
# Manim Slides
Tool for live presentations using either
[Manim (community edition)](https://www.manim.community/)
or [ManimGL](https://3b1b.github.io/manim/).
Manim Slides will *automatically* detect the one you are using!
> [!NOTE]
> This project extends the work of
> [`manim-presentation`](https://github.com/galatolofederico/manim-presentation),
> with a lot more features!
- [Installation](#installation)
- [Usage](#usage)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [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
Manim Slides requires either Manim or ManimGL to be installed, along
with their dependencies. Please checkout the
[documentation](https://eertmans.be/manim-slides/installation.html)
for detailed install instructions.
## Usage
<!-- start usage -->
Using Manim Slides is a two-step process:
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead
of `Scene` (resp. `ThreeDScene`), and add calls to `self.next_slide()`
everytime you want to create a new slide.
2. Run `manim-slides` on rendered animations and display them like a
*PowerPoint* presentation.
The documentation is available [online](https://eertmans.be/manim-slides/).
### Basic Example
Call `self.next_slide()` everytime you want to create a pause between
animations, and `self.next_slide(loop=True)` if you want the next slide to loop
over animations until the user presses continue:
```python
from manim import * # or: from manimlib import *
from manim_slides import Slide
class BasicExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.next_slide() # Waits user to press continue to go to the next slide
self.next_slide(loop=True) # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.next_slide() # This will start a new non-looping slide
self.play(dot.animate.move_to(ORIGIN))
```
First, render the animation files:
```bash
manim-slides render example.py BasicExample
# or use ManimGL
manim-slides render --GL example.py BasicExample
```
<!-- end usage -->
> [!NOTE]
> Using `manim-slides render` makes sure to use the `manim`
> (or `manimlib`) library that was installed in the same Python environment.
> Put simply, this is a wrapper around
> `manim render [ARGS]...` (or `manimgl [ARGS]...`).
<!-- start more-usage -->
To start the presentation using `Scene1`, `Scene2` and so on, run:
```bash
manim-slides [OPTIONS] Scene1 Scene2...
```
In our example:
```bash
manim-slides BasicExample
```
<!-- end more-usage -->
<p align="center">
<img alt="Example GIF" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif">
</p>
For detailed usage documentation, run `manim-slides --help`, or go to the
[documentation](https://eertmans.be/manim-slides/reference/cli.html).
## Interactive Tutorial
Click on the image to watch a slides presentation that explains to you how
to use Manim Slides.
[![Manim Slides Docs](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/docs.png)](https://eertmans.be/manim-slides/)
## More Examples
More examples are available in the
[`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py)
file, if you downloaded the git repository.
## Comparison with Similar Tools
There exists a variety of tools that allows to create slides presentations
containing Manim animations.
Below is a comparison of the most used ones with Manim Slides:
| Project name | Manim Slides | Manim Presentation | Manim Editor | Jupyter Notebooks |
|:------------:|:------------:|:------------------:|:------------:|:-----------------:|
| Link | [![GitHub Repo stars](https://img.shields.io/github/stars/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/stars/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/stars/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/stars/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Activity | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Usage | Command-line | Command-line | Web Browser | Notebook |
| Note | Requires minimal modif. in scenes files | Requires minimal modif. in scenes files | Requires the usage of sections, and configuration through graphical interface | Relies on `nbconvert` to create slides from a Notebook |
| Support for ManimGL | Yes | No | No | No |
| Web Browser presentations | Yes | No | Yes | No |
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
## F.A.Q
### How to increase quality on Windows
On Windows platform, one may encounter a lower image resolution than expected.
Usually, this is observed because Windows rescales every application to
fit the screen.
As found by [@arashash](https://github.com/arashash),
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
the problem can be addressed by changing the scaling factor to 100%:
<p align="center">
<img alt="Windows Fix Scaling" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png">
</p>
in *Settings*->*Display*.
## Contributing
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
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
[documentation-badge]: https://img.shields.io/website?down_color=lightgrey&down_message=offline&label=documentation&up_color=green&up_message=online&url=https%3A%2F%2Feertmans.be%2Fmanim-slides%2F
[documentation-url]: https://eertmans.be/manim-slides/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
[jose-url]: https://doi.org/10.21105/jose.00206
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
[codecov-url]: https://codecov.io/gh/jeertmans/manim-slides
[binder-badge]: https://mybinder.org/badge_logo.svg
[binder-url]: https://mybinder.org/v2/gh/jeertmans/manim-slides-binder/HEAD?filepath=getting_started.ipynb
This branch is kept to keep the https://jeertmans.github.io/manim-slides
link alive, and we redirect to Read The Docs.

View File

@ -1,2 +0,0 @@
style:
background_color: '#000000'

View File

@ -1,55 +0,0 @@
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
FROM python:3.11-slim
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y \
ffmpeg \
build-essential \
gcc \
cmake \
libcairo2-dev \
libffi-dev \
libpango1.0-dev \
freeglut3-dev \
pkg-config \
make \
wget \
ghostscript
# setup a minimal texlive installation
COPY docker/texlive-profile.txt /tmp/
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/usr/local/texlive/bin/x86_64-linux:$PATH
RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz && \
mkdir /tmp/install-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 \
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
mathastext microtype ms physics 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]
ARG NB_USER=manimslidesuser
ARG NB_UID=1000
ENV USER ${NB_USER}
ENV NB_UID ${NB_UID}
ENV HOME /manim-slides
RUN adduser --disabled-password \
--gecos "Default user" \
--uid ${NB_UID} \
${NB_USER}
# create working directory for user to mount local directory into
WORKDIR ${HOME}
USER root
RUN chown -R ${NB_USER}:${NB_USER} ${HOME}
RUN chmod 777 ${HOME}
USER ${NB_USER}
CMD [ "/bin/bash" ]

View File

@ -1,15 +0,0 @@
# Docker Image
Manim Slides Docker image, highly inspired from the Manim Community Docker image.
Building the image can be done with:
```bash
docker build -t manim-slide/manin-slide:TAG -f docker/Dockerfile .
```
from the root directory of the repository.
> [!WARNING]
> If you run the command above from another place,
> Docker will not be able to find expected files.

View File

@ -1,10 +0,0 @@
selected_scheme scheme-minimal
TEXDIR /usr/local/texlive
TEXMFCONFIG ~/.texlive/texmf-config
TEXMFHOME ~/texmf
TEXMFLOCAL /usr/local/texlive/texmf-local
TEXMFSYSCONFIG /usr/local/texlive/texmf-config
TEXMFSYSVAR /usr/local/texlive/texmf-var
TEXMFVAR ~/.texlive/texmf-var
option_doc 0
option_src 0

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -1 +0,0 @@
../../../static/logo.png

View File

@ -1 +0,0 @@
../../../static/logo_dark_docs.png

View File

@ -1 +0,0 @@
../../../static/logo_dark_github.png

View File

@ -1 +0,0 @@
../../../static/logo_dark_transparent.png

View File

@ -1 +0,0 @@
../../../static/logo_light_transparent.png

View File

@ -1,101 +0,0 @@
<!doctype html>
<html>
<head>
<!-- Head stuff -->
</head>
<body>
<!-- Slides stuff -->
<script>
<!-- RevealJS stuff -->
</script>
<!-- Add a clock to each section dynamically using JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
var revealContainer = document.querySelector('.reveal');
// Append dynamic content to each section
var sections = revealContainer.querySelectorAll('.slides > section');
sections.forEach(function (section) {
// Create a new clock container
var clockContainer = document.createElement('div');
clockContainer.className = 'clock';
// Append the new clock container to the section
section.appendChild(clockContainer);
});
// Function to update the clock content
function updateClock() {
var now = new Date();
var hours = now.getHours();
var minutes = now.getMinutes();
var seconds = now.getSeconds();
// Format the time as HH:MM:SS
var timeString = pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
// Update the content of all clock containers
var clockContainers = document.querySelectorAll('.clock');
clockContainers.forEach(function (container) {
container.innerText = timeString;
});
}
// Function to pad zero for single-digit numbers
function pad(number) {
return String(number).padStart(2, "0");
}
// Update the clock every second
setInterval(updateClock, 1000);
// Register a reveal.js event to update the clock on each slide change
Reveal.addEventListener('slidechanged', function (event) {
updateClock();
});
// Initial update
updateClock();
});
</script>
<!-- define the style of the clock -->
<style>
.clock {
position: fixed;
bottom: 10px;
left: 10px;
font-size: 24px;
font-family: "Arial", sans-serif;
color: #333;
}
/* control the relative position of the clock to the slides */
.reveal .slides > section.present, .reveal .slides > section > section.present {
min-height: 100% !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
position: absolute !important;
top: 0 !important;
}
section > h1 {
position: absolute !important;
top: 0 !important;
margin-left: auto !important;
margin-right: auto !important;
left: 0 !important;
right: 0 !important;
}
.print-pdf .reveal .slides > section.present, .print-pdf .reveal .slides > section > section.present {
min-height: 770px !important;
position: relative !important;
}
</style>
</body>
</html>

View File

@ -1,429 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{{ title }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/theme/{{ reveal_theme }}.min.css">
<!-- Theme used for syntax highlighting of code -->
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
<!-- <link rel="stylesheet" href="index.css"> -->
</head>
<body>
<div class="reveal">
<div class="slides">
{%- 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 %}
{%- 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>
{%- endfor -%}
{%- endfor -%}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}
<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealMarkdown, RevealNotes ],
{%- endif %}
// The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different
// resolutions. Can be specified using percentage units.
width: {{ width }},
height: {{ height }},
// Factor of the display size that should remain empty around
// the content
margin: {{ margin }},
// Bounds for smallest/largest possible scale to apply to content
minScale: {{ min_scale }},
maxScale: {{ max_scale }},
// Display presentation control arrows
controls: {{ controls }},
// Help the user learn the controls by providing hints, for example by
// bouncing the down arrow when they first encounter a vertical slide
controlsTutorial: {{ controls_tutorial }},
// Determines where controls appear, "edges" or "bottom-right"
controlsLayout: {{ controls_layout }},
// Visibility rule for backwards navigation arrows; "faded", "hidden"
// or "visible"
controlsBackArrows: {{ controls_back_arrows }},
// Display a presentation progress bar
progress: {{ progress }},
// Display the page number of the current slide
// - true: Show slide number
// - false: Hide slide number
//
// Can optionally be set as a string that specifies the number formatting:
// - "h.v": Horizontal . vertical slide number (default)
// - "h/v": Horizontal / vertical slide number
// - "c": Flattened slide number
// - "c/t": Flattened slide number / total slides
//
// Alternatively, you can provide a function that returns the slide
// number for the current slide. The function should take in a slide
// object and return an array with one string [slideNumber] or
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
slideNumber: {{ slide_number }},
// Can be used to limit the contexts in which the slide number appears
// - "all": Always show the slide number
// - "print": Only when printing to PDF
// - "speaker": Only in the speaker view
showSlideNumber: {{ show_slide_number }},
// Use 1 based indexing for # links to match slide number (default is zero
// based)
hashOneBasedIndex: {{ hash_one_based_index }},
// Add the current slide number to the URL hash so that reloading the
// page/copying the URL will return you to the same slide
hash: {{ hash }},
// Flags if we should monitor the hash and change slides accordingly
respondToHashChanges: {{ respond_to_hash_changes }},
// Push each slide change to the browser history. Implies `hash: true`
history: {{ history }},
// Enable keyboard shortcuts for navigation
keyboard: {{ keyboard }},
// Optional function that blocks keyboard events when retuning false
//
// If you set this to 'focused', we will only capture keyboard events
// for embedded decks when they are in focus
keyboardCondition: {{ keyboard_condition }},
// Disables the default reveal.js slide layout (scaling and centering)
// so that you can use custom CSS layout
disableLayout: {{ disable_layout }},
// Enable the slide overview mode
overview: {{ overview }},
// Vertical centering of slides
center: {{ center }},
// Enables touch navigation on devices with touch input
touch: {{ touch }},
// Loop the presentation
loop: {{ loop }},
// Change the presentation direction to be RTL
rtl: {{ rtl }},
// Changes the behavior of our navigation directions.
//
// "default"
// Left/right arrow keys step between horizontal slides, up/down
// arrow keys step between vertical slides. Space key steps through
// all slides (both horizontal and vertical).
//
// "linear"
// Removes the up/down arrows. Left/right arrows step through all
// slides (both horizontal and vertical).
//
// "grid"
// When this is enabled, stepping left/right from a vertical stack
// to an adjacent vertical stack will land you at the same vertical
// index.
//
// Consider a deck with six slides ordered in two vertical stacks:
// 1.1 2.1
// 1.2 2.2
// 1.3 2.3
//
// If you're on slide 1.3 and navigate right, you will normally move
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
// from 1.3 -> 2.3.
navigationMode: {{ navigation_mode }},
// Randomizes the order of slides each time the presentation loads
shuffle: {{ shuffle }},
// Turns fragments on and off globally
fragments: {{ fragments }},
// Flags whether to include the current fragment in the URL,
// so that reloading brings you to the same fragment position
fragmentInURL: {{ fragment_in_url }},
// Flags if the presentation is running in an embedded mode,
// i.e. contained within a limited portion of the screen
embedded: {{ embedded }},
// Flags if we should show a help overlay when the question-mark
// key is pressed
help: {{ help }},
// Flags if it should be possible to pause the presentation (blackout)
pause: {{ pause }},
// Flags if speaker notes should be visible to all viewers
showNotes: {{ show_notes }},
// Global override for autolaying embedded media (video/audio/iframe)
// - null: Media will only autoplay if data-autoplay is present
// - true: All media will autoplay, regardless of individual setting
// - false: No media will autoplay, regardless of individual setting
autoPlayMedia: {{ auto_play_media }},
// Global override for preloading lazy-loaded iframes
// - null: Iframes with data-src AND data-preload will be loaded when within
// the viewDistance, iframes with only data-src will be loaded when visible
// - true: All iframes with data-src will be loaded when within the viewDistance
// - false: All iframes with data-src will be loaded only when visible
preloadIframes: {{ preload_iframes }},
// Can be used to globally disable auto-animation
autoAnimate: {{ auto_animate }},
// Optionally provide a custom element matcher that will be
// used to dictate which elements we can animate between.
autoAnimateMatcher: {{ auto_animate_matcher }},
// Default settings for our auto-animate transitions, can be
// overridden per-slide or per-element via data arguments
autoAnimateEasing: {{ auto_animate_easing }},
autoAnimateDuration: {{ auto_animate_duration }},
autoAnimateUnmatched: {{ auto_animate_unmatched }},
// CSS properties that can be auto-animated. Position & scale
// is matched separately so there's no need to include styles
// like top/right/bottom/left, width/height or margin.
autoAnimateStyles: {{ auto_animate_styles }},
// Controls automatic progression to the next slide
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
// is present on the current slide or fragment
// - 1+: All slides will progress automatically at the given interval
// - false: No auto-sliding, even if data-autoslide is present
autoSlide: {{ auto_slide }},
// Stop auto-sliding after user input
autoSlideStoppable: {{ auto_slide_stoppable }},
// Use this method for navigation when auto-sliding (defaults to navigateNext)
autoSlideMethod: {{ auto_slide_method }},
// Specify the average time in seconds that you think you will spend
// presenting each slide. This is used to show a pacing timer in the
// speaker view
defaultTiming: {{ default_timing }},
// Enable slide navigation via mouse wheel
mouseWheel: {{ mouse_wheel }},
// Opens links in an iframe preview overlay
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
// individually
previewLinks: {{ preview_links }},
// Exposes the reveal.js API through window.postMessage
postMessage: {{ post_message }},
// Dispatches all reveal.js events to the parent window through postMessage
postMessageEvents: {{ post_message_events }},
// Focuses body when page changes visibility to ensure keyboard shortcuts work
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
// Transition style
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
// Transition speed
transitionSpeed: {{ transition_speed }}, // default/fast/slow
// Transition style for full page slide backgrounds
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
// The maximum number of pages a single slide can expand onto when printing
// to PDF, unlimited by default
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
// Prints each fragment on a separate slide
pdfSeparateFragments: {{ pdf_separate_fragments }},
// Offset used to reduce the height of content within exported PDF pages.
// This exists to account for environment differences based on how you
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
// on precisely the total height of the document whereas in-browser
// printing has to end one pixel before.
pdfPageHeightOffset: {{ pdf_page_height_offset }},
// Number of slides away from the current that are visible
viewDistance: {{ view_distance }},
// Number of slides away from the current that are visible on mobile
// devices. It is advisable to set this to a lower number than
// viewDistance in order to save resources.
mobileViewDistance: {{ mobile_view_distance }},
// The display mode that will be used to show slides
display: {{ display }},
// Hide cursor if inactive
hideInactiveCursor: {{ hide_inactive_cursor }},
// Time before the cursor is hidden (in ms)
hideCursorTime: {{ hide_cursor_time }}
});
{% if data_uri %}
// Fix found by @t-fritsch on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
function fixBase64VideoBackground(event) {
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
if (event.currentSlide.getAttribute('data-background-video')) {
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
video = background.querySelector('video'),
sources = video.querySelectorAll('source');
sources.forEach((source, i) => {
const src = source.getAttribute('src');
if(src.match(/^data:video.*;base64$/)) {
const nextSrc = sources[i+1]?.getAttribute('src');
video.setAttribute('src', `${src},${nextSrc}`);
}
});
}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{% endif %}
</script>
<!-- Add a clock to each section dynamically using JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
var revealContainer = document.querySelector('.reveal');
// Append dynamic content to each section
var sections = revealContainer.querySelectorAll('.slides > section');
sections.forEach(function (section) {
// Create a new clock container
var clockContainer = document.createElement('div');
clockContainer.className = 'clock';
// Append the new clock container to the section
section.appendChild(clockContainer);
});
// Function to update the clock content
function updateClock() {
var now = new Date();
var hours = now.getHours();
var minutes = now.getMinutes();
var seconds = now.getSeconds();
// Format the time as HH:MM:SS
var timeString = pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
// Update the content of all clock containers
var clockContainers = document.querySelectorAll('.clock');
clockContainers.forEach(function (container) {
container.innerText = timeString;
});
}
// Function to pad zero for single-digit numbers
function pad(number) {
return String(number).padStart(2, "0");
}
// Update the clock every second
setInterval(updateClock, 1000);
// Register a reveal.js event to update the clock on each slide change
Reveal.addEventListener('slidechanged', function (event) {
updateClock();
});
// Initial update
updateClock();
});
</script>
<!-- define the style of the clock -->
<style>
.clock {
position: fixed;
bottom: 10px;
left: 10px;
font-size: 24px;
font-family: "Arial", sans-serif;
color: #333;
}
/* control the relative position of the clock to the slides */
.reveal .slides > section.present, .reveal .slides > section > section.present {
min-height: 100% !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
position: absolute !important;
top: 0 !important;
}
section > h1 {
position: absolute !important;
top: 0 !important;
margin-left: auto !important;
margin-right: auto !important;
left: 0 !important;
right: 0 !important;
}
.print-pdf .reveal .slides > section.present, .print-pdf .reveal .slides > section > section.present {
min-height: 770px !important;
position: relative !important;
}
</style>
</body>
</html>

View File

@ -1 +0,0 @@
../../../static/wizard_dark.png

View File

@ -1 +0,0 @@
../../../static/wizard_light.png

View File

@ -1,6 +0,0 @@
# Changelog
```{include} ../../CHANGELOG.md
:start-after: <!-- start changelog -->
:end-before: <!-- end changelog -->
```

View File

@ -1,91 +0,0 @@
# type: ignore
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import sys
from datetime import date
from manim_slides import __version__
assert sys.version_info >= (3, 10), "Building docs requires Python 3.10"
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Manim Slides"
copyright = f"2024-{date.today().year}, Jérome Eertmans"
author = "Jérome Eertmans"
version = __version__
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
# Built-in
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
# Additional
"nbsphinx",
"myst_parser",
"sphinxext.opengraph",
"sphinx_click",
"sphinx_copybutton",
# Custom
"manim_slides.docs.manim_slides_directive",
]
autodoc_typehints = "both"
myst_enable_extensions = [
"colon_fence",
"html_admonition",
]
templates_path = ["_templates"]
exclude_patterns = []
# Removes the 'package.module' part from package.module.Class
add_module_names = False
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_static_path = ["_static"]
html_theme_options = {
"light_logo": "logo_light_transparent.png",
"dark_logo": "logo_dark_transparent.png",
"footer_icons": [
{
"name": "GitHub",
"url": "https://github.com/jeertmans/manim-slides",
"html": """
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
""",
"class": "",
},
],
"source_repository": "https://github.com/jeertmans/manim-slides/",
"source_branch": "main",
"source_directory": "docs/source/",
}
# -- Intersphinx mapping
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"manim": ("https://docs.manim.community/en/stable/", None),
"manimlib": ("https://3b1b.github.io/manim/", None),
"numpy": ("https://numpy.org/doc/stable/", None),
}
# -- OpenGraph settings
ogp_site_url = "https://eertmans.be/manim-slides/"
ogp_use_first_image = True

View File

@ -1,46 +0,0 @@
# Contributing
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!
```{toctree}
:hidden:
workflow
internals
```
[Workflow](./workflow)
: how to work on this project. Start here if you're a new contributor.
[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

@ -1,11 +0,0 @@
# Internals
Manim-Slides' work in split in two steps: first, when rendering animation, and, second, when converting multiple animations into one slides presentation.
## Rendering
To render animations, Manim Slides simply uses Manim or ManimGL, and creates some additional output files that it needs for the presentation.
## Slides presentation
Manim Slides searches for the local artifacts it generated previously, and concatenates them into one presentation. For the graphical interface, it uses `PySide6`.

View File

@ -1,129 +0,0 @@
# Workflow
This document is there to help you recreate a working environment for Manim Slides.
## Dependencies
```{include} ../installation.md
:start-after: <!-- start deps -->
:end-before: <!-- end deps -->
```
## Forking the repository and cloning it locally
We use GitHub to host Manim Slides' repository, and we encourage contributors to use git.
Useful links:
* [GitHub's Hello World](https://docs.github.com/en/get-started/quickstart/hello-world).
* [GitHub Pull Request in 100 Seconds](https://www.youtube.com/watch?v=8lGpZkjnkt4&ab_channel=Fireship).
Once you feel comfortable with git and GitHub, [fork](https://github.com/jeertmans/manim-slides/fork) the repository, and clone it locally.
As for every Python project, using virtual environment is recommended to avoid
conflicts between modules.
For this project, we use [PDM](https://pdm-project.org/) to easily manage project
and development dependencies. If not already, please install this tool.
## Installing Python modules
With PDM, installation becomes straightforward:
```bash
pdm 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 `-G|--group` option:
```bash
pdm install -Gmanim # For Manim
# or
pdm install -Gmanimgl # For ManimGL
```
Additionnally, Manim Slides comes with groups of dependencies for development purposes:
```bash
pdm install -Gdev # For linters and formatters
# or
pdm install -Gdocs # To build the documentation locally
# or
pdm install -Gtest # To run tests
```
:::{note}
You can combine any number of groups or extras when installing the package locally.
You can also install everything with `pdm install -G:all`.
:::
## Running commands
Because modules are installed in a new Python environment,
you cannot use them directly in the shell.
Instead, you either need to prepend `pdm run` to any command, e.g.:
```bash
pdm run manim-slides wizard
```
or [enter a new shell](https://pdm-project.org/latest/usage/venv/#activate-a-virtualenv)
that uses this new Python environment:
```bash
eval $(pdm venv activate) # Click on the link above to see shell-specific command
manim-slides wizard
```
## Testing your code
Most of the tests are done with GitHub actions, thus not on your computer.
The only command you should run locally is:
```bash
pdm run pre-commit run --all-files
```
This runs a few linter and formatter to make sure the code quality and style stay
constant across time.
If a warning or an error is displayed, please fix it before going to next step.
For testing your code, simply run:
```bash
pdm run pytest
```
## Building the documentation
The documentation is generated using Sphinx, based on the content
in `docs/source` and in the `manim_slides` Python package.
To generate the documentation, run the following:
```bash
cd docs
pdm run make html
```
Then, the output index file is located at `docs/build/html/index.html` and
can be opened with any modern browser.
:::{warning}
Building the documentation can take quite some time, especially
the first time as it needs to render all the animations.
Further builds should run faster.
:::
## Proposing changes
Once you feel ready and think your contribution is ready to be reviewed,
create a [pull request](https://github.com/jeertmans/manim-slides/pulls)
and wait for a reviewer to check your work!
Many thanks to you!

View File

@ -1,2 +0,0 @@
[restructuredtext parser]
syntax_highlight = short

View File

@ -1,29 +0,0 @@
# Features Table
The following summarizes the different presentation features Manim Slides offers.
:::{table} Comparison of the different presentation methods.
:widths: auto
:align: center
| Feature / Constraint | [`present`](reference/cli.md) | [`convert --to=html`](reference/cli.md) | [`convert --to=pptx`](reference/cli.md) | [`convert --to=pdf`](reference/cli.md)
| :--- | :---: | :---: | :---: | :---: |
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
| Replay slide | Yes | No | No | N/A |
| Pause animation | Yes | No | No | N/A |
| Play slide in reverse | Yes | No | No | N/A |
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
| Needs Python with Manim Slides installed | Yes | No | No | No
| Requires internet access | No | Yes | No | No |
| Auto. play slides | Yes | Yes | Yes | N/A |
| Loops support | Yes | Yes | Yes | N/A |
| Fully customizable | No | Yes (`--use-template` option) | No | No |
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1] | None |
| Works cross-platforms | Yes | Yes | Partly[^1][^2] | Yes |
:::
[^1]: If you encounter a problem where slides do not automatically play or loops do not work,
please
[file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).
[^2]: PowerPoint online does not seem to support automatic playing of videos,
so you need LibreOffice Impress on Linux platforms.

View File

@ -1,55 +0,0 @@
---
hide-toc: true
og:description: Manim Slides makes creating slides with Manim super easy!
---
```{eval-rst}
.. image:: _static/logo_light_transparent.png
:width: 600px
:align: center
:class: only-light
:alt: Manim Slide logo
```
```{eval-rst}
.. image:: _static/logo_dark_transparent.png
:width: 600px
:align: center
:class: only-dark
:alt: Manim Slide logo
```
# Welcome to Manim Slide's documentation
Manim Slides makes creating slides with Manim super easy!
In a [very few steps](./quickstart),
you can create slides and present them either using the GUI, or your browser.
Slide through the demo below to get a quick glimpse on what you can do with
Manim Slides.
```{eval-rst}
.. manim-slides:: ../../example.py:ConvertExample
:hide_source:
:quality: high
```
```{toctree}
:hidden:
quickstart
installation
reference/index
features_table
manim_or_manimgl
```
```{toctree}
:caption: Development
:hidden:
contributing/index
changelog
license
```

View File

@ -1,157 +0,0 @@
# Installation
While installing Manim Slides and its dependencies on your global Python is fine,
we recommend using a virtual environment
(e.g., [venv](https://docs.python.org/3/tutorial/venv.html)) for a local installation.
Therefore, the following documentation will install Manim Slides using
[pipx](https://pipx.pypa.io/). This tool is a drop-in replacement
for installing Python packages that ship with one or more executable.
The benefit of using pipx is that it will automatically create a new virtual
environment for every package you install.
:::{note}
Everytime you read `pipx install`, you can use `pip install` instead,
if you are working in a virtual environment or else.
:::
## Dependencies
<!-- start deps -->
Manim Slides requires either Manim or ManimGL to be installed, along
with their dependencies.
Having both packages installed is fine too.
If none of those packages are installed,
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
The recommended way to install the latest release
with all features is to use pipx:
```bash
pipx install -U "manim-slides[pyside6-full]"
```
:::{tip}
While not necessary, the `-U` indicates that we would
like to upgrade to the latest version available,
if Manim Slides is already installed.
:::
:::{note}
The quotes `"` are added because not all shell support unquoted
brackets (e.g., zsh) or commas (e.g., Windows).
:::
You can check that Manim Slides was correctly installed with:
```bash
manim-slides --version
```
## Custom install
If you want more control on what dependencies are installed,
you can always install the bare minimal dependencies with:
```bash
pipx install -U manim-slides
```
And install additional dependencies later.
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
pipx install -U "manim-slides[manim]" # For Manim
# or
pipx install -U "manim-slides[manimgl]" # For ManimGL
```
For optional dependencies documentation, see
[next section](#optional-dependencies).
:::{warning}
If you are installing with pipx, this is mandatory to at least include
either `manim` or `manimgl`.
:::
[^1]: You still need to have Manim or ManimGL platform-specific dependencies
installed on your computer.
## Optional dependencies
Along with the optional dependencies for Manim and ManimGL,
Manim Slides offers additional *extras*, that can be activated
using optional dependencies:
- `full`, to include `magic`, `manim`, `manimgl`, and
`sphinx-directive` extras (see below);
- `magic`, to include a Jupyter magic to render
animations inside notebooks. This automatically installs `manim`,
and does not work with ManimGL;
- `manim` and `manimgl`, for installing the corresponding
dependencies;
- `pyqt6` to include PyQt6 Qt bindings. Those bindings are available
on most platforms and Python version, but produce a weird black
screen between slide with `manim-slides present`,
see [#QTBUG-118501](https://bugreports.qt.io/browse/QTBUG-118501);
- `pyqt6-full` to include `full` and `pyqt6`;
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
on most platforms and Python version, except on Python 3.12[^2];
- `pyside6-full` to include `full` and `pyside6`;
- `sphinx-directive`, to generate presentation inside your Sphinx
documentation. This automatically installs `manim`,
and does not work with ManimGL.
Installing those extras can be done with the following syntax:
```bash
pipx install -U "manim-slides[extra1,extra2]"
```
[^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
observe the same visual bug as with PyQt6.
## When you need a Qt backend
Before `v5.1`, Manim Slides automatically included PySide6 as
a Qt backend. As only `manim-slides present` and `manim-slides wizard`
command need a graphical library, and installing PySide6 on all platforms
and Python version can be sometimes complicated, Manim Slides chooses
**not to include** any Qt backend.
The use can choose between PySide6 (best) and PyQt6, depending on their
availability and licensing rules.
As of `v5.1`, you **need** to have Qt bindings installed to use
`manim-slides present` or `manim-slides wizard`. The recommended way to
install those are via optional dependencies, as explained above.
## Install from source
An alternative way to install Manim Slides is to clone the git repository,
and build the package from source. Read the
[contributing guide](./contributing/workflow)
to know how to process.

View File

@ -1,5 +0,0 @@
# License
```{include} ../../LICENSE.md
```

View File

@ -1,71 +0,0 @@
# Manim or ManimGL
Manim Slides supports both Manim (Community Edition) and ManimGL (by 3b1b).
Because both modules have slightly different APIs, Manim Slides needs to know
which Manim API you are using, to import the correct module.
## Default Behavior
By default, Manim Slides looks at {py:data}`sys.modules` and chooses the first
Manim package that is already imported: `manim` for Manim,
`manimlib` for ManimGL. This works pretty well when rendering
the slides.
If both modules are present in {py:data}`sys.modules`, then Manim Slides will
prefer using `manim`.
### Usage
The simplest way to use Manim Slides with the correct Manim API is to:
1. first import the Manim API;
2. and, then, import `manim_slides`.
Example for `manim`:
```python
from manim import *
from manim_slides import Slide
```
Example for `manimlib`:
```python
from manimlib import *
from manim_slides import Slide
```
### Example of Default Import
The following code shows how Manim Slides detected that `manimlib`
was imported, so the {py:class}`Slide<manim_slides.slide.Slide>`
automatically subclasses the class from ManimGL, not Manim.
```python
from manimlib import Scene
from manim_slides import Slide
assert issubclass(Slide, Scene) # Slide subclasses Scene from ManimGL
from manim import Scene
assert not issubclass(Slide, Scene) # but not Scene from Manim
```
## Custom Manim API
If you want to override the default Manim API, you can set the `MANIM_API`
environment variable to:
- `manim` or `manimce` to import `manim`;
- `manimlib` or `manimgl` to import `manimlib`;
prior to importing `manim_slides`.
Note that Manim Slides will still first look at {py:data}`sys.modules` to check
if any of the two modules is already imported.
If you want to force Manim Slides to obey the `MANIM_API` environment variable,
you must also set `FORCE_MANIM_API=1`.

View File

@ -1,34 +0,0 @@
# Quickstart
If not already, install Manim Slides, along with either Manim or ManimGL,
see [installation](./installation).
## Creating your first slides
```{include} ../../README.md
:start-after: <!-- start usage -->
:end-before: <!-- end usage -->
```
:::{note}
Using `manim-slides render` makes sure to use the `manim`
(or `manimlib`) library that was installed in the same Python environment.
Put simply, this is a wrapper around
`manim render [ARGS]...` (or `manimgl [ARGS]...`).
:::
```{include} ../../README.md
:start-after: <!-- start more-usage -->
:end-before: <!-- end more-usage -->
```
The output slides should look this this:
```{eval-rst}
.. manim-slides:: ../../example.py:BasicExample
:hide_source:
:quality: high
```
For more advanced examples, see the [Examples](reference/examples) section.

View File

@ -1,40 +0,0 @@
# Application Programming Interface
Manim Slides' API is very limited: it simply consists of two classes, `Slide`
and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
Therefore, we only document here the methods we think the end-user will ever
use, not the methods used internally when rendering.
## Slide
```{eval-rst}
.. autoclass:: manim_slides.slide.Slide
:members:
add_to_canvas,
canvas,
canvas_mobjects,
mobjects_without_canvas,
next_section,
next_slide,
remove_from_canvas,
wait_time_between_slides,
wipe,
zoom,
```
## 3D Slide
```{eval-rst}
.. autoclass:: manim_slides.slide.ThreeDSlide
:members:
```
## Animations
```{eval-rst}
.. automodule:: manim_slides.slide.animation
:members:
Wipe,
Zoom,
```

View File

@ -1,10 +0,0 @@
# Command Line Interface
This page contains an exhaustive list of all the commands available with `manim-slides`.
```{eval-rst}
.. click:: manim_slides.__main__:cli
:prog: manim-slides
:nested: full
```

View File

@ -1,71 +0,0 @@
# Customize your RevealJS slides
One of the benefits of the `convert` command is the use of template files.
Currently, only the HTML export uses one. If not specified, the template
will be the one shipped with Manim Slides, see
[`manim_slides/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html).
Because you can actually use your own template with the `--use-template`
option, possibilities are infinite!
:::{warning}
Currently, the `PresentationConfig` class and its components
are not part of the public API. You can still use them, e.g.,
in the templates, but you may expect breaking changes between
releases.
Eventually, this will become part of the public API too,
and we will document its usage.
:::
## Adding a clock to each slide
In this example, we show how to add a self-updating clock
to the bottom left corner of every slide.
:::{note}
This example is inspired from
[@gsong-math's comment](https://github.com/jeertmans/manim-slides/issues/356#issuecomment-1902626943)
on Manim Slides' repository.
:::
### What to add
Whenever you want to create a template, it is best practice
to start from the default one (see link above).
Modifying it needs very basic HTML/JavaScript/CSS skills.
To add a clock, you can simply add the following to the
default template file:
```{eval-rst}
.. literalinclude:: ../_static/template.diff
:language: html
```
:::{tip}
Because we use RevealJS to generate HTML slides,
we recommend you to take a look at
[RevealJS' documentation](https://revealjs.com/).
:::
### How it renders
Then, using the `:template: <path/to/custom_template.html>`
option, the basic example renders as follows:
```{eval-rst}
.. manim-slides:: ../../../example.py:BasicExample
:hide_source:
:template: ../_static/template.html
```
### Full code
Below, you can read the full content of the template file.
```{eval-rst}
.. literalinclude:: ../_static/template.html
:language: html+jinja
```

View File

@ -1,172 +0,0 @@
# Examples
Contents of `example.py`.
Do not forget to import Manim Slides and Manim or ManimGL:
```python
from manim import *
from manim_slides import Slide, ThreeDSlide
```
or
```python
from manimlib import *
from manim_slides import Slide, ThreeDSlide
```
Then, each presentation, named `SCENE`, was generated with those two commands:
```bash
manim example.py SCENE # or manimgl example SCENE
manim-slides convert SCENE scene.html -ccontrols=true
```
where `-ccontrols=true` indicates that we want to display the blue navigation arrows.
## Basic Example
Basic example from quickstart.
```{eval-rst}
.. manim-slides:: ../../../example.py:BasicExample
:hide_source:
:quality: high
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:pyobject: BasicExample
```
## 3D Example
Example using 3D camera. As Manim and ManimGL handle 3D differently,
definitions are slightly different.
### With Manim
```{eval-rst}
.. manim-slides:: ../../../example.py:ThreeDExample
:hide_source:
:quality: high
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:dedent: 4
:start-after: [manim-3d]
:end-before: [manim-3d]
```
### With ManimGL
```{eval-rst}
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:dedent: 4
:start-after: [manimgl-3d]
:end-before: [manimgl-3d]
```
## Subclass Custom Scenes
For compatibility reasons, Manim Slides only provides subclasses for
`Scene` and `ThreeDScene`.
However, subclassing other scene classes is totally possible,
and very simple to do actually!
[For example](https://github.com/jeertmans/manim-slides/discussions/185),
you can subclass the `MovingCameraScene` class from `manim`
with the following code:
```{code-block} python
:linenos:
from manim import *
from manim_slides import Slide
class MovingCameraSlide(Slide, MovingCameraScene):
pass
```
And later use this class anywhere in your code:
```{code-block} python
:linenos:
class SubclassExample(MovingCameraSlide):
"""Example taken from ManimCE's docs."""
def construct(self):
self.camera.frame.save_state()
ax = Axes(x_range=[-1, 10], y_range=[-1, 10])
graph = ax.plot(lambda x: np.sin(x), color=WHITE, x_range=[0, 3 * PI])
dot_1 = Dot(ax.i2gp(graph.t_min, graph))
dot_2 = Dot(ax.i2gp(graph.t_max, graph))
self.add(ax, graph, dot_1, dot_2)
self.play(self.camera.frame.animate.scale(0.5).move_to(dot_1))
self.next_slide()
self.play(self.camera.frame.animate.move_to(dot_2))
self.next_slide()
self.play(Restore(self.camera.frame))
self.wait()
```
:::{note}
If you do not plan to reuse `MovingCameraSlide` more than once, then you can
directly write the `construct` method in the body of `MovingCameraSlide`.
:::
```{eval-rst}
.. manim-slides:: SubclassExample
:hide_source:
:quality: high
from manim import *
from manim_slides import Slide
class MovingCameraSlide(Slide, MovingCameraScene):
pass
class SubclassExample(MovingCameraSlide):
def construct(self):
self.camera.frame.save_state()
ax = Axes(x_range=[-1, 10], y_range=[-1, 10])
graph = ax.plot(lambda x: np.sin(x), color=WHITE, x_range=[0, 3 * PI])
dot_1 = Dot(ax.i2gp(graph.t_min, graph))
dot_2 = Dot(ax.i2gp(graph.t_max, graph))
self.add(ax, graph, dot_1, dot_2)
self.play(self.camera.frame.animate.scale(0.5).move_to(dot_1))
self.next_slide()
self.play(self.camera.frame.animate.move_to(dot_2))
self.next_slide()
self.play(Restore(self.camera.frame))
self.wait()
```
## Advanced Example
A more advanced example is `ConvertExample`, which is used as demo slide and tutorial.
```{eval-rst}
.. manim-slides:: ../../../example.py:ConvertExample
:hide_source:
:quality: high
.. literalinclude:: ../../../example.py
:language: python
:linenos:
:pyobject: ConvertExample
```

View File

@ -1,71 +0,0 @@
# Graphical User Interface
Manim Slides' graphical user interface (GUI) is the *de facto* way to present slides.
If you do not specify one of the commands listed in the [CLI reference](./cli),
Manim Slides will use **present** by default, which launches a GUI window,
playing your scene(s) like so:
```bash
manim-slides [present] [SCENES]...
```
Some optional parameters can be specified and can be listed with:
```bash
manim-slides present --help
```
:::{note}
All the `SCENES` must be in the same folder (`--folder DIRECTORY`), which
defaults to `./slides`. If you rendered your animations without changing
directory, you should not worry about that :-)
:::
## Configuration File
It is possible to configure Manim Slides via a configuration file, even though
this feature is currently limited. You may initiliaze the default configuration
file with:
```bash
manim-slides init
```
:::{warning}
Note that, by default, Manim Slides will use default key bindings that are
platform-dependent. If you decide to overwrite those with a config file, you may
encounter some problems from platform to platform.
:::
## Configuring Key Bindings
If you wish to use other key bindings than the defaults, you can run the
configuration wizard with:
```bash
manim-slides wizard
```
A similar window to the image below will pop up and prompt to change keys.
```{eval-rst}
.. image:: ../_static/wizard_light.png
:width: 300px
:align: center
:class: only-light
:alt: Manim Slide Wizard
```
```{eval-rst}
.. image:: ../_static/wizard_dark.png
:width: 300px
:align: center
:class: only-dark
:alt: Manim Slide Wizard
```
:::{note}
Even though it is not currently supported through the GUI, you can select
multiple key binding for the same action by modifying the config file.
:::

View File

@ -1,40 +0,0 @@
# HTML Presentations
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 with every command with Manim Slides, converting slides' fragments into one
HTML file (and its assets) can be done in one command:
```bash
manim-slides convert [SCENES]... DEST
```
where `DEST` is the `.html` destination file.
## Configuring the Template
Many configuration options are available through the `-c<option>=<value>` syntax.
Most, if not all, RevealJS options should be available by default. If that is
not the case, please
[fill an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
on GitHub.
You can print the list of available options with:
```bash
manim-slides convert --show-config
```
## Using a Custom Template
The default template used for HTML conversion can be found on
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/data/revealjs_template.html)
or printed with the `--show-template` option.
If you wish to use another template, you can do so with the
`--use-template FILE` option.
## More about HTML Slides
You can read more about HTML slides in the [sharing](./sharing) section.

View File

@ -1,38 +0,0 @@
# Reference Documentation
Automatically generated reference for Manim Slides.
```{toctree}
:hidden:
api
cli
customize_html
examples
gui
html
IPython magic <ipython_magic>
sharing
Sphinx Extension <sphinx_extension>
```
[Application Programming Interface](./api): list of classes and methods that may
be useful to the end-user.
[Command Line Interface](./cli): list of all commands available using Manim
Slides' executable.
[Examples](./examples): curated list of examples and their output.
[Graphical User Interface](./gui): details about the main Manim Slide' feature.
[HTML Presentation](./html): an alternative way of presenting your animations.
[IPython Magic](./ipython_magic): a magic to render and display Manim Slides inside notebooks.
+ [Example](./magic_example): example notebook using the magics.
[Sharing](./sharing): how to share your presentation with others.
[Sphinx Extension](./sphinx_extension): a Sphinx extension for diplaying Manim Slides animations within your documentation.

View File

@ -1,6 +0,0 @@
# Manim Slides' IPython magic
```{eval-rst}
.. automodule:: manim_slides.ipython.ipython_magic
:members: ManimSlidesMagic
```

View File

@ -1,99 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "6896875b-34ce-4fc5-809c-669c295067e7",
"metadata": {},
"source": [
"# Jupyter Magic Example\n",
"\n",
"This small example shows how to use the Manim Slides cell (`%%manim_slides`) and line (`%manim_slides`) magics:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a73f1c06-c7f8-4f19-a90e-e283bfb8c7c5",
"metadata": {},
"outputs": [],
"source": [
"from manim import *\n",
"from manim_slides import *"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68dda1a0-74ff-4d9e-9575-5b25a98f21e7",
"metadata": {},
"outputs": [],
"source": [
"%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true\n",
"\n",
"config.media_embed = True\n",
"\n",
"class MySlide(Slide):\n",
" def construct(self):\n",
" square = Square()\n",
" circle = Circle()\n",
" \n",
" self.play(Create(square))\n",
" self.next_slide()\n",
" self.play(Transform(square, circle))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "54fa2d3e-bfee-417d-b64b-f3f30a8749ea",
"metadata": {},
"outputs": [],
"source": [
"class MyOtherSlide(Slide):\n",
" def construct(self):\n",
" text = VGroup(\n",
" Text(\"Press\"),\n",
" Text(\"and\"),\n",
" Text(\"loop\"),\n",
" ).arrange(DOWN, buff=1.0)\n",
"\n",
" self.play(Write(text))\n",
" self.next_slide(loop=True)\n",
" self.play(Indicate(text[-1], scale_factor=2.0, run_time=0.5))\n",
" self.next_slide()\n",
" self.play(FadeOut(text))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7d8ad450-1487-4ca7-8d89-bf8ac344e1fa",
"metadata": {},
"outputs": [],
"source": [
"%manim_slides -v WARNING --progress_bar None MyOtherSlide --manim-slides controls=true"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"language": "python",
"name": "manim-slides"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -1,195 +0,0 @@
# Sharing your slides
Maybe one of the most important features is the ability to share your
presentation with other people, or even with yourself but on another computer!
There exists a variety of solutions, and all of them are exposed here.
We will go from the *most restrictive* method, to the least restrictive one.
If you need to present on a computer without prior knowledge on what will be
installed on it, please directly refer to the last sections.
:::{note}
In the next sections, we will assume your animations are described
in `example.py`, and you have one presentation called `BasicExample`.
:::
## With Manim Slides installed on the target machine
If Manim Slides, Manim (or ManimGL), and their dependencies are installed, then
using `manim-slides present` allows for the best presentations, with the most
options available.
### Sharing your Python file(s)
The lightest way to share your presentation is with the Python files that
describe the slides.
If you have such files, you can recompile the animations locally, and use
`manim-slides present` for your presentation. You may want to copy / paste
you own `.manim-slides.json` config file, but it is **not recommended** if
you are sharing from one platform (e.g., Linux) to another (e.g., Windows) as
the key bindings might not be the same.
Example:
```bash
# If you use ManimGl, add `--GL` after `render`
manim-slides render example.py BasicExample
# This or `manim-slides BasicExample` works since
# `present` is implied by default
manim-slides present BasicExample
```
### Sharing your animations files
If you do not want to recompile all the animations, you can simply share the
slides folder (defaults to `./slides`). Then, Manim Slides will be able to read
the animations from this folder and its subdirectories.
Example:
```bash
# Make sure that the slides directory is in the current
# working directory, or specify with `--folder <FOLDER>`
manim-slides present BasicExample
```
and the corresponding tree:
```
.
└── slides
   ├── BasicExample.json
   └── files
     └── BasicExample (files not shown)
```
## Without Manim Slides installed on the target machine
An alternative to `manim-slides present` is `manim-slides convert`.
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
[Pull Request](https://github.com/jeertmans/manim-slides/compare).
A major advantage of HTML files is that they can be opened cross-platform,
granted one has a modern web browser (which is pretty standard).
### Sharing HTML and animation files
First, you need to create the HTML file and its assets directory.
Example:
```bash
manim-slides convert BasicExample basic_example.html
```
Then, you need to copy the HTML files and its assets directory to target location,
while keeping the relative path between the HTML and the assets the same. The
easiest solution is to compress both the file and the directory into one ZIP,
and to extract it to the desired location.
By default, the assets directory will be named after the main HTML file, using `{basename}_assets`.
Example:
```
.
├── basic_example_assets
│   ├── 1413466013_2261824125_223132457.mp4
│   ├── 1672018281_2145352439_3942561600.mp4
│   └── 1672018281_3136302242_2191168284.mp4
└── basic_example.html
```
Then, you can simply open the HTML file with any web browser application.
If you want to embed the presentation inside an HTML web page, a possibility is
to use an `iframe`:
```html
<div style="position:relative;padding-bottom:56.25%;">
<!-- 56.25 comes from aspect ratio of 16:9, change this accordingly -->
<iframe
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
frameborder="0"
width="100%"
height="100%"
allowfullscreen
allow="autoplay"
src="basic_example.html">
</iframe>
</div>
```
The additional code comes from
[this article](https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page)
and it there to preserve the original aspect ratio (16:9).
### Sharing ONE HTML file
If you set the `data_uri` option to `true` (with `-cdata_uri=true`),
all animations will be data URI encoded, making the HTML a self-contained
presentation file that can be shared on its own.
### Over the internet
HTML conversion makes it convenient to play your presentation on a
remote server.
This is how your are able to watch all the examples on this website. If you want
to know how to share your slide with GitHub pages, see the
[workflow file](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/pages.yml).
:::{warning}
Keep in mind that playing large video files over the internet network
can take some time, and *glitches* may occur between slide transitions for this
reason.
:::
### Using the Github starter template
A [starter template](https://github.com/jeertmans/manim-slides-starter) is
available which allows to quickly get going with a new Manim slides
presentation on your Github account. The template comes ready with
functionality to automate tasks using Github actions and publish to Github
Pages. Please refer to the template page for usage instructions.
### With PowerPoint (*EXPERIMENTAL*)
A recent conversion feature is to the PowerPoint format, thanks to the
`python-pptx` package. Even though it is fully working,
it is still considered in an *EXPERIMENTAL* status because we do not
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
Basically, you can create a PowerPoint in a single command:
```bash
manim-slides convert --to=pptx BasicExample basic_example.pptx
```
All the videos and necessary files will be contained inside the `.pptx` file, so
you can safely share it with anyone. By default, the `poster_frame_image`, i.e.,
what is displayed by PowerPoint when the video is not playing, is the first
frame of each slide. This allows for smooth transitions.
In the future, we hope to provide more features to this format,
so feel free to suggest new features too!
### Static PDF presentation
If you ever need backup slides, that are only made of PDF pages
with static images, you can generate such a PDF with the following command:
```bash
manim-slides convert --to=pdf BasicExample basic_example.pdf
```
Note that you will lose all the benefits from animated slides. Therefore,
this is only recommended to be used as a backup plan. By default, the last frame
of each slide will be printed. This can be changed to be the first one with
`-cframe_index=first`.

View File

@ -1,6 +0,0 @@
# Manim Slides' Sphinx directive
```{eval-rst}
.. automodule:: manim_slides.docs.manim_slides_directive
:members: ManimSlidesDirective
```

View File

@ -1,311 +0,0 @@
# flake8: noqa: F403, F405
# type: ignore
from manim_slides import Slide, ThreeDSlide
from manim_slides.slide import MANIM, MANIMGL
if MANIM:
from manim import *
elif MANIMGL:
from manimlib import *
class BasicExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.next_slide()
self.play(dot.animate.move_to(ORIGIN))
class ConvertExample(Slide):
"""WARNING: this example does not seem to work with ManimGL."""
def construct(self):
self.wait_time_between_slides = 0.1
title = VGroup(
Text("From Manim animations", t2c={"From": BLUE}),
Text("to slides presentation", t2c={"to": BLUE}),
Text("with Manim Slides", t2w={"[-12:]": BOLD}, t2c={"[-13:]": YELLOW}),
).arrange(DOWN)
step_1 = Text("1. In your scenes file, import Manim Slides")
step_2 = Text("2. Replace Scene with Slide")
step_3 = Text("3. In construct, add pauses where you need")
step_4 = Text("4. You can also create loops")
step_5 = Text("5. Render you scene with Manim")
step_6 = Text("6. Open your presentation with Manim Slides")
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
step.scale(0.5).to_corner(UL)
step = step_1
self.play(FadeIn(title))
self.next_slide()
code = Code(
code="""from manim import *
class Example(Scene):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
square = Square()
self.play(Transform(dot, square))
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_1 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Scene):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
square = Square()
self.play(Transform(dot, square))
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_2 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
square = Square()
self.play(Transform(dot, square))
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_3 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot()
self.add(dot)
self.play(Indicate(dot, scale_factor=2))
self.next_slide()
square = Square()
self.play(Transform(dot, square))
self.next_slide()
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_4 = Code(
code="""from manim import *
from manim_slides import Slide
class Example(Slide):
def construct(self):
dot = Dot()
self.add(dot)
self.next_slide(loop=True)
self.play(Indicate(dot, scale_factor=2))
self.next_slide()
square = Square()
self.play(Transform(dot, square))
self.next_slide()
self.play(Rotate(square, angle=PI/2))
""",
language="python",
)
code_step_5 = Code(
code="manim example.py Example",
language="console",
)
code_step_6 = Code(
code="manim-slides Example",
language="console",
)
or_text = Text("or generate HTML presentation").scale(0.5)
code_step_7 = Code(
code="manim-slides convert Example slides.html --open",
language="console",
).shift(DOWN)
self.wipe(title, code)
self.next_slide()
self.play(FadeIn(step, shift=RIGHT))
self.play(Transform(code, code_step_1))
self.next_slide()
self.play(Transform(step, step_2))
self.play(Transform(code, code_step_2))
self.next_slide()
self.play(Transform(step, step_3))
self.play(Transform(code, code_step_3))
self.next_slide()
self.play(Transform(step, step_4))
self.play(Transform(code, code_step_4))
self.next_slide()
self.play(Transform(step, step_5))
self.play(Transform(code, code_step_5))
self.next_slide(auto_next=True)
self.play(Transform(step, step_6))
self.play(Transform(code, code_step_6))
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
self.next_slide(loop=True)
self.play(FadeIn(watch_text))
self.play(FadeOut(watch_text))
self.next_slide()
self.clear()
dot = Dot()
self.add(dot)
self.next_slide(loop=True)
self.play(Indicate(dot, scale_factor=2))
self.next_slide()
square = Square()
self.play(Transform(dot, square))
self.remove(dot)
self.add(square)
self.next_slide()
self.play(Rotate(square, angle=PI / 4))
self.next_slide()
learn_more_text = (
VGroup(
Text("Learn more about Manim Slides:"),
Text("https://github.com/jeertmans/manim-slides", color=YELLOW),
)
.arrange(DOWN)
.scale(0.75)
)
self.play(Transform(square, learn_more_text))
# For ThreeDExample, things are different
if not MANIMGL:
# [manim-3d]
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.add(axes)
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.next_slide()
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.next_slide()
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.play(dot.animate.move_to(ORIGIN))
self.next_slide()
self.play(dot.animate.move_to(RIGHT * 3))
self.next_slide()
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.next_slide()
self.play(dot.animate.move_to(ORIGIN))
# [manim-3d]
else:
# [manimgl-3d]
# WARNING: 3b1b's manim change how ThreeDScene work,
# this is why things have to be managed differently.
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.add(axes)
frame = self.camera.frame
frame.set_euler_angles(
theta=30 * DEGREES,
phi=75 * DEGREES,
)
self.play(GrowFromCenter(circle))
def updater(m, dt):
return m.increment_theta((75 * DEGREES / 4) * dt)
frame.add_updater(updater)
self.next_slide()
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.next_slide()
frame.remove_updater(updater)
self.play(frame.animate.set_theta(30 * DEGREES))
self.play(dot.animate.move_to(ORIGIN))
self.next_slide()
self.play(dot.animate.move_to(RIGHT * 3))
self.next_slide()
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.next_slide()
self.play(dot.animate.move_to(ORIGIN))
# [manimgl-3d]

View File

@ -1,6 +0,0 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource>
<file alias="icon.png">static/icon.png</file>
</qresource>
</RCC>

View File

@ -1,61 +0,0 @@
import sys
from types import ModuleType
from typing import Any, List
from .__version__ import __version__
class Module(ModuleType):
def __getattr__(self, name: str) -> Any:
if name == "Slide" or name == "ThreeDSlide":
module = __import__(
"manim_slides.slide", None, None, ["Slide", "ThreeDSlide"]
)
return getattr(module, name)
elif name == "ManimSlidesMagic":
module = __import__(
"manim_slides.ipython.ipython_magic", None, None, ["ManimSlidesMagic"]
)
magic = getattr(module, name)
from IPython import get_ipython
ipy = get_ipython()
if ipy is not None:
ipy.register_magics(magic)
return magic
return ModuleType.__getattribute__(self, name)
def __dir__(self) -> List[str]:
result = list(new_module.__all__)
result.extend(
(
"__file__",
"__doc__",
"__all__",
"__docformat__",
"__name__",
"__path__",
"__package__",
"__version__",
)
)
return result
old_module = sys.modules["manim_slides"]
new_module = sys.modules["manim_slides"] = Module("manim_slides")
new_module.__dict__.update(
{
"__file__": __file__,
"__package__": "manim_slides",
"__path__": __path__,
"__doc__": __doc__,
"__version__": __version__,
"__all__": ("__version__", "ManimSlidesMagic", "Slide", "ThreeDSlide"),
}
)

View File

@ -1,73 +0,0 @@
import json
import click
import requests
from click_default_group import DefaultGroup
from .__version__ import __version__
from .convert import convert
from .logger import logger
from .present import list_scenes, present
from .render import render
from .wizard import init, wizard
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
@click.option(
"--notify-outdated-version/--silent",
" /-S",
is_flag=True,
default=True,
help="Check if a new version of Manim Slides is available.",
)
@click.version_option(__version__, "-v", "--version")
@click.help_option("-h", "--help")
def cli(notify_outdated_version: bool) -> None:
"""
Manim Slides command-line utilities.
If no command is specified, defaults to `present`.
"""
# Code below is mostly a copy from:
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
if notify_outdated_version:
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
try:
req_info: requests.models.Response = requests.get(manim_info_url, timeout=2)
req_info.raise_for_status()
stable = req_info.json()["info"]["version"]
if stable != __version__:
click.echo(
"You are using Manim Slides version "
+ click.style(f"v{__version__}", fg="red")
+ ", but version "
+ click.style(f"v{stable}", fg="green")
+ " is available."
)
click.echo(
"You should consider upgrading via "
+ click.style("pip install -U manim-slides", fg="yellow")
)
except requests.exceptions.HTTPError:
logger.debug(f"HTTP Error: {warn_prompt}")
except requests.exceptions.ConnectionError:
logger.debug(f"Connection Error: {warn_prompt}")
except requests.exceptions.Timeout:
logger.debug(f"Timed Out: {warn_prompt}")
except json.JSONDecodeError:
logger.debug(warn_prompt)
logger.debug(f"Error decoding JSON from {manim_info_url}")
except Exception:
logger.debug(f"Something went wrong: {warn_prompt}")
cli.add_command(convert)
cli.add_command(init)
cli.add_command(list_scenes)
cli.add_command(present)
cli.add_command(render)
cli.add_command(wizard)
if __name__ == "__main__":
cli()

View File

@ -1 +0,0 @@
__version__ = "5.1.0"

View File

@ -1,82 +0,0 @@
from pathlib import Path
from typing import Any, Callable
import click
from click import Context, Parameter
from .defaults import CONFIG_PATH, FOLDER_PATH
from .logger import logger
F = Callable[..., Any]
Wrapper = Callable[[F], F]
def config_path_option(function: F) -> F:
"""Wrap a function to add configuration path option."""
wrapper: Wrapper = click.option(
"-c",
"--config",
"config_path",
metavar="FILE",
default=CONFIG_PATH,
type=click.Path(dir_okay=False, path_type=Path),
help="Set path to configuration file.",
show_default=True,
)
return wrapper(function)
def config_options(function: F) -> F:
"""Wrap a function to add configuration options."""
function = config_path_option(function)
function = click.option(
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."
)(function)
function = click.option(
"-m",
"--merge",
is_flag=True,
help="Merge any existing configuration file with the new configuration.",
)(function)
return function
def verbosity_option(function: F) -> F:
"""Wrap a function to add verbosity option."""
def callback(ctx: Context, param: Parameter, value: str) -> None:
if not value or ctx.resilient_parsing:
return
logger.setLevel(value)
wrapper: Wrapper = click.option(
"-v",
"--verbosity",
type=click.Choice(
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
case_sensitive=False,
),
help="Verbosity of CLI output.",
default=None,
expose_value=False,
envvar="MANIM_SLIDES_VERBOSITY",
show_envvar=True,
callback=callback,
)
return wrapper(function)
def folder_path_option(function: F) -> F:
"""Wrap a function to add folder path option."""
wrapper: Wrapper = click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False, path_type=Path),
help="Set slides folder.",
show_default=True,
)
return wrapper(function)

View File

@ -1,342 +0,0 @@
import json
import shutil
from functools import wraps
from inspect import Parameter, signature
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
import rtoml
from pydantic import (
BaseModel,
Field,
FilePath,
PositiveInt,
PrivateAttr,
field_validator,
model_validator,
)
from pydantic_extra_types.color import Color
from .logger import logger
Receiver = Callable[..., Any]
class Signal(BaseModel): # type: ignore[misc]
__receivers: List[Receiver] = PrivateAttr(default_factory=list)
def connect(self, receiver: Receiver) -> None:
self.__receivers.append(receiver)
def disconnect(self, receiver: Receiver) -> None:
self.__receivers.remove(receiver)
def emit(self, *args: Any) -> None:
for receiver in self.__receivers:
receiver(*args)
def key_id(name: str) -> PositiveInt:
"""Avoid importing Qt too early."""
from qtpy.QtCore import Qt
return getattr(Qt, f"Key_{name}")
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
ids: List[PositiveInt] = Field(unique=True)
name: Optional[str] = None
__signal: Signal = PrivateAttr(default_factory=Signal)
@field_validator("ids")
@classmethod
def ids_is_non_empty_set(cls, ids: Set[Any]) -> Set[Any]:
if len(ids) <= 0:
raise ValueError("Key's ids must be a non-empty set")
return ids
def set_ids(self, *ids: int) -> None:
self.ids = list(set(ids))
def match(self, key_id: int) -> bool:
m = key_id in self.ids
if m:
logger.debug(f"Pressed key: {self.name}")
return m
@property
def signal(self) -> Signal:
return self.__signal
def connect(self, function: Receiver) -> None:
self.__signal.connect(function)
class Keys(BaseModel): # type: ignore[misc]
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
PLAY_PAUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("Space")], name="PLAY / PAUSE")
)
NEXT: Key = Field(default_factory=lambda: Key(ids=[key_id("Right")], name="NEXT"))
PREVIOUS: Key = Field(
default_factory=lambda: Key(ids=[key_id("Left")], name="PREVIOUS")
)
REVERSE: Key = Field(default_factory=lambda: Key(ids=[key_id("V")], name="REVERSE"))
REPLAY: Key = Field(default_factory=lambda: Key(ids=[key_id("R")], name="REPLAY"))
FULL_SCREEN: Key = Field(
default_factory=lambda: Key(ids=[key_id("F")], name="TOGGLE FULL SCREEN")
)
HIDE_MOUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("H")], name="HIDE / SHOW MOUSE")
)
@model_validator(mode="before")
@classmethod
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
ids: Set[int] = set()
for key in values.values():
if len(ids.intersection(key["ids"])) != 0:
raise ValueError(
"Two or more keys share a common key code: please make sure each key has distinct key codes"
)
ids.update(key["ids"])
return values
def merge_with(self, other: "Keys") -> "Keys":
for key_name, key in self:
other_key = getattr(other, key_name)
key.ids = list(set(key.ids).union(other_key.ids))
key.name = other_key.name or key.name
return self
def dispatch_key_function(self) -> Callable[[PositiveInt], None]:
_dispatch = {}
for _, key in self:
for _id in key.ids:
_dispatch[_id] = key.signal
def dispatch(key: PositiveInt) -> None:
if signal := _dispatch.get(key, None):
signal.emit()
return dispatch
class Config(BaseModel): # type: ignore[misc]
"""General Manim Slides config."""
keys: Keys = Field(default_factory=Keys)
@classmethod
def from_file(cls, path: Path) -> "Config":
"""Read a configuration from a file."""
return cls.model_validate(rtoml.load(path)) # type: ignore
def to_file(self, path: Path) -> None:
"""Dump the configuration to a file."""
rtoml.dump(self.model_dump(), path, pretty=True)
def merge_with(self, other: "Config") -> "Config":
"""Merge with another config."""
self.keys = self.keys.merge_with(other.keys)
return self
class BaseSlideConfig(BaseModel): # type: ignore
"""Base class for slide config."""
loop: bool = False
auto_next: bool = False
playback_rate: float = 1.0
reversed_playback_rate: float = 1.0
notes: str = ""
dedent_notes: bool = True
@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
"""
Wrap a function to transform keyword argument into an instance of this class.
The function signature is updated to reflect the new keyword-only arguments.
The wrapped function must follow two criteria:
- its last parameter must be ``**kwargs`` (or equivalent);
- and its second last parameter must be ``<arg_name>``.
"""
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
@wraps(fun)
def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807
fun_kwargs = {
key: value
for key, value in kwargs.items()
if key not in cls.__fields__
}
fun_kwargs[arg_name] = cls(**kwargs)
return fun(*args, **fun_kwargs)
sig = signature(fun)
parameters = list(sig.parameters.values())
parameters[-2:-1] = [
Parameter(
field_name,
Parameter.KEYWORD_ONLY,
default=field_info.default,
annotation=field_info.annotation,
)
for field_name, field_info in cls.__fields__.items()
]
sig = sig.replace(parameters=parameters)
__wrapper__.__signature__ = sig # type: ignore[attr-defined]
return __wrapper__
return _wrapper_
@model_validator(mode="after")
@classmethod
def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig"
) -> "BaseSlideConfig":
if base_slide_config.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes)
return base_slide_config
class PreSlideConfig(BaseSlideConfig):
"""Slide config to be used prior to rendering."""
start_animation: int
end_animation: int
@classmethod
def from_base_slide_config_and_animation_indices(
cls,
base_slide_config: BaseSlideConfig,
start_animation: int,
end_animation: int,
) -> "PreSlideConfig":
return cls(
start_animation=start_animation,
end_animation=end_animation,
**base_slide_config.dict(),
)
@field_validator("start_animation", "end_animation")
@classmethod
def index_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
return v
@model_validator(mode="after")
@classmethod
def start_animation_is_before_end(
cls, pre_slide_config: "PreSlideConfig"
) -> "PreSlideConfig":
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
)
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return pre_slide_config
@model_validator(mode="after")
@classmethod
def loop_and_auto_next_disallowed(
cls, pre_slide_config: "PreSlideConfig"
) -> "PreSlideConfig":
if pre_slide_config.loop and pre_slide_config.auto_next:
raise ValueError(
"You cannot have both `loop=True` and `auto_next=True`, "
"because a looping slide has no ending. "
"This may be supported in the future if "
"https://github.com/jeertmans/manim-slides/pull/299 gets merged."
)
return pre_slide_config
@property
def slides_slice(self) -> slice:
return slice(self.start_animation, self.end_animation)
class SlideConfig(BaseSlideConfig):
"""Slide config to be used after rendering."""
file: FilePath
rev_file: FilePath
@classmethod
def from_pre_slide_config_and_files(
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
) -> "SlideConfig":
return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
class PresentationConfig(BaseModel): # type: ignore[misc]
slides: List[SlideConfig] = Field(min_length=1)
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black"
@classmethod
def from_file(cls, path: Path) -> "PresentationConfig":
"""Read a presentation configuration from a file."""
with open(path) as f:
obj = json.load(f)
slides = obj.setdefault("slides", [])
parent = path.parent.parent # Never fails, but parents[1] can fail
for slide in slides:
if file := slide.get("file", None):
slide["file"] = parent / file
if rev_file := slide.get("rev_file", None):
slide["rev_file"] = parent / rev_file
return cls.model_validate(obj) # type: ignore
def to_file(self, path: Path) -> None:
"""Dump the presentation configuration to a file."""
with open(path, "w") as f:
f.write(self.model_dump_json(indent=2))
def copy_to(
self, folder: Path, use_cached: bool = True, include_reversed: bool = True
) -> "PresentationConfig":
"""Copy the files to a given directory."""
for slide_config in self.slides:
file = slide_config.file
rev_file = slide_config.rev_file
dest = folder / file.name
rev_dest = folder / rev_file.name
slide_config.file = dest
slide_config.rev_file = rev_dest
if not use_cached or not dest.exists():
shutil.copy(file, dest)
if include_reversed and (not use_cached or not rev_dest.exists()):
shutil.copy(rev_file, rev_dest)
return self

View File

@ -1,714 +0,0 @@
import mimetypes
import os
import platform
import subprocess
import sys
import tempfile
import webbrowser
from base64 import b64encode
from enum import Enum
from importlib import resources
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type, Union
import click
import cv2
import pptx
from click import Context, Parameter
from jinja2 import Template
from lxml import etree
from PIL import Image
from pydantic import (
BaseModel,
ConfigDict,
Field,
FilePath,
GetCoreSchemaHandler,
PositiveFloat,
PositiveInt,
ValidationError,
conlist,
)
from pydantic_core import CoreSchema, core_schema
from pydantic_extra_types.color import Color
from tqdm import tqdm
from . import templates
from .commons import folder_path_option, verbosity_option
from .config import PresentationConfig
from .logger import logger
from .present import get_scenes_presentation_config
def open_with_default(file: Path) -> None:
system = platform.system()
if system == "Darwin":
subprocess.call(("open", str(file)))
elif system == "Windows":
os.startfile(str(file)) # type: ignore[attr-defined]
else:
subprocess.call(("xdg-open", str(file)))
def validate_config_option(
ctx: Context, param: Parameter, value: Any
) -> Dict[str, str]:
config = {}
for c_option in value:
try:
key, value = c_option.split("=")
config[key] = value
except ValueError:
raise click.BadParameter(
f"Configuration options `{c_option}` could not be parsed into "
"a proper (key, value) pair. "
"Please use an `=` sign to separate key from value."
) from None
return config
def file_to_data_uri(file: Path) -> str:
"""Read a video and return the corresponding data-uri."""
b64 = b64encode(file.read_bytes()).decode("ascii")
mime_type = mimetypes.guess_type(file)[0] or "video/mp4"
return f"data:{mime_type};base64,{b64}"
def get_duration_ms(file: Path) -> float:
"""Read a video and return its duration in milliseconds."""
cap = cv2.VideoCapture(str(file))
fps: int = cap.get(cv2.CAP_PROP_FPS)
frame_count: int = cap.get(cv2.CAP_PROP_FRAME_COUNT)
return 1000 * frame_count / fps
class Converter(BaseModel): # type: ignore
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
assets_dir: str = "{basename}_assets"
template: Optional[Path] = None
def convert_to(self, dest: Path) -> None:
"""Convert self, i.e., a list of presentations, into a given format."""
raise NotImplementedError
def load_template(self) -> str:
"""
Return the template as a string.
An empty string is returned if no template is used.
"""
return ""
def open(self, file: Path) -> Any:
"""Open a file, generated with converter, using appropriate application."""
raise NotImplementedError
@classmethod
def from_string(cls, s: str) -> Type["Converter"]:
"""Return the appropriate converter from a string name."""
return {
"html": RevealJS,
"pdf": PDF,
"pptx": PowerPoint,
}[s]
class Str(str):
"""A simple string, but quoted when needed."""
# This fixes pickling issue on Python 3.8
__reduce_ex__ = str.__reduce_ex__
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.str_schema()
def __str__(self) -> str:
"""Ensure that the string is correctly quoted."""
if self in ["true", "false", "null"]:
return self
else:
return f"'{super().__str__()}'"
class StrEnum(Enum):
def __str__(self) -> str:
return str(self.value)
Function = str # Basically, anything
class JsTrue(str, StrEnum):
true = "true"
class JsFalse(str, StrEnum):
false = "false"
class JsBool(Str, StrEnum): # type: ignore
true = "true"
false = "false"
class JsNull(Str, StrEnum): # type: ignore
null = "null"
class ControlsLayout(Str, StrEnum): # type: ignore
edges = "edges"
bottom_right = "bottom-right"
class ControlsBackArrows(Str, StrEnum): # type: ignore
faded = "faded"
hidden = "hidden"
visibly = "visibly"
class SlideNumber(Str, StrEnum): # type: ignore
true = "true"
false = "false"
hdotv = "h.v"
handv = "h/v"
c = "c"
candt = "c/t"
class ShowSlideNumber(Str, StrEnum): # type: ignore
all = "all"
print = "print"
speaker = "speaker"
class KeyboardCondition(Str, StrEnum): # type: ignore
null = "null"
focused = "focused"
class NavigationMode(Str, StrEnum): # type: ignore
default = "default"
linear = "linear"
grid = "grid"
class AutoPlayMedia(Str, StrEnum): # type: ignore
null = "null"
true = "true"
false = "false"
PreloadIframes = AutoPlayMedia
class AutoAnimateMatcher(Str, StrEnum): # type: ignore
null = "null"
class AutoAnimateEasing(Str, StrEnum): # type: ignore
ease = "ease"
AutoSlide = Union[PositiveInt, JsFalse]
class AutoSlideMethod(Str, StrEnum): # type: ignore
null = "null"
MouseWheel = Union[JsNull, float]
class Transition(Str, StrEnum): # type: ignore
none = "none"
fade = "fade"
slide = "slide"
convex = "convex"
concave = "concave"
zoom = "zoom"
class TransitionSpeed(Str, StrEnum): # type: ignore
default = "default"
fast = "fast"
slow = "slow"
class BackgroundSize(Str, StrEnum): # type: ignore
# From: https://developer.mozilla.org/en-US/docs/Web/CSS/background-size
# TODO: support more background size
contain = "contain"
cover = "cover"
BackgroundTransition = Transition
class Display(Str, StrEnum): # type: ignore
block = "block"
class RevealTheme(str, StrEnum):
black = "black"
white = "white"
league = "league"
beige = "beige"
sky = "sky"
night = "night"
serif = "serif"
simple = "simple"
soralized = "solarized"
blood = "blood"
moon = "moon"
black_contrast = "black-contrast"
white_contrast = "white-contrast"
dracula = "dracula"
class RevealJS(Converter):
# Export option: use data-uri
data_uri: bool = False
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
margin: float = 0.04
min_scale: float = 0.2
max_scale: float = 2.0
# Configuration options from RevealJS
controls: JsBool = JsBool.false
controls_tutorial: JsBool = JsBool.true
controls_layout: ControlsLayout = ControlsLayout.bottom_right
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
progress: JsBool = JsBool.false
slide_number: SlideNumber = SlideNumber.false
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
hash_one_based_index: JsBool = JsBool.false
hash: JsBool = JsBool.false
respond_to_hash_changes: JsBool = JsBool.false
history: JsBool = JsBool.false
keyboard: JsBool = JsBool.true
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
disable_layout: JsBool = JsBool.false
overview: JsBool = JsBool.true
center: JsBool = JsBool.true
touch: JsBool = JsBool.true
loop: JsBool = JsBool.false
rtl: JsBool = JsBool.false
navigation_mode: NavigationMode = NavigationMode.default
shuffle: JsBool = JsBool.false
fragments: JsBool = JsBool.true
fragment_in_url: JsBool = JsBool.true
embedded: JsBool = JsBool.false
help: JsBool = JsBool.true
pause: JsBool = JsBool.true
show_notes: JsBool = JsBool.false
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
preload_iframes: PreloadIframes = PreloadIframes.null
auto_animate: JsBool = JsBool.true
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
auto_animate_duration: float = 1.0
auto_animate_unmatched: JsBool = JsBool.true
auto_animate_styles: List[str] = Field(
default_factory=lambda: [
"opacity",
"color",
"background-color",
"padding",
"font-size",
"line-height",
"letter-spacing",
"border-width",
"border-color",
"border-radius",
"outline",
"outline-offset",
]
)
auto_slide: AutoSlide = 0
auto_slide_stoppable: JsBool = JsBool.true
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
default_timing: Union[JsNull, int] = JsNull.null
mouse_wheel: JsBool = JsBool.false
preview_links: JsBool = JsBool.false
post_message: JsBool = JsBool.true
post_message_events: JsBool = JsBool.false
focus_body_on_page_visibility_change: JsBool = JsBool.true
transition: Transition = Transition.none
transition_speed: TransitionSpeed = TransitionSpeed.default
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
background_transition: BackgroundTransition = BackgroundTransition.none
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
pdf_separate_fragments: JsBool = JsBool.true
pdf_page_height_offset: int = -1
view_distance: int = 3
mobile_view_distance: int = 2
display: Display = Display.block
hide_inactive_cursor: JsBool = JsBool.true
hide_cursor_time: int = 5000
# Appearance options from RevealJS
background_color: Color = "black"
reveal_version: str = "4.6.1"
reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides"
# Pydantic options
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def load_template(self) -> str:
"""Return the RevealJS HTML template as a string."""
if isinstance(self.template, Path):
return self.template.read_text()
if sys.version_info < (3, 9):
return resources.read_text(templates, "revealjs.html")
return resources.files(templates).joinpath("revealjs.html").read_text()
def open(self, file: Path) -> bool:
return webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> None:
"""
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
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
logger.debug(f"Assets will be saved to: {full_assets_dir}")
full_assets_dir.mkdir(parents=True, exist_ok=True)
for presentation_config in self.presentation_configs:
presentation_config.copy_to(full_assets_dir, include_reversed=False)
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "w") as f:
revealjs_template = Template(self.load_template())
options = self.dict()
options["assets_dir"] = assets_dir
has_notes = any(
slide_config.notes != ""
for presentation_config in self.presentation_configs
for slide_config in presentation_config.slides
)
content = revealjs_template.render(
file_to_data_uri=file_to_data_uri,
get_duration_ms=get_duration_ms,
has_notes=has_notes,
**options,
)
f.write(content)
class FrameIndex(str, Enum):
first = "first"
last = "last"
class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PDF presentation, saved to DEST."""
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
cap = cv2.VideoCapture(str(file))
if frame_index == FrameIndex.last:
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
ret, frame = cap.read()
cap.release()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return Image.fromarray(frame)
else:
raise ValueError("Failed to read {image_index} image from video file")
images = []
for i, presentation_config in enumerate(self.presentation_configs):
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
images.append(
read_image_from_video_file(slide_config.file, self.frame_index)
)
dest.parent.mkdir(parents=True, exist_ok=True)
images[0].save(
dest,
"PDF",
resolution=self.resolution,
save_all=True,
append_images=images[1:],
)
class PowerPoint(Converter):
left: PositiveInt = 0
top: PositiveInt = 0
width: PositiveInt = 1280
height: PositiveInt = 720
auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None: # noqa: C901
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
prs.slide_width = self.width * 9525
prs.slide_height = self.height * 9525
layout = prs.slide_layouts[6] # Should be blank
# 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
) -> None:
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
el_cnt = xpath(
media.element.getparent().getparent().getparent(),
'.//p:timing//p:video//p:spTgt[@spid="%s"]' % el_id,
)[0]
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
cond.set("delay", "0")
if loop:
ctn = xpath(el_cnt.getparent().getparent(), ".//p:cTn")[0]
ctn.set("repeatCount", "indefinite")
def xpath(el: etree.Element, query: str) -> etree.XPath:
nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
return etree.ElementBase.xpath(el, query, namespaces=nsmap)
def save_first_image_from_video_file(file: Path) -> Optional[str]:
cap = cv2.VideoCapture(file.as_posix())
ret, frame = cap.read()
cap.release()
if ret:
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
cv2.imwrite(f.name, frame)
f.close()
return f.name
else:
logger.warn("Failed to read first image from video file")
return None
for i, presentation_config in enumerate(self.presentation_configs):
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
file = slide_config.file
mime_type = mimetypes.guess_type(file)[0]
if self.poster_frame_image is None:
poster_frame_image = save_first_image_from_video_file(file)
else:
poster_frame_image = str(self.poster_frame_image)
slide = prs.slides.add_slide(layout)
movie = slide.shapes.add_movie(
str(file),
self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes
if self.auto_play_media:
auto_play_media(movie, loop=slide_config.loop)
dest.parent.mkdir(parents=True, exist_ok=True)
prs.save(dest)
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""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:
return
to = ctx.params.get("to", "html")
converter = Converter.from_string(to)(
presentation_configs=[PresentationConfig()]
)
for key, value in converter.dict().items():
click.echo(f"{key}: {value!r}")
ctx.exit()
return click.option( # type: ignore
"--show-config",
is_flag=True,
help="Show supported options for given format and exit.",
default=None,
expose_value=False,
show_envvar=True,
callback=callback,
)(function)
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""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:
return
to = ctx.params.get("to", "html")
template = ctx.params.get("template", None)
converter = Converter.from_string(to)(
presentation_configs=[PresentationConfig()], template=template
)
click.echo(converter.load_template())
ctx.exit()
return click.option( # type: ignore
"--show-template",
is_flag=True,
help="Show the template (currently) used for a given conversion format and exit.",
default=None,
expose_value=False,
show_envvar=True,
callback=callback,
)(function)
@click.command()
@click.argument("scenes", nargs=-1)
@folder_path_option
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
@click.option(
"--to",
type=click.Choice(["auto", "html", "pdf", "pptx"], case_sensitive=False),
metavar="FORMAT",
default="auto",
show_default=True,
help="Set the conversion format to use. Use 'auto' to detect format from DEST.",
)
@click.option(
"--open",
"open_result",
is_flag=True,
help="Open the newly created file using the approriate application.",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
@click.option(
"-c",
"--config",
"config_options",
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. "
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
)
@click.option(
"--use-template",
"template",
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``.",
)
@show_template_option
@show_config_options
@verbosity_option
def convert(
scenes: List[str],
folder: Path,
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: Dict[str, str],
template: Optional[Path],
) -> None:
"""Convert SCENE(s) into a given format and writes the result in DEST."""
presentation_configs = get_scenes_presentation_config(scenes, folder)
try:
if to == "auto":
fmt = dest.suffix[1:].lower()
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warn(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = Converter.from_string(to)
converter = cls(
presentation_configs=presentation_configs,
template=template,
**config_options,
)
converter.convert_to(dest)
if open_result:
converter.open(dest)
except ValidationError as e:
errors = e.errors()
msg = [
f"{len(errors)} error(s) occured with configuration options for '{to}', see below."
]
for error in errors:
option = error["loc"][0]
_msg = error["msg"]
msg.append(f"Option '{option}': {_msg}")
raise click.UsageError("\n".join(msg)) from None

View File

@ -1,4 +0,0 @@
from pathlib import Path
FOLDER_PATH: Path = Path("./slides")
CONFIG_PATH: Path = Path(".manim-slides.toml")

View File

@ -1,532 +0,0 @@
# type: ignore
r"""
A directive for including Manim Slides in a Sphinx document
===========================================================
.. warning::
This Sphinx extension requires Manim to be installed,
and won't probably work on ManimGL examples.
.. note::
The current implementation is highly inspired from Manim's own
sphinx directive, from v0.17.3.
When rendering the HTML documentation, the ``.. manim-slides::``
directive implemented here allows to include rendered videos.
This directive requires three additional dependencies:
``manim``, ``docutils`` and ``jinja2``. The last two are usually bundled
with Sphinx.
You can install them manually, or with the extra keyword:
.. code-block:: bash
pip install "manim-slides[sphinx-directive]"
Note that you will still need to install Manim's platform-specific dependencies,
see
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
Usage
-----
First, you must include the directive in the Sphinx configuration file:
.. code-block:: python
:caption: Sphinx configuration file (usually :code:`docs/source/conf.py`).
:emphasize-lines: 3
extensions = [
# ...
"manim_slides.docs.manim_slides_directive",
]
Its basic usage that allows processing **inline content**
looks as follows::
.. manim-slides:: MySlide
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
...
It is required to pass the name of the class representing the
scene to be rendered to the directive.
As a second application, the directive can also be used to
render scenes that are defined within doctests, for example::
.. manim-slides:: DirectiveDoctestExample
:ref_classes: Dot
>>> from manim import Create, Dot, RED
>>> from manim_slides import Slide
>>> dot = Dot(color=RED)
>>> dot.color
<Color #fc6255>
>>> class DirectiveDoctestExample(Slide):
... def construct(self):
... self.play(Create(dot))
...
A third application is to render scenes from another specific file::
.. manim-slides:: file.py:FileExample
:hide_source:
:quality: high
.. warning::
The code will be executed with the current working directory
being the same as the one containing the source file. This being said,
you should probably not include examples that rely on external files, since
relative paths risk to be broken.
Options
-------
Options can be passed as follows::
.. manim-slides:: <file>:<Class name>
:<option name>: <value>
The following configuration options are supported by the
directive:
hide_source
If this flag is present without argument,
the source code is not displayed above the rendered video.
quality : {'low', 'medium', 'high', 'fourk'}
Controls render quality of the video, in analogy to
the corresponding command line flags.
ref_classes
A list of classes, separated by spaces, that is
rendered in a reference block after the source code.
ref_functions
A list of functions, separated by spaces,
that is rendered in a reference block after the source code.
ref_methods
A list of methods, separated by spaces,
that is rendered in a reference block after the source code.
template
A path to the template file to use.
config_options
An unprocessed string of options to pass to ``manim-slides convert``.
Options must be separated with a space, and each option must be
a key, value pair using an equal sign as a separator.
Unlike for the CLI version, you don't need to prepend each option with
``-c``.
E.g., pass ``slide_number=true controls=false``.
By default, ``controls=true`` is set.
Examples
--------
The following code::
.. manim-slides:: MySlide
:hide_source:
:config_options: slide_number=true controls=false
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
text = Text("Hello")
self.wipe([], text)
self.next_slide()
self.play(text.animate.scale(2))
self.next_slide()
self.zoom(text)
Renders as follows:
.. manim-slides:: MySlide
:hide_source:
:config_options: slide_number=true controls=false
from manim import *
from manim_slides import Slide
class MySlide(Slide):
def construct(self):
text = Text("Hello")
self.wipe([], text)
self.next_slide()
self.play(text.animate.scale(2))
self.next_slide()
self.zoom(text)
""" # noqa: D400, D415
from __future__ import annotations
import csv
import itertools as it
import re
import shlex
import sys
from pathlib import Path
from timeit import timeit
import jinja2
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import StringList
from manim import QUALITIES
from ..convert import RevealJS
from ..present import get_scenes_presentation_config
classnamedict = {}
class SkipManimNode(nodes.Admonition, nodes.Element):
"""
Auxiliary node class that is used when the ``skip-manim-slides`` tag is present or
``.pot`` files are being built.
Skips rendering the manim-slides directive and outputs a placeholder instead.
"""
pass
def visit(self, node, name=""):
self.visit_admonition(node, name)
if not isinstance(node[0], nodes.title):
node.insert(0, nodes.title("skip-manim-slides", "Example Placeholder"))
def depart(self, node):
self.depart_admonition(node)
def process_name_list(option_input: str, reference_type: str) -> list[str]:
r"""
Reformats a string of space separated class names as a list of strings containing
valid Sphinx references.
Tests
-----
::
>>> process_name_list("Tex TexTemplate", "class")
[':class:`~.Tex`', ':class:`~.TexTemplate`']
>>> process_name_list("Scene.play Mobject.rotate", "func")
[':func:`~.Scene.play`', ':func:`~.Mobject.rotate`']
"""
return [f":{reference_type}:`~.{name}`" for name in option_input.split()]
class ManimSlidesDirective(Directive):
r"""
The manim-slides directive, rendering videos while building the documentation.
See the module docstring for documentation.
"""
has_content = True
required_arguments = 1
optional_arguments = 0
option_spec = { # noqa: RUF012
"hide_source": bool,
"quality": lambda arg: directives.choice(
arg,
("low", "medium", "high", "fourk"),
),
"ref_modules": lambda arg: process_name_list(arg, "mod"),
"ref_classes": lambda arg: process_name_list(arg, "class"),
"ref_functions": lambda arg: process_name_list(arg, "func"),
"ref_methods": lambda arg: process_name_list(arg, "meth"),
"template": lambda arg: Path(arg),
"config_options": lambda arg: dict(
option.split("=") for option in shlex.split(arg)
),
}
final_argument_whitespace = True
def run(self): # noqa: C901
# Rendering is skipped if the tag skip-manim is present,
# or if we are making the pot-files
should_skip = (
"skip-manim-slides"
in self.state.document.settings.env.app.builder.tags.tags
or self.state.document.settings.env.app.builder.name == "gettext"
)
if should_skip:
node = SkipManimNode()
self.state.nested_parse(
StringList(
[
f"Placeholder block for ``{self.arguments[0]}``.",
"",
".. code-block:: python",
"",
]
+ [" " + line for line in self.content]
),
self.content_offset,
node,
)
return [node]
from manim import config, tempconfig
global classnamedict
def split_file_cls(arg: str) -> tuple[Path, str]:
if ":" in arg:
file, cls = arg.split(":", maxsplit=1)
_, file = self.state.document.settings.env.relfn2path(file)
return Path(file), cls
else:
return None, arg
arguments = [split_file_cls(arg) for arg in self.arguments]
clsname = arguments[0][1]
if clsname not in classnamedict:
classnamedict[clsname] = 1
else:
classnamedict[clsname] += 1
hide_source = "hide_source" in self.options
ref_content = (
self.options.get("ref_modules", [])
+ self.options.get("ref_classes", [])
+ self.options.get("ref_functions", [])
+ self.options.get("ref_methods", [])
)
if ref_content:
ref_block = "References: " + " ".join(ref_content)
else:
ref_block = ""
if "quality" in self.options:
quality = f'{self.options["quality"]}_quality'
else:
quality = "example_quality"
frame_rate = QUALITIES[quality]["frame_rate"]
pixel_height = QUALITIES[quality]["pixel_height"]
pixel_width = QUALITIES[quality]["pixel_width"]
state_machine = self.state_machine
document = state_machine.document
source_file_name = Path(document.attributes["source"])
source_rel_name = source_file_name.relative_to(setup.confdir)
source_rel_dir = source_rel_name.parents[0]
dest_dir = Path(setup.app.builder.outdir, source_rel_dir).absolute()
if not dest_dir.exists():
dest_dir.mkdir(parents=True, exist_ok=True)
source_block = [
".. code-block:: python",
"",
*(" " + line for line in self.content),
]
source_block = "\n".join(source_block)
config.media_dir = (Path(setup.confdir) / "media").absolute()
config.images_dir = "{media_dir}/images"
config.video_dir = "{media_dir}/videos/{quality}"
output_file = f"{clsname}-{classnamedict[clsname]}"
config.assets_dir = Path("_static")
config.progress_bar = "none"
config.verbosity = "WARNING"
example_config = {
"frame_rate": frame_rate,
"pixel_height": pixel_height,
"pixel_width": pixel_width,
"output_file": output_file,
}
if file := arguments[0][0]:
user_code = file.absolute().read_text().splitlines()
else:
user_code = self.content
if user_code[0].startswith(">>> "): # check whether block comes from doctest
user_code = [
line[4:] for line in user_code if line.startswith((">>> ", "... "))
]
code = [
*user_code,
f"{clsname}().render()",
]
try:
with tempconfig(example_config):
print(f"Rendering {clsname}...") # noqa: T201
run_time = timeit(lambda: exec("\n".join(code), globals()), number=1)
video_dir = config.get_dir("video_dir")
except Exception as e:
raise RuntimeError(f"Error while rendering example {clsname}") from e
_write_rendering_stats(
clsname,
run_time,
self.state.document.settings.env.docname,
)
# copy video file to output directory
filename = f"{output_file}.html"
filesrc = video_dir / filename
destfile = Path(dest_dir, filename)
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")
)
template = self.options.get("template", None)
if template:
template = source_file_name.parents[0].joinpath(template)
config_options = self.options.get("config_options", {})
config_options.setdefault("controls", "true")
RevealJS(
presentation_configs=presentation_configs,
template=template,
**config_options,
).convert_to(destfile)
rendered_template = jinja2.Template(TEMPLATE).render(
clsname=clsname,
clsname_lowercase=clsname.lower(),
hide_source=hide_source,
filesrc_rel=Path(filesrc).relative_to(setup.confdir).as_posix(),
output_file=output_file,
source_block=source_block,
ref_block=ref_block,
)
state_machine.insert_input(
rendered_template.split("\n"),
source=document.attributes["source"],
)
return []
rendering_times_file_path = Path("../rendering_times.csv")
def _write_rendering_stats(scene_name, run_time, file_name):
with rendering_times_file_path.open("a") as file:
csv.writer(file).writerow(
[
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
scene_name,
"%.3f" % run_time,
],
)
def _log_rendering_times(*args):
if rendering_times_file_path.exists():
with rendering_times_file_path.open() as file:
data = list(csv.reader(file))
if len(data) == 0:
sys.exit()
print("\nRendering Summary\n-----------------\n") # noqa: T201
max_file_length = max(len(row[0]) for row in data)
for key, group in it.groupby(data, key=lambda row: row[0]):
key = key.ljust(max_file_length + 1, ".")
group = list(group)
if len(group) == 1:
row = group[0]
print(f"{key}{row[2].rjust(7, '.')}s {row[1]}") # noqa: T201
continue
time_sum = sum(float(row[2]) for row in group)
print( # noqa: T201
f"{key}{f'{time_sum:.3f}'.rjust(7, '.')}s => {len(group)} EXAMPLES",
)
for row in group:
print( # noqa: T201
f"{' '*(max_file_length)} {row[2].rjust(7)}s {row[1]}"
)
print("") # noqa: T201
def _delete_rendering_times(*args):
if rendering_times_file_path.exists():
rendering_times_file_path.unlink()
def setup(app):
app.add_node(SkipManimNode, html=(visit, depart))
setup.app = app
setup.config = app.config
setup.confdir = app.confdir
app.add_directive("manim-slides", ManimSlidesDirective)
app.connect("builder-inited", _delete_rendering_times)
app.connect("build-finished", _log_rendering_times)
metadata = {"parallel_read_safe": False, "parallel_write_safe": True}
return metadata
TEMPLATE = r"""
{% if not hide_source %}
.. raw:: html
<div id="{{ clsname_lowercase }}" class="admonition admonition-manim-example">
<p class="admonition-title">Example: {{ clsname }} <a class="headerlink" href="#{{ clsname_lowercase }}">¶</a></p>
{% endif %}
.. raw:: html
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
<div style="position:relative;padding-bottom:56.25%;">
<iframe
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
frameborder="0"
width="100%"
height="100%"
allowfullscreen
allow="autoplay"
src="./{{ output_file }}.html">
</iframe>
</div>
{% if not hide_source %}
{{ source_block }}
{{ ref_block }}
.. raw:: html
</div>
{% endif %}
"""

View File

@ -1,267 +0,0 @@
"""
Utilities for using Manim Slides with IPython (in particular: Jupyter notebooks).
=================================================================================
.. toctree::
:hidden:
magic_example
.. note::
The current implementation is highly inspired from Manim's own
IPython magics, from v0.17.3.
This magic requires two additional dependencies: ``manim`` and ``IPython``.
You can install them manually, or with the extra keyword:
.. code-block:: bash
pip install "manim-slides[magic]"
Note that you will still need to install Manim's platform-specific dependencies,
see
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
"""
from __future__ import annotations
import logging
import mimetypes
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any
from IPython import get_ipython
from IPython.core.interactiveshell import InteractiveShell
from IPython.core.magic import Magics, line_cell_magic, magics_class, needs_local_scope
from IPython.display import HTML, display
from manim import config, logger, tempconfig
from manim.__main__ import main
from manim.constants import RendererType
from manim.renderer.shader import shader_program_cache
from ..convert import RevealJS
from ..present import get_scenes_presentation_config
@magics_class
class ManimSlidesMagic(Magics): # type: ignore
def __init__(self, shell: InteractiveShell) -> None:
super().__init__(shell)
self.rendered_files: dict[Path, Path] = {}
@needs_local_scope
@line_cell_magic
def manim_slides( # noqa: C901
self,
line: str,
cell: str | None = None,
local_ns: dict[str, Any] | None = None,
) -> None:
r"""
Render Manim Slides contained in IPython cells. Works as a line or cell magic.
.. note::
This magic works pretty much like the one from Manim, except that it
will render Manim Slides using RevealJS. For passing arguments to
Manim Slides' convert module, use ``-manim-slides key=value``.
Everything that is after ``--manim-slides`` will be send to
Manim Slides' command. E.g., use ``--manim-slides controls=true``
to display control buttons.
.. hint::
This line and cell magic works best when used in a JupyterLab
environment: while all of the functionality is available for
classic Jupyter notebooks as well, it is possible that videos
sometimes don't update on repeated execution of the same cell
if the scene name stays the same.
This problem does not occur when using JupyterLab.
Please refer to `<https://jupyter.org/>`_ for more information about JupyterLab
and Jupyter notebooks.
Usage in line mode::
%manim_slides [CLI options] MyAwesomeSlide
Usage in cell mode::
%%manim_slides [CLI options] MyAwesomeSlide
class MyAweseomeSlide(Slide):
def construct(self):
...
Run ``%manim_slides --help`` and ``%manim_slides render --help``
for possible command line interface options.
.. note::
The maximal width of the rendered videos that are displayed in the notebook can be
configured via the ``media_width`` configuration option. The default is set to ``25vw``,
which is 25% of your current viewport width. To allow the output to become as large
as possible, set ``config.media_width = "100%"``.
The ``media_embed`` option will embed the image/video output in the notebook. This is
generally undesirable as it makes the notebooks very large, but is required on some
platforms (notably Google's CoLab, where it is automatically enabled unless suppressed
by ``config.embed = False``) and needed in cases when the notebook (or converted HTML
file) will be moved relative to the video locations. Use-cases include building
documentation with Sphinx and JupyterBook. See also the
:mod:`Manim Slides directive for Sphinx
<manim_slides.docs.manim_slides_directive>`.
Examples
--------
First make sure to put ``from manim_slides import ManimSlidesMagic``,
or even ``from manim_slides import *``
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
could look as follows::
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true data_uri=true
class MySlide(Slide):
def construct(self):
square = Square()
circle = Circle()
self.play(Create(square))
self.next_slide()
self.play(Transform(square, circle))
Evaluating this cell will render and display the ``MySlide`` slide
defined in the body of the cell.
.. note::
In case you want to hide the red box containing the output progress bar, the ``progress_bar`` config
option should be set to ``None``. This can also be done by passing ``--progress_bar None`` as a
CLI flag.
"""
if local_ns is None:
local_ns = {}
if cell:
exec(cell, local_ns)
split_args = line.split("--manim-slides", 2)
manim_args = split_args[0].split()
if len(split_args) == 2:
manim_slides_args = split_args[1].split()
else:
manim_slides_args = []
args = manim_args
if not len(args) or "-h" in args or "--help" in args or "--version" in args:
main(args, standalone_mode=False, prog_name="manim")
return
modified_args = self.add_additional_args(args)
args = main(modified_args, standalone_mode=False, prog_name="manim")
with tempconfig(local_ns.get("config", {})):
config.digest_args(args)
logging.getLogger("manim-slides").setLevel(logging.getLogger("manim").level)
renderer = None
if config.renderer == RendererType.OPENGL:
from manim.renderer.opengl_renderer import OpenGLRenderer
renderer = OpenGLRenderer()
try:
scene_cls = local_ns[config["scene_names"][0]]
scene = scene_cls(renderer=renderer)
scene.render()
finally:
# Shader cache becomes invalid as the context is destroyed
shader_program_cache.clear()
# Close OpenGL window here instead of waiting for the main thread to
# finish causing the window to stay open and freeze
if renderer is not None and renderer.window is not None:
renderer.window.close()
if config["output_file"] is None:
logger.info("No output file produced")
return
local_path = Path(config["output_file"]).relative_to(Path.cwd())
tmpfile = (
Path(config["media_dir"]) / "jupyter" / f"{_generate_file_name()}.html"
)
if local_path in self.rendered_files:
self.rendered_files[local_path].unlink()
pass
self.rendered_files[local_path] = tmpfile
tmpfile.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(local_path, tmpfile)
file_type = mimetypes.guess_type(config["output_file"])[0] or "video/mp4"
embed = config["media_embed"]
if embed is None:
# videos need to be embedded when running in google colab.
# do this automatically in case config.media_embed has not been
# set explicitly.
embed = "google.colab" in str(get_ipython())
if not file_type.startswith("video"):
raise ValueError(
f"Manim Slides only supports video files, not {file_type}"
)
clsname = config["scene_names"][0]
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
if embed: # Embedding implies data-uri
kwargs["data_uri"] = "true"
# TODO: FIXME
# Seems like files are blocked so date-uri is the only working option...
if kwargs.get("data_uri", "false").lower().strip() == "false":
logger.warn(
"data_uri option is currently automatically enabled, "
"because using local video files does not seem to work properly."
)
kwargs["data_uri"] = "true"
presentation_configs = get_scenes_presentation_config(
[clsname], Path("./slides")
)
RevealJS(presentation_configs=presentation_configs, **kwargs).convert_to(
tmpfile
)
if embed:
result = HTML(
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" srcdoc="{srcdoc}"></iframe></div>""".format(
srcdoc=tmpfile.read_text().replace('"', "'")
)
)
else:
result = HTML(
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{src}"></iframe></div>""".format(
src=tmpfile.as_posix()
)
)
display(result)
def add_additional_args(self, args: list[str]) -> list[str]:
additional_args = ["--jupyter"]
# Use webm to support transparency
if "-t" in args and "--format" not in args:
additional_args += ["--format", "webm"]
return additional_args + args[:-1] + [""] + [args[-1]]
def _generate_file_name() -> str:
return config["scene_names"][0] + "@" + datetime.now().strftime("%Y-%m-%d@%H-%M-%S") # type: ignore

View File

@ -1,52 +0,0 @@
"""
Logger utils, mostly copied from Manim Community.
Source code:
https://github.com/ManimCommunity/manim/blob/d5b65b844b8ce8ff5151a2f56f9dc98cebbc1db4/manim/_config/logger_utils.py#L29-L101
"""
import logging
from rich.console import Console
from rich.logging import RichHandler
__all__ = ["logger"]
HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
"Played",
"animations",
"scene",
"Reading",
"Writing",
"script",
"arguments",
"Invalid",
"Aborting",
"module",
"File",
"Rendering",
"Rendered",
"Pressed key",
]
def make_logger() -> logging.Logger:
"""Make a logger similar to the one used by Manim."""
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
rich_handler = RichHandler(
show_time=True,
console=Console(),
)
logger = logging.getLogger("manim-slides")
logger.setLevel(logging.getLogger("manim").level)
logger.addHandler(rich_handler)
if not (libav_logger := logging.getLogger("libav")).hasHandlers():
libav_logger.addHandler(rich_handler)
return logger
make_logger()
logger = logging.getLogger("manim-slides")

View File

@ -1,325 +0,0 @@
import signal
import sys
from pathlib import Path
from typing import List, Optional, Tuple
import click
from click import Context, Parameter
from pydantic import ValidationError
from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
@click.command()
@folder_path_option
@click.help_option("-h", "--help")
@verbosity_option
def list_scenes(folder: Path) -> None:
"""List available scenes."""
for i, scene in enumerate(_list_scenes(folder), start=1):
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder: Path) -> List[str]:
"""List available scenes in given directory."""
scenes = []
for filepath in folder.glob("*.json"):
try:
_ = PresentationConfig.from_file(filepath)
scenes.append(filepath.stem)
except (
Exception
) as e: # Could not parse this file as a proper presentation config
logger.warn(
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
)
logger.debug(f"Found {len(scenes)} valid scene configuration files in `{folder}`.")
return scenes
def prompt_for_scenes(folder: Path) -> List[str]:
"""Prompt the user to select scenes within a given folder."""
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
for i, scene in scene_choices.items():
click.secho(f"{i}: {scene}", fg="green")
click.echo()
click.echo("Choose number corresponding to desired scene/arguments.")
click.echo("(Use comma separated list for multiple entries)")
def value_proc(value: Optional[str]) -> List[str]:
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
if not all(0 < i <= len(scene_choices) for i in indices):
raise click.UsageError("Please only enter numbers displayed on the screen.")
return [scene_choices[i] for i in indices]
if len(scene_choices) == 0:
raise click.UsageError(
"No scenes were found, are you in the correct directory?"
)
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
return scenes # type: ignore
except ValueError as e:
raise click.UsageError(str(e)) from None
def get_scenes_presentation_config(
scenes: List[str], folder: Path
) -> List[PresentationConfig]:
"""Return a list of presentation configurations based on the user input."""
if len(scenes) == 0:
scenes = prompt_for_scenes(folder)
presentation_configs = []
for scene in scenes:
config_file = folder / f"{scene}.json"
if not config_file.exists():
raise click.UsageError(
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
)
try:
presentation_configs.append(PresentationConfig.from_file(config_file))
except ValidationError as e:
raise click.UsageError(str(e)) from None
return presentation_configs
def start_at_callback(
ctx: Context, param: Parameter, values: str
) -> Tuple[Optional[int], ...]:
if values == "(None, None)":
return (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,
) from None
values_tuple = values.split(",")
n_values = len(values_tuple)
if n_values == 2:
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 2 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
@folder_path_option
@click.option("--start-paused", is_flag=True, help="Start paused.")
@click.option(
"-F",
"--full-screen",
"--fullscreen",
"full_screen",
is_flag=True,
help="Toggle full screen mode.",
)
@click.option(
"-s",
"--skip-all",
is_flag=True,
help="Skip all slides, useful the test if slides are working. "
"Automatically sets ``--exit-after-last-slide`` to True.",
)
@click.option(
"--exit-after-last-slide",
is_flag=True,
help="At the end of last slide, the application will be exited.",
)
@click.option(
"-H",
"--hide-mouse",
is_flag=True,
help="Hide mouse cursor.",
)
@click.option(
"--aspect-ratio",
type=click.Choice(["keep", "ignore"], case_sensitive=False),
default="keep",
help="Set the aspect ratio mode to be used when rescaling the video.",
show_default=True,
)
@click.option(
"--sa",
"--start-at",
"start_at",
metavar="<SCENE,SLIDE>",
type=str,
callback=start_at_callback,
default=(None, None),
help="Start presenting at (x, y), equivalent to ``--sacn x --sasn y``, "
"and overrides values if not None.",
)
@click.option(
"--sacn",
"--start-at-scene-number",
"start_at_scene_number",
metavar="INDEX",
type=int,
default=0,
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=0,
help="Start presenting at a given slide number (0 is first, -1 is last).",
)
@click.option(
"-S",
"--screen",
"screen_number",
metavar="NUMBER",
type=int,
default=None,
help="Present content on the given screen (a.k.a. display).",
)
@click.option(
"--playback-rate",
metavar="RATE",
type=float,
default=1.0,
help="Playback rate of the video slides, see PySide6 docs for details. "
" The playback rate of each slide is defined as the product of its default "
" playback rate and the provided value.",
)
@click.option(
"--next-terminates-loop",
"next_terminates_loop",
is_flag=True,
help="If set, pressing next will turn any looping slide into a play slide.",
)
@click.option(
"--hide-info-window",
is_flag=True,
help="Hide info window.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
scenes: List[str],
config_path: Path,
folder: Path,
start_paused: bool,
full_screen: bool,
skip_all: bool,
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
start_at: Tuple[Optional[int], Optional[int], Optional[int]],
start_at_scene_number: int,
start_at_slide_number: int,
screen_number: Optional[int],
playback_rate: float,
next_terminates_loop: bool,
hide_info_window: bool,
) -> None:
"""
Present SCENE(s), one at a time, in order.
Each SCENE parameter must be the name of a Manim scene,
with existing SCENE.json config file.
You can present the same SCENE multiple times by repeating the parameter.
Use ``manim-slide list-scenes`` to list all available
scenes in a given folder.
"""
if skip_all:
exit_after_last_slide = True
presentation_configs = get_scenes_presentation_config(scenes, folder)
if config_path.exists():
try:
config = Config.from_file(config_path)
except ValidationError as e:
raise click.UsageError(str(e)) from None
else:
logger.debug("No configuration file found, using default configuration.")
config = Config()
if start_at[0]:
start_at_scene_number = start_at[0]
if start_at[1]:
start_at_slide_number = start_at[1]
from ..qt_utils import qapp
app = qapp()
app.setApplicationName("Manim Slides")
if screen_number is not None:
try:
screen = app.screens()[screen_number]
except IndexError:
logger.error(
f"Invalid screen number {screen_number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
)
screen = None
else:
screen = None
from qtpy.QtCore import Qt
aspect_ratio_modes = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
from .player import Player
player = Player(
config,
presentation_configs,
start_paused=start_paused,
full_screen=full_screen,
skip_all=skip_all,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio_mode=aspect_ratio_modes[aspect_ratio],
presentation_index=start_at_scene_number,
slide_index=start_at_slide_number,
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
)
player.show()
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.exec())

View File

@ -1,549 +0,0 @@
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from qtpy.QtMultimedia import QMediaPlayer
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)
from ..config import Config, PresentationConfig, SlideConfig
from ..logger import logger
from ..resources import * # noqa: F403
WINDOW_NAME = "Manim Slides"
class Info(QWidget): # type: ignore[misc]
key_press_event: Signal = Signal(QKeyEvent)
close_event: Signal = Signal(QCloseEvent)
def __init__(
self,
*,
full_screen: bool,
aspect_ratio_mode: Qt.AspectRatioMode,
screen: Optional[QScreen],
) -> None:
super().__init__()
if screen:
self.setScreen(screen)
self.move(screen.geometry().topLeft())
if full_screen:
self.setWindowState(Qt.WindowFullScreen)
layout = QHBoxLayout()
# Current slide view
left_layout = QVBoxLayout()
left_layout.addWidget(
QLabel("Current slide"),
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
main_video_widget = QVideoWidget()
main_video_widget.setAspectRatioMode(aspect_ratio_mode)
main_video_widget.setFixedSize(720, 480)
self.video_sink = main_video_widget.videoSink()
left_layout.addWidget(main_video_widget)
# Current slide informations
self.scene_label = QLabel()
self.slide_label = QLabel()
self.start_time = datetime.now()
self.time_label = QLabel()
self.elapsed_label = QLabel("00h00m00s")
self.timer = QTimer()
self.timer.start(1000) # every second
self.timer.timeout.connect(self.update_time)
bottom_left_layout = QHBoxLayout()
bottom_left_layout.addWidget(
QLabel("Scene:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.scene_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Slide:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.slide_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Time:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.time_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Elapsed:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.elapsed_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
left_layout.addLayout(bottom_left_layout)
layout.addLayout(left_layout)
layout.addSpacing(20)
# Next slide preview
right_layout = QVBoxLayout()
right_layout.addWidget(
QLabel("Next slide"),
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
next_video_widget = QVideoWidget()
next_video_widget.setAspectRatioMode(aspect_ratio_mode)
next_video_widget.setFixedSize(360, 240)
self.next_media_player = QMediaPlayer()
self.next_media_player.setVideoOutput(next_video_widget)
self.next_media_player.setLoops(-1)
right_layout.addWidget(next_video_widget)
# Notes
self.slide_notes = QLabel()
self.slide_notes.setWordWrap(True)
self.slide_notes.setTextFormat(Qt.TextFormat.MarkdownText)
self.slide_notes.setFixedWidth(360)
right_layout.addWidget(
self.slide_notes,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
layout.addLayout(right_layout)
widget = QWidget()
widget.setLayout(layout)
main_layout = QVBoxLayout()
main_layout.addWidget(widget, alignment=Qt.AlignmentFlag.AlignCenter)
self.setLayout(main_layout)
@Slot()
def update_time(self) -> None:
now = datetime.now()
seconds = (now - self.start_time).total_seconds()
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
self.time_label.setText(now.strftime("%Y/%m/%d %H:%M:%S"))
self.elapsed_label.setText(
f"{int(hours):02d}h{int(minutes):02d}m{int(seconds):02d}s"
)
@Slot()
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close_event.emit(event)
@Slot()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
self.key_press_event.emit(event)
class Player(QMainWindow): # type: ignore[misc]
presentation_changed: Signal = Signal()
slide_changed: Signal = Signal()
def __init__(
self,
config: Config,
presentation_configs: List[PresentationConfig],
*,
start_paused: bool = False,
full_screen: bool = False,
skip_all: bool = False,
exit_after_last_slide: bool = False,
hide_mouse: bool = False,
aspect_ratio_mode: Qt.AspectRatioMode = Qt.KeepAspectRatio,
presentation_index: int = 0,
slide_index: int = 0,
screen: Optional[QScreen] = None,
playback_rate: float = 1.0,
next_terminates_loop: bool = False,
hide_info_window: bool = False,
):
super().__init__()
# Wizard's config
self.config = config
# Presentation configs
self.presentation_configs = presentation_configs
self.__current_presentation_index = 0
self.__current_slide_index = 0
self.current_presentation_index = presentation_index
self.current_slide_index = slide_index
self.__current_file: Path = self.current_slide_config.file
self.__playing_reversed_slide = False
# Widgets
if screen:
self.setScreen(screen)
self.move(screen.geometry().topLeft())
if full_screen:
self.setWindowState(Qt.WindowFullScreen)
else:
w, h = self.current_presentation_config.resolution
geometry = self.geometry()
geometry.setWidth(w)
geometry.setHeight(h)
self.setGeometry(geometry)
if hide_mouse:
self.setCursor(Qt.BlankCursor)
self.setWindowTitle(WINDOW_NAME)
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.video_widget = QVideoWidget()
self.video_sink = self.video_widget.videoSink()
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
self.setCentralWidget(self.video_widget)
self.media_player = QMediaPlayer(self)
self.media_player.setVideoOutput(self.video_widget)
self.playback_rate = playback_rate
self.presentation_changed.connect(self.presentation_changed_callback)
self.slide_changed.connect(self.slide_changed_callback)
self.info = Info(
full_screen=full_screen, aspect_ratio_mode=aspect_ratio_mode, screen=screen
)
self.info.close_event.connect(self.closeEvent)
self.info.key_press_event.connect(self.keyPressEvent)
self.video_sink.videoFrameChanged.connect(
lambda frame: self.info.video_sink.setVideoFrame(frame)
)
self.hide_info_window = hide_info_window
# Connecting key callbacks
self.config.keys.QUIT.connect(self.close)
self.config.keys.PLAY_PAUSE.connect(self.play_pause)
self.config.keys.NEXT.connect(self.next)
self.config.keys.PREVIOUS.connect(self.previous)
self.config.keys.REVERSE.connect(self.reverse)
self.config.keys.REPLAY.connect(self.replay)
self.config.keys.FULL_SCREEN.connect(self.full_screen)
self.config.keys.HIDE_MOUSE.connect(self.hide_mouse)
self.dispatch = self.config.keys.dispatch_key_function()
# Misc
self.exit_after_last_slide = exit_after_last_slide
self.next_terminates_loop = next_terminates_loop
# Setting-up everything
if skip_all:
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
self.media_player.setLoops(1) # Otherwise looping slides never end
if status == QMediaPlayer.MediaStatus.EndOfMedia:
self.load_next_slide()
self.media_player.mediaStatusChanged.connect(media_status_changed)
else:
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
if (
status == QMediaPlayer.MediaStatus.EndOfMedia
and self.current_slide_config.auto_next
):
self.load_next_slide()
self.media_player.mediaStatusChanged.connect(media_status_changed)
if self.current_slide_config.loop:
self.media_player.setLoops(-1)
self.load_current_media(start_paused=start_paused)
self.presentation_changed.emit()
self.slide_changed.emit()
"""
Properties
"""
@property
def presentations_count(self) -> int:
return len(self.presentation_configs)
@property
def current_presentation_index(self) -> int:
return self.__current_presentation_index
@current_presentation_index.setter
def current_presentation_index(self, index: int) -> None:
if 0 <= index < self.presentations_count:
self.__current_presentation_index = index
elif -self.presentations_count <= index < 0:
self.__current_presentation_index = index + self.presentations_count
else:
logger.warn(f"Could not set presentation index to {index}.")
return
self.presentation_changed.emit()
@property
def current_presentation_config(self) -> PresentationConfig:
return self.presentation_configs[self.current_presentation_index]
@property
def current_slides_count(self) -> int:
return len(self.current_presentation_config.slides)
@property
def current_slide_index(self) -> int:
return self.__current_slide_index
@current_slide_index.setter
def current_slide_index(self, index: int) -> None:
if 0 <= index < self.current_slides_count:
self.__current_slide_index = index
elif -self.current_slides_count <= index < 0:
self.__current_slide_index = index + self.current_slides_count
else:
logger.warn(f"Could not set slide index to {index}.")
return
self.slide_changed.emit()
@property
def current_slide_config(self) -> SlideConfig:
return self.current_presentation_config.slides[self.current_slide_index]
@property
def current_file(self) -> Path:
return self.__current_file
@current_file.setter
def current_file(self, file: Path) -> None:
self.__current_file = file
@property
def next_slide_config(self) -> Optional[SlideConfig]:
if self.playing_reversed_slide:
return self.current_slide_config
elif self.current_slide_index < self.current_slides_count - 1:
return self.presentation_configs[self.current_presentation_index].slides[
self.current_slide_index + 1
]
elif self.current_presentation_index < self.presentations_count - 1:
return self.presentation_configs[
self.current_presentation_index + 1
].slides[0]
else:
return None
@property
def next_file(self) -> Optional[Path]:
if slide_config := self.next_slide_config:
return slide_config.file # type: ignore[no-any-return]
return None
@property
def playing_reversed_slide(self) -> bool:
return self.__playing_reversed_slide
@playing_reversed_slide.setter
def playing_reversed_slide(self, playing_reversed_slide: bool) -> None:
self.__playing_reversed_slide = playing_reversed_slide
"""
Loading slides
"""
def load_current_media(self, start_paused: bool = False) -> None:
url = QUrl.fromLocalFile(str(self.current_file))
self.media_player.setSource(url)
if self.playing_reversed_slide:
self.media_player.setPlaybackRate(
self.current_slide_config.reversed_playback_rate * self.playback_rate
)
else:
self.media_player.setPlaybackRate(
self.current_slide_config.playback_rate * self.playback_rate
)
if start_paused:
self.media_player.pause()
else:
self.media_player.play()
def load_current_slide(self) -> None:
slide_config = self.current_slide_config
self.current_file = slide_config.file
if slide_config.loop:
self.media_player.setLoops(-1)
else:
self.media_player.setLoops(1)
self.load_current_media()
def load_previous_slide(self) -> None:
self.playing_reversed_slide = False
if self.current_slide_index > 0:
self.current_slide_index -= 1
elif self.current_presentation_index > 0:
self.current_presentation_index -= 1
self.current_slide_index = self.current_slides_count - 1
else:
logger.info("No previous slide.")
return
self.load_current_slide()
def load_next_slide(self) -> None:
if self.playing_reversed_slide:
self.playing_reversed_slide = False
self.preview_next_slide() # Slide number did not change, but next did
elif self.current_slide_index < self.current_slides_count - 1:
self.current_slide_index += 1
elif self.current_presentation_index < self.presentations_count - 1:
self.current_presentation_index += 1
self.current_slide_index = 0
elif self.exit_after_last_slide:
self.close()
return
else:
logger.info("No more slide to play.")
return
self.load_current_slide()
def load_reversed_slide(self) -> None:
self.playing_reversed_slide = True
self.current_file = self.current_slide_config.rev_file
self.load_current_media()
"""
Key callbacks and slots
"""
@Slot()
def presentation_changed_callback(self) -> None:
index = self.current_presentation_index
count = self.presentations_count
self.info.scene_label.setText(f"{index+1:4d}/{count:4<d}")
@Slot()
def slide_changed_callback(self) -> None:
index = self.current_slide_index
count = self.current_slides_count
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.slide_notes.setText(self.current_slide_config.notes)
self.preview_next_slide()
def preview_next_slide(self) -> None:
if slide_config := self.next_slide_config:
url = QUrl.fromLocalFile(str(slide_config.file))
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()
def show(self) -> None:
super().show()
if not self.hide_info_window:
self.info.show()
@Slot()
def close(self) -> None:
logger.info("Closing gracefully...")
self.info.close()
super().close()
@Slot()
def next(self) -> None:
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif self.next_terminates_loop and self.media_player.loops() != 1:
position = self.media_player.position()
self.media_player.setLoops(1)
self.media_player.stop()
self.media_player.setPosition(position)
self.media_player.play()
else:
self.load_next_slide()
@Slot()
def previous(self) -> None:
self.load_previous_slide()
@Slot()
def reverse(self) -> None:
self.load_reversed_slide()
self.preview_next_slide()
@Slot()
def replay(self) -> None:
self.media_player.setPosition(0)
self.media_player.play()
@Slot()
def play_pause(self) -> None:
state = self.media_player.playbackState()
if state == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif state == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
@Slot()
def full_screen(self) -> None:
if self.windowState() == Qt.WindowFullScreen:
self.setWindowState(Qt.WindowNoState)
else:
self.setWindowState(Qt.WindowFullScreen)
@Slot()
def hide_mouse(self) -> None:
if self.cursor().shape() == Qt.BlankCursor:
self.setCursor(Qt.ArrowCursor)
else:
self.setCursor(Qt.BlankCursor)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
key = event.key()
self.dispatch(key)
event.accept()

View File

@ -1,14 +0,0 @@
"""Qt utils."""
from qtpy.QtWidgets import QApplication
def qapp() -> QApplication:
"""
Return a QApplication instance, creating one
if needed.
"""
if app := QApplication.instance():
return app
return QApplication([])

View File

@ -1,54 +0,0 @@
"""
Alias command to either
``manim render [OPTIONS] [ARGS]...`` or
``manimgl [OPTIONS] [ARGS]...``.
This is especially useful for two reasons:
1. You can are sure to execute the rendering command with the same Python environment
as for ``manim-slides``.
2. You can pass options to the config.
"""
import subprocess
import sys
from typing import Tuple
import click
@click.command(
context_settings=dict(
ignore_unknown_options=True, allow_extra_args=True, help_option_names=("-h",)
),
options_metavar="[-h] [--CE|--GL]",
)
@click.option(
"--CE",
is_flag=True,
envvar="MANIM_RENDERER",
show_envvar=True,
help="If set, use Manim Community Edition (CE) renderer. "
"If this or ``--GL`` is not set, defaults to CE renderer.",
)
@click.option(
"--GL",
is_flag=True,
envvar="MANIMGL_RENDERER",
show_envvar=True,
help="If set, use ManimGL renderer.",
)
@click.argument("args", metavar="[RENDERER_ARGS]...", nargs=-1, type=click.UNPROCESSED)
def render(ce: bool, gl: bool, args: Tuple[str, ...]) -> None:
"""
Render SCENE(s) from the input FILE, using the specified renderer.
Use ``manim-slides render --help`` to see help information for
a specific renderer.
"""
if ce and gl:
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
if gl:
subprocess.run([sys.executable, "-m", "manimlib", *args])
else:
subprocess.run([sys.executable, "-m", "manim", "render", *args])

View File

@ -1,171 +0,0 @@
# type: ignore
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.4.0
# WARNING! All changes made in this file will be lost!
from qtpy import QtCore
qt_resource_data = b"\
\x00\x00\x08\x1c\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x01\x00\x00\x00\x01\x00\x08\x06\x00\x00\x00\x5cr\xa8f\
\x00\x00\x01\x84iCCPICC prof\
ile\x00\x00(\x91}\x91=H\xc3@\x1c\xc5_\
S\xa5R+\x0e\xed \xe2\x90\xa1:Y\x10-\xe2\xa8\
U(B\x85P+\xb4\xea`r\xfd\x84&\x0dI\x8a\
\x8b\xa3\xe0Zp\xf0c\xb1\xea\xe0\xe2\xac\xab\x83\xab \
\x08~\x808:9)\xbaH\x89\xffK\x0a-b<\
8\xee\xc7\xbb{\x8f\xbbw\x80\xd0\xac2\xd5\xec\x99\x00\
T\xcd2\xd2\xc9\x84\x98\xcd\xad\x8a\x81W\x04\x11F?\
\xe2\x88\xcb\xcc\xd4\xe7$)\x05\xcf\xf1u\x0f\x1f_\xef\
b<\xcb\xfb\xdc\x9fc _0\x19\xe0\x13\x89g\x99\
nX\xc4\x1b\xc4\xd3\x9b\x96\xcey\x9f8\xc2\xcar\x9e\
\xf8\x9cx\xdc\xa0\x0b\x12?r]q\xf9\x8ds\xc9a\
\x81gF\x8cLz\x9e8B,\x96\xbaX\xe9bV\
6T\xe28q4\xafj\x94/d]\xces\xde\xe2\
\xacV\xeb\xac}O\xfe\xc2PA[Y\xe6:\xcd\x11\
$\xb1\x88%H\x10\xa1\xa0\x8e\x0a\xaa\xb0\x10\xa3U#\
\xc5D\x9a\xf6\x13\x1e\xfea\xc7/\x91K!W\x05\x8c\
\x1c\x0b\xa8A\x85\xec\xf8\xc1\xff\xe0w\xb7fqj\xd2\
M\x0a%\x80\xde\x17\xdb\xfe\x18\x05\x02\xbb@\xaba\xdb\
\xdf\xc7\xb6\xdd:\x01\xfc\xcf\xc0\x95\xd6\xf1\xd7\x9a\xc0\xcc\
'\xe9\x8d\x8e\x16=\x02\x06\xb7\x81\x8b\xeb\x8e\xa6\xec\x01\
\x97;\xc0\xd0\x93.\x1b\xb2#\xf9i\x0a\xc5\x22\xf0~\
F\xdf\x94\x03\xc2\xb7@p\xcd\xed\xad\xbd\x8f\xd3\x07 \
C]\xa5n\x80\x83C`\xacD\xd9\xeb\x1e\xef\xee\xeb\
\xee\xed\xdf3\xed\xfe~\x00\xd6\xd3r\xcf+\xa2\xc1_\
\x00\x00\x00\x06bKGD\x004\x004\x004\xaf4\
\x1c\xc0\x00\x00\x00\x09pHYs\x00\x00.#\x00\x00\
.#\x01x\xa5?v\x00\x00\x00\x07tIME\x07\
\xe6\x0a\x13\x0c\x0f\x03\x13^\x06\xfe\x00\x00\x00\x19tE\
XtComment\x00Create\
d with GIMPW\x81\x0e\x17\x00\
\x00\x05\xf4IDATx\xda\xed\xddA\x92\x9b:\x18\
\x85QK\xe5%\xc1\xfe\x17\x00{rF\x19\xa4\xcb\xee\
\x801X\xd2=g\x98z\x956 }\xfe\xd5\xddy\
\xbe\xdd\x00\x00\x00\x00\x00\x801\x95\xab\xbe\xd04M\x0f\
\xb7\x1b\xb6[\xd7\xb5t\x1d\x00\x9b\x1e\xda\x8eA\xb1\xf1\
!7\x04\xc5\xc6\x87\xdc\x10\x14\x1b\x1fr#Pl~\
\xc8\x0dA\xb1\xf9!7\x02\xc5\xe6\x87\xdc\x08\x14\x9b\x1f\
r#Pl~\xc8\x8d@\xb1\xf9!7\x02\xc5\xe6\x87\
\xdc\x08T\x9b\x1frU\xb7\x00\xc6\xb4\xe5\x8d\xbb|\xe2\
/\x01\xfa<\x0aT\x9b\x1f\x1c\x01\x80\xc0\xa3@\xf5\xee\
\x0f&\x00 p\x0a\xa8\xde\xfd\xc1\x04\x00\x04N\x01\x02\
\x00&\x00@\x00\x9c\xff!\xca\xbd\x85\x17\xb1,\x8b'\
A\x9cy\x9e\xb3\x03`\xe3\x93\xec\xef\xfa\xfff\x08\xaa\
\xcd\x0f\xb9\x13p\xb5\xf9!7\x02~\x0a\x00\xc1.\x0f\
\x80w\x7fhgo\x98\x00\xc0\x04\x00\x08\x00 \x00\x80\
\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\x00 \
\x00\x80\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\
\x00 \x00\x80\x00\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\
\x00|\xd2\xbd\xb7\x17\xfc\xdbG)\x8f\xfc\xb1c-^\
\xb7g\xd1\xffu\x97\x9f\x7f0M\xd3\xe3\xcc/x\xe4\
\x06m\xfd\x1c\xf5\x91\x16_\x8b\xd7\xbc\xe7\xf3\xec\x13\x9f\
\xc5\x91\xeb\xde\xf35\xde\xb1\xaek\xe9\xf2\x08\xb0\xe7\xc6\
\x9c}\x13\x93\xafy\xef\xd7I|\x16=]w\x1d\xf1\
\xe6\x8f\xb0\xf0Z\xbc\xe6w\xff\xfe\xc4g\xd1\x8b\xea\xe6\
\xb7w\xbdG\xae\xb9\xd5\xfb\x95\xf6\x1c{\xb9\xe6\xea\x01\
x\xbd\xae/\xf7\x99\xf81\xa0\x85v\xe9\xeb;:\xe1\
\x006\xbfkF\x00l\x04\xd7\x8e\x00t\xbc\xf8m\
\x00\x11\x10\x00\x8b\xde\xfd\x10C\x01\xb0\xf9qo\x04\xc0\
\x02w\x8f\xdc\x04\x01\xb0\xb0\xdd+\x04\xc0\xf9V\x04\x10\
\x00\x8bX<\x11\x00\x9b\xdf\xbdD\x00,X\xf7\x14\x01\
\xb0P\xdd[\x04\xc0yU\x04\x10\x00\x8bRl\x11\x00\
\x9b\xdf\xbdG\x00,@\xcf\x00\x01\xb0\xf0<\x0b\x04\xc0\
\xf9S\x04\x10\x00\x8b\xcc\xf3A\x00,.\x13\x1a\x02`\
\xf3{f\x08\x80\x85\xe4\xd9\x09\x00FI\x11\x10\x00,\
\x1a1\x17\x00\x8b\x05\xcfV\x00,\x10<c\x01\xb00\
\xf0\xac\x05\xc0\xf9\x10\x11\x10\x00\x8b\x00\xf1\x17\x00\x9b\x1f\
kA\x00<p\xac\x09\x01\xf0\xa0\xb16\x04\xc0y\x0f\
\x11\x10\x00\x0f\x15\xeb\xa5\x1bw\x0f\x13r\xd7\x8d_\x04\
\x82`\x02\x00\x02\x00\x08\x00 \x00\x80\x00\x00\x02\x00\x08\
\x00 \x00\x80\x00\x00\x02\xd0\xbeeY<e\xac\x9d\xe4\
\x09@\x04\xb0f\xc2\x8f\x00\xcb\xb2\x08\x016\x7f\xfa\xf7\
\x00D\x00o\x12\xc1\x01\x10\x01\xac\x89\xf0\x00\x88\x00\xd6\
Bx\x00D\x00k\xc0\xef\x01\xf8\xe6\xa0\xcd/\x00X\
\x0c\x9e\xb7\x00X\x14\x16\x85\x89O\x00,\x10<[\x01\
\xb0P\xf0L\x05\xc0\xa8\x88\xcd/\x00\x16\x0f\x9e\x9f\x00\
XD\x98\xe0\x04@\x04\xf0\xac\x04\xc0\xc2\xc23\x12\x00\
\xa3%6\xbf\x00Xlx\x1e\x02`\xd1a\x22\x13\x00\
\x11\xc0\xbd\x17\x00\x0b\x11\xf7\x5c\x00\x8c\xa2\xd8\xfc\x02`\
q\xe2\xfe\x0a\x80E\x8a\x09K\x00D\x00\xf7R\x00,\
\x5c\xdcC\x010\xbab\xf3\x0b\x80\xc5\x8c\xfb%\x00\x16\
\xb5\xfb\x84\x00X\xdc\x8eK\x08\x80\x08\xb8'\x08\x80w\
;\x9b\x1f\x01\xb0\xf8]?\x02`\x13\x98\x80\x10\x00\x11\
p\xad\x08\xc0\x08\x0b\xad\xe5\xd7\xfb\xa9\xd7f\xf3\x0b\x80\
\x05s\xe2h\x9c\x10\x11\xaf9l\x02x\xe7!\xf4\xfe\
\xe0Z\xbc\xe6Q\xc3t\xd6k\xef\xe1\x9a\xeb\x88\x0fa\
\x94j\xb7x\xcd{\xbfN\xea7\xfbz\xb9\xe6:\xd2\
M\x1dq\xb1m\xb9\xa6\xab\xafy\xeb\xd7\x1b\xe9Y\xec\
Y[=]w\xf9\xf9\x07\xd34=\x94\x91\xad\xe6y\
\x8e|\xc6g]\xf7\xb3\xbf\xf7\x93\xd6u\xfdg\xcf\xdf\
-a\x04=\xf7\xba\xfd\x1e\x00\x04\x13\x00\x10\x00@\x00\
\x00\x01\x00\x04\x00\x10\x00@\x00\x00\x01\x00\x04\x00\x10\x00\
@\x00\x00\x01\x00\x04\x00\x10\x00@\x00\x00\x01\x00\x04\x00\
\x10\x00@\x00\x00\x01\x00\x04\x00\x18=\x00g\xff\x7f\xcf\
\xa1W\xdf\xd8\x1b5\xe5B\xc1\xe6o\xe8\x08 \x02\xf0\
\xfd\xbdPS/\x1c\xd27\xff\xed\xf6\xe4\xb3\x01o\xb7\
\xf3?\x1f\xf0\x15\x9f\x1b\x88M\x7f\xae\xa6?\x1b\xd0D\
0V<=\xcf\xf6\xf91\xe0@\xef*6\x1c\x02 \
\x04n\x02\x9b\xc6\xff\x97\x01x\xf6\x1f\x22\x02\x98\x00p\
$ !\x00\xa6\x00\xd3\x00c\x8f\xff&\x00\x11 \xdc\
\x7f\xdf\xe5\xbf\xf5;\x01\x9c\xe3\xca\x1f\x17\x0aO\xdb\xef\
\xfe\x9b&\x00G\x01\xd3\x00cn~G\x00\x11\xc0\x11\
`\x1bG\x01G\x02\xa1\x19\xeb\xdd\x7f\xd7\x04\xe0(`\
\x1a`\xac\xcd\xbf\xfb\x08 \x02\x22\xc08\x9b\x7f\xd7\x11\
\xc0q\xc0\x91@X\xc6\xda\xfc\xbb'\x00\x93\x80i\x80\
q6\xff\xdb\x01\x10\x01\x11\xa0\xff\xcd\xff\xf6\x11\xc0q\
\xc0\x91@H\xfa\xdf\xfc\x87&\x00\x93\x80i\x80\xbe7\
\xffG&\x00\xd3\x80I@<\xfa\xdb\xf8\xa7\x04@\x08\
\x84@\x00\xfa\xd8\xf8\xa7\x06@\x08D@\x00\xda\xde\xf8\
\x97\x04@\x08\xb2# \x00\xedn\xfcK\x03 \x08\x99\
\x11\x10\x80\xf66|\x13\x01`\xach\xbe\x0a\xc1\xd9\x01\
\xf0\x13\xa8\xe3\xfcs`\xef\x0c\x87y\xa7\x17\x00D@\
\x08\x04\x80+\x22`\x1a@\x00\x84@\x048\xcc7Q\
:\x97\xfc\x13\x15\xdf\x044\x01\x98\x04l\x02L\x00$\
N\x03\xe2g\x02\xc0\x86@\x00\x10\x01\x1c\x01\x88:\x12\
\x88\x9d\x09\x00\x1b\x04\x01@\x04p\x04 \xeaH n\
&\x00l\x18\x04\x00\x11\xc0\x11\x80\xa8#\x81\x98\x99\x00\
\xb0\x81\x10\x00D\x00G\x00\xa2\x8e\x04\xe2e\x02\xc0\x86\
B\x00\x10\x01\x1c\x01\x88:\x12\x88\x95\x09\x00\x1b\x0c\x01\
@\x04p\x04 \xeaH N&\x00l8\x04\x00\x11\
\xc0\x11\x80\xa8#\x81\x18\x99\x00\xb0\x01\x11\x00D\x00G\
\x00\xa2\x8e\x04\xe2c\x02\xc04\x80\x09\x80\xc4i@p\
L\x00\x98\x06\x10\x00D\x00\x01@\x04\xf0=\x002\xbe\
/ .&\x00L\x03\x08\x00\x22\x80#\x00QG\x02\
11\x01`\x1a@\x00\x10\x01\x1c\x01\x88:\x12\x88\x87\
\x00\x10\x1a\x01\x9b_\x00\x08\x8d\x80\xcd\x0fA1\xe8\xfd\
S\x8c\x01\x00\xda\xf2\x07\xc0\xb4\x09d\x9d\x1fRw\x00\
\x00\x00\x00IEND\xaeB`\x82\
"
qt_resource_name = b"\
\x00\x08\
\x0aaZ\xa7\
\x00i\
\x00c\x00o\x00n\x00.\x00p\x00n\x00g\
"
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x847\x9eu\x9f\
"
def qInitResources():
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
qInitResources()

View File

@ -1,63 +0,0 @@
__all__ = [
"MANIM",
"MANIMGL",
"API_NAME",
"Slide",
"ThreeDSlide",
]
import os
import sys
class ManimApiNotFoundError(ImportError):
"""Error raised if specified manim API could be imported."""
_msg = "Could not import the specified manim API"
def __init__(self) -> None:
super().__init__(self._msg)
API_NAMES = {
"manim": "manim",
"manimce": "manim",
"manimlib": "manimlib",
"manimgl": "manimlib",
}
MANIM_API: str = "MANIM_API"
FORCE_MANIM_API: str = "FORCE_" + MANIM_API
API: str = os.environ.get(MANIM_API, "manim").lower()
if API not in API_NAMES:
raise ImportError(
f"Specified MANIM_API={API!r} is not in valid options: " f"{API_NAMES}",
)
API_NAME = API_NAMES[API]
if not os.environ.get(FORCE_MANIM_API):
if "manim" in sys.modules:
API_NAME = "manim"
elif "manimlib" in sys.modules:
API_NAME = "manimlib"
MANIM: bool = API_NAME == "manim"
MANIMGL: bool = API_NAME == "manimlib"
if MANIM:
try:
from .manim import Slide, ThreeDSlide
except ImportError as e:
raise ManimApiNotFoundError from e
elif MANIMGL:
try:
from .manimlib import Slide, ThreeDSlide
except ImportError as e:
raise ManimApiNotFoundError from e
else:
raise ManimApiNotFoundError

View File

@ -1,159 +0,0 @@
"""
Additional animations for Manim objects.
Like with Manim, animations are classes that must be put inside a
:meth:`Scene.play<manim.scene.scene.Scene.play>` call.
For each of the provided classes, there exists a method variant
that directly calls ``self.play(Animation(...))``, see
:class:`Slide<manim_slides.slide.Slide>`.
"""
__all__ = ["Wipe", "Zoom"]
from typing import Any, Mapping, Optional, Sequence
import numpy as np
from . import MANIM
if MANIM:
from manim import LEFT, AnimationGroup, FadeIn, FadeOut
from manim.mobject.mobject import Mobject
else:
from manimlib import LEFT, AnimationGroup, FadeIn, FadeOut
Mobject = Any
class Wipe(AnimationGroup): # type: ignore[misc]
"""
Creates a wipe animation that will shift all the current objects and future objects
by a given value.
:param current: A sequence of mobjects to remove from the scene.
:param future: A sequence of mobjects to add to the scene.
:param shift: The shift vector, used for both fading in and out.
:param fade_in_kwargs: Keyword arguments passed to
:class:`FadeIn<manim.animation.fading.FadeIn>`.
:param fade_out_kwargs: Keyword arguments passed to
:class:`FadeOut<manim.animation.fading.FadeOut>`.
:param kwargs: Keyword arguments passed to
:class:`AnimationGroup<manim.animation.composition.AnimationGroup>`.
Examples
--------
.. manim-slides:: WipeClassExample
from manim import *
from manim_slides import Slide
from manim_slides.slide.animation import Wipe
class WipeClassExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
square = Square()
self.play(FadeIn(circle))
self.next_slide()
self.play(Wipe(circle, square, shift=3 * LEFT))
"""
def __init__(
self,
current: Optional[Sequence[Mobject]] = None,
future: Optional[Sequence[Mobject]] = None,
shift: np.ndarray = LEFT,
fade_in_kwargs: Optional[Mapping[str, Any]] = None,
fade_out_kwargs: Optional[Mapping[str, Any]] = None,
**kwargs: Any,
):
animations = []
if future:
if fade_in_kwargs is None:
fade_in_kwargs = {}
for mobject in future:
animations.append(FadeIn(mobject, shift=shift, **fade_in_kwargs))
if current:
if fade_out_kwargs is None:
fade_out_kwargs = {}
for mobject in current:
animations.append(FadeOut(mobject, shift=shift, **fade_out_kwargs))
super().__init__(*animations, **kwargs)
class Zoom(AnimationGroup): # type: ignore[misc]
"""
Creates a zoom animation that will fade out all the current objects, and fade in all
the future objects. Objects are faded in a direction that goes towards the camera.
:param current: A sequence of mobjects to remove from the scene.
:param future: A sequence of mobjects to add to the scene.
:param scale: How much the objects are scaled (up or down).
:param out: If set, the objects fade in the opposite direction.
:param fade_in_kwargs: Keyword arguments passed to
:class:`FadeIn<manim.animation.fading.FadeIn>`.
:param fade_out_kwargs: Keyword arguments passed to
:class:`FadeOut<manim.animation.fading.FadeOut>`.
:param kwargs: Keyword arguments passed to
:class:`AnimationGroup<manim.animation.composition.AnimationGroup>`.
Examples
--------
.. manim-slides:: ZoomClassExample
from manim import *
from manim_slides import Slide
from manim_slides.slide.animation import Zoom
class ZoomClassExample(Slide):
def construct(self):
circles = [Circle(radius=i) for i in range(1, 4)]
self.play(FadeIn(circles[0]))
self.next_slide()
for i in range(2):
self.play(Zoom(circles[i], circles[i+1]))
self.next_slide()
"""
def __init__(
self,
current: Optional[Sequence[Mobject]] = None,
future: Optional[Sequence[Mobject]] = None,
scale: float = 4.0,
out: bool = False,
fade_in_kwargs: Optional[Mapping[str, Any]] = None,
fade_out_kwargs: Optional[Mapping[str, Any]] = None,
**kwargs: Any,
) -> None:
scale_in = 1.0 / scale
scale_out = scale
if out:
scale_in, scale_out = scale_out, scale_in
animations = []
if future:
if fade_in_kwargs is None:
fade_in_kwargs = {}
for mobject in future:
animations.append(FadeIn(mobject, scale=scale_in, **fade_in_kwargs))
if current:
if fade_out_kwargs is None:
fade_out_kwargs = {}
for mobject in current:
animations.append(FadeOut(mobject, scale=scale_out, **fade_out_kwargs))
super().__init__(*animations, **kwargs)

View File

@ -1,640 +0,0 @@
from __future__ import annotations
__all__ = ["BaseSlide"]
import platform
from abc import abstractmethod
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
MutableMapping,
Sequence,
ValuesView,
)
import numpy as np
from tqdm import tqdm
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
from ..defaults import FOLDER_PATH
from ..logger import logger
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
from . import MANIM
if TYPE_CHECKING:
from .animation import Wipe, Zoom
if MANIM:
from manim.mobject.mobject import Mobject
else:
Mobject = Any
LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
class BaseSlide:
def __init__(
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)
self._output_folder: Path = output_folder
self._slides: list[PreSlideConfig] = []
self._base_slide_config: BaseSlideConfig = BaseSlideConfig()
self._current_slide = 1
self._current_animation = 0
self._start_animation = 0
self._canvas: MutableMapping[str, Mobject] = {}
self._wait_time_between_slides = 0.0
@property
@abstractmethod
def _frame_height(self) -> float:
"""Return the scene's frame height."""
raise NotImplementedError
@property
@abstractmethod
def _frame_width(self) -> float:
"""Return the scene's frame width."""
raise NotImplementedError
@property
@abstractmethod
def _background_color(self) -> str:
"""Return the scene's background color."""
raise NotImplementedError
@property
@abstractmethod
def _resolution(self) -> tuple[int, int]:
"""Return the scene's resolution used during rendering."""
raise NotImplementedError
@property
@abstractmethod
def _partial_movie_files(self) -> list[Path]:
"""Return a list of partial movie files, a.k.a animations."""
raise NotImplementedError
@property
@abstractmethod
def _show_progress_bar(self) -> bool:
"""Return True if progress bar should be displayed."""
raise NotImplementedError
@property
@abstractmethod
def _leave_progress_bar(self) -> bool:
"""Return True if progress bar should be left after completed."""
raise NotImplementedError
@property
@abstractmethod
def _start_at_animation_number(self) -> int | None:
"""If set, return the animation number at which rendering start."""
raise NotImplementedError
@property
def canvas(self) -> MutableMapping[str, Mobject]:
"""
Return the canvas associated to the current slide.
The canvas is a mapping between names and Mobjects,
for objects that are assumed to stay in multiple slides.
For example, a section title or a slide number.
Examples
--------
.. manim-slides:: CanvasExample
from manim import *
from manim_slides import Slide
class CanvasExample(Slide):
def update_canvas(self):
self.counter += 1
old_slide_number = self.canvas["slide_number"]
new_slide_number = Text(f"{self.counter}").move_to(old_slide_number)
self.play(Transform(old_slide_number, new_slide_number))
def construct(self):
title = Text("My Title").to_corner(UL)
self.counter = 1
slide_number = Text("1").to_corner(DL)
self.add_to_canvas(title=title, slide_number=slide_number)
self.play(FadeIn(title), FadeIn(slide_number))
self.next_slide()
circle = Circle(radius=2)
dot = Dot()
self.update_canvas()
self.play(Create(circle))
self.play(MoveAlongPath(dot, circle))
self.next_slide()
self.update_canvas()
square = Square()
self.wipe(self.mobjects_without_canvas, square)
self.next_slide()
self.update_canvas()
self.play(
Transform(
self.canvas["title"],
Text("New Title").to_corner(UL)
)
)
self.next_slide()
self.remove_from_canvas("title", "slide_number")
self.wipe(self.mobjects_without_canvas, [])
"""
return self._canvas
def add_to_canvas(self, **objects: Mobject) -> None:
"""
Add objects to the canvas, using key values as names.
:param objects: A mapping between names and Mobjects.
.. note::
This method does not actually do anything in terms of
animations. You must still call :code:`self.add` or
play some animation that introduces each Mobject for
it to appear. The same applies when removing objects.
"""
self._canvas.update(objects)
def remove_from_canvas(self, *names: str) -> None:
"""Remove objects from the canvas."""
for name in names:
self._canvas.pop(name)
@property
def canvas_mobjects(self) -> ValuesView[Mobject]:
"""Return Mobjects contained in the canvas."""
return self.canvas.values()
@property
def mobjects_without_canvas(self) -> Sequence[Mobject]:
"""
Return the list of objects contained in the scene, minus those present in
the canvas.
"""
return [
mobject
for mobject in self.mobjects # type: ignore[attr-defined]
if mobject not in self.canvas_mobjects
]
@property
def wait_time_between_slides(self) -> float:
r"""
Return the wait duration (in seconds) added between two slides.
By default, this value is set to 0.
Setting this value to something bigger than 0 will result in a
:code:`self.wait` animation called at the end of every slide.
.. note::
This is useful because animations are usually only terminated
when a new animation is played. You can observe the small difference
in the examples below: the circle is not fully complete in the first
slide of the first example, but well in the second example.
Examples
--------
.. manim-slides:: WithoutWaitExample
from manim import *
from manim_slides import Slide
class WithoutWaitExample(Slide):
def construct(self):
circle = Circle(radius=2)
arrow = Arrow().next_to(circle, RIGHT).scale(-1)
text = Text("Small\ngap").next_to(arrow, RIGHT)
self.play(Create(arrow), FadeIn(text))
self.play(Create(circle))
self.next_slide()
self.play(FadeOut(circle))
.. manim-slides:: WithWaitExample
from manim import *
from manim_slides import Slide
class WithWaitExample(Slide):
def construct(self):
self.wait_time_between_slides = 0.1 # A small value > 1 / FPS
circle = Circle(radius=2)
arrow = Arrow().next_to(circle, RIGHT).scale(-1)
text = Text("No more\ngap").next_to(arrow, RIGHT)
self.play(Create(arrow), FadeIn(text))
self.play(Create(circle))
self.next_slide()
self.play(FadeOut(circle))
"""
return self._wait_time_between_slides
@wait_time_between_slides.setter
def wait_time_between_slides(self, wait_time: float) -> None:
self._wait_time_between_slides = max(wait_time, 0.0)
def play(self, *args: Any, **kwargs: Any) -> None:
"""Overload `self.play` and increment animation count."""
super().play(*args, **kwargs) # type: ignore[misc]
self._current_animation += 1
@BaseSlideConfig.wrapper("base_slide_config")
def next_slide(
self,
*,
base_slide_config: BaseSlideConfig,
**kwargs: Any,
) -> None:
"""
Create a new slide with previous animations, and setup options
for the next slide.
This usually means that the user will need to press some key before the
next slide is played. By default, this is the right arrow key.
:param args:
Positional arguments to be passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
:param loop:
If set, next slide will be looping.
:param auto_next:
If set, next slide will play immediately play the next slide
upon terminating.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
:param playback_rate:
Playback rate at which the video is played.
Note that this is only supported by ``manim-slides present``.
:param reversed_playback_rate:
Playback rate at which the reversed video is played.
Note that this is only supported by ``manim-slides present``.
:param notes:
Presenter notes, in Markdown format.
Note that PowerPoint does not support Markdown.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html/pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param kwargs:
Keyword arguments to be passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
.. note::
Calls to :func:`next_slide` at the very beginning or at the end are
not needed, since they are automatically added.
.. warning::
When rendered with RevealJS, loops cannot be in the first nor
the last slide.
.. seealso::
When using ``manim`` API, this method will also call
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`.
Examples
--------
The following contains 3 slides:
#. the first with nothing on it;
#. the second with "Hello World!" fading in;
#. and the last with the text fading out;
.. manim-slides:: NextSlideExample
from manim import *
from manim_slides import Slide
class NextSlideExample(Slide):
def construct(self):
text = Text("Hello World!")
self.play(FadeIn(text))
self.next_slide()
self.play(FadeOut(text))
The following contains one slide that will loop endlessly.
.. manim-slides:: LoopExample
from manim import *
from manim_slides import Slide
class LoopExample(Slide):
def construct(self):
dot = Dot(color=BLUE, radius=1)
self.play(FadeIn(dot))
self.next_slide(loop=True)
self.play(Indicate(dot, scale_factor=2))
self.next_slide()
self.play(FadeOut(dot))
The following contains one slide that triggers the next slide
upon terminating.
.. manim-slides:: AutoNextExample
from manim import *
from manim_slides import Slide
class AutoNextExample(Slide):
def construct(self):
square = Square(color=RED, side_length=2)
self.play(GrowFromCenter(square))
self.next_slide(auto_next=True)
self.play(Wiggle(square))
self.next_slide()
self.wipe(square)
The following contains speaker notes. On the webbrowser,
the speaker view can be triggered by pressing :kbd:`S`.
.. manim-slides:: SpeakerNotesExample
from manim import *
from manim_slides import Slide
class SpeakerNotesExample(Slide):
def construct(self):
self.next_slide(notes="Some introduction")
square = Square(color=GREEN, side_length=2)
self.play(GrowFromCenter(square))
self.next_slide(notes="We now rotate the slide")
self.play(Rotate(square, PI / 2))
self.next_slide(notes="Bye bye")
self.zoom(square)
"""
if self._current_animation > self._start_animation:
if self.wait_time_between_slides > 0.0:
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
self._slides.append(
PreSlideConfig.from_base_slide_config_and_animation_indices(
self._base_slide_config,
self._start_animation,
self._current_animation,
)
)
self._current_slide += 1
self._base_slide_config = base_slide_config
self._start_animation = self._current_animation
def _add_last_slide(self) -> None:
"""Add a 'last' slide to the end of slides."""
if (
len(self._slides) > 0
and self._current_animation == self._slides[-1].end_animation
):
return
self._slides.append(
PreSlideConfig.from_base_slide_config_and_animation_indices(
self._base_slide_config,
self._start_animation,
self._current_animation,
)
)
def _save_slides(self, use_cache: bool = True) -> None:
"""
Save slides, optionally using cached files.
Note that cached files only work with Manim.
"""
self._add_last_slide()
files_folder = self._output_folder / "files"
scene_name = str(self)
scene_files_folder = files_folder / scene_name
scene_files_folder.mkdir(parents=True, exist_ok=True)
files: list[Path] = self._partial_movie_files
# We must filter slides that end before the animation offset
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 = max(0, slide.start_animation - offset)
slide.end_animation -= offset
slides: list[SlideConfig] = []
for pre_slide_config in tqdm(
self._slides,
desc=f"Concatenating animation files to '{scene_files_folder}' and generating reversed animations",
leave=self._leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self._show_progress_bar,
):
slide_files = files[pre_slide_config.slides_slice]
file = merge_basenames(slide_files)
dst_file = scene_files_folder / file.name
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
# We only concat animations if it was not present
if not use_cache or not dst_file.exists():
concatenate_video_files(slide_files, dst_file)
# We only reverse video if it was not present
if not use_cache or not rev_file.exists():
reverse_video_file(dst_file, rev_file)
slides.append(
SlideConfig.from_pre_slide_config_and_files(
pre_slide_config, dst_file, rev_file
)
)
logger.info(
f"Generated {len(slides)} slides to '{scene_files_folder.absolute()}'"
)
slide_path = self._output_folder / f"{scene_name}.json"
PresentationConfig(
slides=slides,
resolution=self._resolution,
background_color=self._background_color,
).to_file(slide_path)
logger.info(
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
)
def wipe(
self,
*args: Any,
direction: np.ndarray = LEFT,
return_animation: bool = False,
**kwargs: Any,
) -> Wipe | None:
"""
Play a wipe animation that will shift all the current objects outside of the
current scene's scope, and all the future objects inside.
:param args: Positional arguments passed to
:class:`Wipe<manim_slides.slide.animation.Wipe>`.
:param direction: The wipe direction, that will be scaled by the scene size.
:param return_animation: If set, return the animation instead of
playing it. This is useful to combine multiple animations with this one.
:param kwargs: Keyword arguments passed to
:class:`Wipe<manim_slides.slide.animation.Wipe>`.
Examples
--------
.. manim-slides:: WipeExample
from manim import *
from manim_slides import Slide
class WipeExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
square = Square()
text = Text("This is a wipe example").next_to(square, DOWN)
beautiful = Text("Beautiful, no?")
self.play(FadeIn(circle))
self.next_slide()
self.wipe(circle, Group(square, text))
self.next_slide()
self.wipe(Group(square, text), beautiful, direction=UP)
self.next_slide()
anim = self.wipe(
beautiful,
circle,
direction=DOWN + RIGHT,
return_animation=True
)
self.play(anim)
"""
from .animation import Wipe
shift_amount = np.asarray(direction) * np.array(
[self._frame_width, self._frame_height, 0.0]
)
kwargs.setdefault("shift", shift_amount)
animation = Wipe(
*args,
**kwargs,
)
if return_animation:
return animation
self.play(animation)
return None
def zoom(
self,
*args: Any,
return_animation: bool = False,
**kwargs: Any,
) -> Zoom | None:
"""
Play a zoom animation that will fade out all the current objects, and fade in
all the future objects. Objects are faded in a direction that goes towards the
camera.
:param args: Positional arguments passed to
:class:`Zoom<manim_slides.slide.animation.Zoom>`.
:param return_animation: If set, return the animation instead of
playing it. This is useful to combine multiple animations with this one.
:param kwargs: Keyword arguments passed to
:class:`Zoom<manim_slides.slide.animation.Zoom>`.
Examples
--------
.. manim-slides:: ZoomExample
from manim import *
from manim_slides import Slide
class ZoomExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
square = Square()
self.play(FadeIn(circle))
self.next_slide()
self.zoom(circle, square)
self.next_slide()
anim = self.zoom(
square,
circle,
out=True,
scale=10.0,
return_animation=True
)
self.play(anim)
"""
from .animation import Zoom
animation = Zoom(*args, **kwargs)
if return_animation:
return animation
self.play(animation)
return None

View File

@ -1,149 +0,0 @@
from pathlib import Path
from typing import Any, List, Optional, Tuple
from manim import Scene, ThreeDScene, config
from ..config import BaseSlideConfig
from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
"""
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
for slides rendering.
"""
@property
def _frame_height(self) -> float:
return config["frame_height"] # type: ignore
@property
def _frame_width(self) -> float:
return config["frame_width"] # type: ignore
@property
def _background_color(self) -> str:
color = config["background_color"]
if hex_color := getattr(color, "hex", None):
return hex_color # type: ignore
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
return color.to_hex() # type: ignore
@property
def _resolution(self) -> Tuple[int, int]:
return config["pixel_width"], config["pixel_height"]
@property
def _partial_movie_files(self) -> List[Path]:
# When rendering with -na,b (manim only)
# the animations not in [a,b] will be skipped,
# but animation before a will have a None source file.
return [
Path(file)
for file in self.renderer.file_writer.partial_movie_files
if file is not None
]
@property
def _show_progress_bar(self) -> bool:
return config["progress_bar"] != "none" # type: ignore
@property
def _leave_progress_bar(self) -> bool:
return config["progress_bar"] == "leave" # type: ignore
@property
def _start_at_animation_number(self) -> Optional[int]:
return config["from_animation_number"] # type: ignore
def next_section(self, *args: Any, **kwargs: Any) -> None:
"""
Alias to :meth:`next_slide`.
:param args:
Positional arguments to be passed to :meth:`next_slide`.
:param kwargs:
Keyword arguments to be passed to :meth:`next_slide`.
.. attention::
This method is only available when using ``manim`` API.
"""
self.next_slide(*args, **kwargs)
@BaseSlideConfig.wrapper("base_slide_config")
def next_slide(
self,
*args: Any,
base_slide_config: BaseSlideConfig,
**kwargs: Any,
) -> None:
Scene.next_section(self, *args, **kwargs)
BaseSlide.next_slide.__wrapped__(
self,
base_slide_config=base_slide_config,
)
def render(self, *args: Any, **kwargs: Any) -> None:
"""MANIM render."""
# 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")
super().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
self._save_slides()
class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]
"""
Inherits from :class:`Slide` and
:class:`ThreeDScene<manim.scene.three_d_scene.ThreeDScene>` and provide necessary
tools for slides rendering.
Examples
--------
.. manim-slides:: ThreeDExample
from manim import *
from manim_slides import ThreeDSlide
class ThreeDExample(ThreeDSlide):
def construct(self):
title = Text("A 2D Text")
self.play(FadeIn(title))
self.next_slide()
sphere = Sphere([0, 0, -3])
self.move_camera(phi=PI/3, theta=-PI/4, distance=7)
self.play(
GrowFromCenter(sphere),
Transform(title, Text("A 3D Text"))
)
self.next_slide()
bye = Text("Bye!")
self.next_slide(loop=True)
self.wipe(
self.mobjects_without_canvas,
[bye],
direction=UP
)
self.wait(.5)
self.wipe(
self.mobjects_without_canvas,
[title, sphere],
direction=DOWN
)
self.wait(.5)
self.next_slide()
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
"""
pass

View File

@ -1,72 +0,0 @@
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Tuple
from manimlib import Scene, ThreeDCamera
from manimlib.utils.file_ops import get_sorted_integer_files
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,
)
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
@property
def _frame_width(self) -> float:
return self.camera.frame.get_width() # type: ignore
@property
def _background_color(self) -> str:
return self.camera_config["background_color"].hex # type: ignore
@property
def _resolution(self) -> Tuple[int, int]:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
@property
def _partial_movie_files(self) -> List[Path]:
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return [
Path(file)
for file in get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
]
@property
def _show_progress_bar(self) -> bool:
return True
@property
def _leave_progress_bar(self) -> bool:
return self.leave_progress_bars # type: ignore
@property
def _start_at_animation_number(self) -> Optional[int]:
return self.start_at_animation_number # type: ignore
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer."""
super().run(*args, **kwargs)
self._save_slides(use_cache=False)
class ThreeDSlide(Slide):
CONFIG: ClassVar[Dict[str, Any]] = {
"camera_class": ThreeDCamera,
}
pass

View File

@ -1,343 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{{ title }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/theme/{{ reveal_theme }}.min.css">
<!-- Theme used for syntax highlighting of code -->
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
<!-- <link rel="stylesheet" href="index.css"> -->
</head>
<body>
<div class="reveal">
<div class="slides">
{%- 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 %}
{%- 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>
{%- endfor -%}
{%- endfor -%}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}
<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealMarkdown, RevealNotes ],
{%- endif %}
// The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different
// resolutions. Can be specified using percentage units.
width: {{ width }},
height: {{ height }},
// Factor of the display size that should remain empty around
// the content
margin: {{ margin }},
// Bounds for smallest/largest possible scale to apply to content
minScale: {{ min_scale }},
maxScale: {{ max_scale }},
// Display presentation control arrows
controls: {{ controls }},
// Help the user learn the controls by providing hints, for example by
// bouncing the down arrow when they first encounter a vertical slide
controlsTutorial: {{ controls_tutorial }},
// Determines where controls appear, "edges" or "bottom-right"
controlsLayout: {{ controls_layout }},
// Visibility rule for backwards navigation arrows; "faded", "hidden"
// or "visible"
controlsBackArrows: {{ controls_back_arrows }},
// Display a presentation progress bar
progress: {{ progress }},
// Display the page number of the current slide
// - true: Show slide number
// - false: Hide slide number
//
// Can optionally be set as a string that specifies the number formatting:
// - "h.v": Horizontal . vertical slide number (default)
// - "h/v": Horizontal / vertical slide number
// - "c": Flattened slide number
// - "c/t": Flattened slide number / total slides
//
// Alternatively, you can provide a function that returns the slide
// number for the current slide. The function should take in a slide
// object and return an array with one string [slideNumber] or
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
slideNumber: {{ slide_number }},
// Can be used to limit the contexts in which the slide number appears
// - "all": Always show the slide number
// - "print": Only when printing to PDF
// - "speaker": Only in the speaker view
showSlideNumber: {{ show_slide_number }},
// Use 1 based indexing for # links to match slide number (default is zero
// based)
hashOneBasedIndex: {{ hash_one_based_index }},
// Add the current slide number to the URL hash so that reloading the
// page/copying the URL will return you to the same slide
hash: {{ hash }},
// Flags if we should monitor the hash and change slides accordingly
respondToHashChanges: {{ respond_to_hash_changes }},
// Push each slide change to the browser history. Implies `hash: true`
history: {{ history }},
// Enable keyboard shortcuts for navigation
keyboard: {{ keyboard }},
// Optional function that blocks keyboard events when retuning false
//
// If you set this to 'focused', we will only capture keyboard events
// for embedded decks when they are in focus
keyboardCondition: {{ keyboard_condition }},
// Disables the default reveal.js slide layout (scaling and centering)
// so that you can use custom CSS layout
disableLayout: {{ disable_layout }},
// Enable the slide overview mode
overview: {{ overview }},
// Vertical centering of slides
center: {{ center }},
// Enables touch navigation on devices with touch input
touch: {{ touch }},
// Loop the presentation
loop: {{ loop }},
// Change the presentation direction to be RTL
rtl: {{ rtl }},
// Changes the behavior of our navigation directions.
//
// "default"
// Left/right arrow keys step between horizontal slides, up/down
// arrow keys step between vertical slides. Space key steps through
// all slides (both horizontal and vertical).
//
// "linear"
// Removes the up/down arrows. Left/right arrows step through all
// slides (both horizontal and vertical).
//
// "grid"
// When this is enabled, stepping left/right from a vertical stack
// to an adjacent vertical stack will land you at the same vertical
// index.
//
// Consider a deck with six slides ordered in two vertical stacks:
// 1.1 2.1
// 1.2 2.2
// 1.3 2.3
//
// If you're on slide 1.3 and navigate right, you will normally move
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
// from 1.3 -> 2.3.
navigationMode: {{ navigation_mode }},
// Randomizes the order of slides each time the presentation loads
shuffle: {{ shuffle }},
// Turns fragments on and off globally
fragments: {{ fragments }},
// Flags whether to include the current fragment in the URL,
// so that reloading brings you to the same fragment position
fragmentInURL: {{ fragment_in_url }},
// Flags if the presentation is running in an embedded mode,
// i.e. contained within a limited portion of the screen
embedded: {{ embedded }},
// Flags if we should show a help overlay when the question-mark
// key is pressed
help: {{ help }},
// Flags if it should be possible to pause the presentation (blackout)
pause: {{ pause }},
// Flags if speaker notes should be visible to all viewers
showNotes: {{ show_notes }},
// Global override for autolaying embedded media (video/audio/iframe)
// - null: Media will only autoplay if data-autoplay is present
// - true: All media will autoplay, regardless of individual setting
// - false: No media will autoplay, regardless of individual setting
autoPlayMedia: {{ auto_play_media }},
// Global override for preloading lazy-loaded iframes
// - null: Iframes with data-src AND data-preload will be loaded when within
// the viewDistance, iframes with only data-src will be loaded when visible
// - true: All iframes with data-src will be loaded when within the viewDistance
// - false: All iframes with data-src will be loaded only when visible
preloadIframes: {{ preload_iframes }},
// Can be used to globally disable auto-animation
autoAnimate: {{ auto_animate }},
// Optionally provide a custom element matcher that will be
// used to dictate which elements we can animate between.
autoAnimateMatcher: {{ auto_animate_matcher }},
// Default settings for our auto-animate transitions, can be
// overridden per-slide or per-element via data arguments
autoAnimateEasing: {{ auto_animate_easing }},
autoAnimateDuration: {{ auto_animate_duration }},
autoAnimateUnmatched: {{ auto_animate_unmatched }},
// CSS properties that can be auto-animated. Position & scale
// is matched separately so there's no need to include styles
// like top/right/bottom/left, width/height or margin.
autoAnimateStyles: {{ auto_animate_styles }},
// Controls automatic progression to the next slide
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
// is present on the current slide or fragment
// - 1+: All slides will progress automatically at the given interval
// - false: No auto-sliding, even if data-autoslide is present
autoSlide: {{ auto_slide }},
// Stop auto-sliding after user input
autoSlideStoppable: {{ auto_slide_stoppable }},
// Use this method for navigation when auto-sliding (defaults to navigateNext)
autoSlideMethod: {{ auto_slide_method }},
// Specify the average time in seconds that you think you will spend
// presenting each slide. This is used to show a pacing timer in the
// speaker view
defaultTiming: {{ default_timing }},
// Enable slide navigation via mouse wheel
mouseWheel: {{ mouse_wheel }},
// Opens links in an iframe preview overlay
// Add `data-preview-link` and `data-preview-link="false"` to customise each link
// individually
previewLinks: {{ preview_links }},
// Exposes the reveal.js API through window.postMessage
postMessage: {{ post_message }},
// Dispatches all reveal.js events to the parent window through postMessage
postMessageEvents: {{ post_message_events }},
// Focuses body when page changes visibility to ensure keyboard shortcuts work
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
// Transition style
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
// Transition speed
transitionSpeed: {{ transition_speed }}, // default/fast/slow
// Transition style for full page slide backgrounds
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
// The maximum number of pages a single slide can expand onto when printing
// to PDF, unlimited by default
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
// Prints each fragment on a separate slide
pdfSeparateFragments: {{ pdf_separate_fragments }},
// Offset used to reduce the height of content within exported PDF pages.
// This exists to account for environment differences based on how you
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
// on precisely the total height of the document whereas in-browser
// printing has to end one pixel before.
pdfPageHeightOffset: {{ pdf_page_height_offset }},
// Number of slides away from the current that are visible
viewDistance: {{ view_distance }},
// Number of slides away from the current that are visible on mobile
// devices. It is advisable to set this to a lower number than
// viewDistance in order to save resources.
mobileViewDistance: {{ mobile_view_distance }},
// The display mode that will be used to show slides
display: {{ display }},
// Hide cursor if inactive
hideInactiveCursor: {{ hide_inactive_cursor }},
// Time before the cursor is hidden (in ms)
hideCursorTime: {{ hide_cursor_time }}
});
{% if data_uri %}
// Fix found by @t-fritsch on GitHub
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-6651475.
function fixBase64VideoBackground(event) {
// event.previousSlide, event.currentSlide, event.indexh, event.indexv
if (event.currentSlide.getAttribute('data-background-video')) {
const background = Reveal.getSlideBackground(event.indexh, event.indexv),
video = background.querySelector('video'),
sources = video.querySelectorAll('source');
sources.forEach((source, i) => {
const src = source.getAttribute('src');
if(src.match(/^data:video.*;base64$/)) {
const nextSrc = sources[i+1]?.getAttribute('src');
video.setAttribute('src', `${src},${nextSrc}`);
}
});
}
}
Reveal.on( 'ready', fixBase64VideoBackground );
Reveal.on( 'slidechanged', fixBase64VideoBackground );
{% endif %}
</script>
</body>
</html>

View File

@ -1,98 +0,0 @@
import hashlib
import tempfile
from pathlib import Path
from typing import List
import av
from .logger import logger
def concatenate_video_files(files: List[Path], dest: Path) -> None:
"""Concatenate multiple video files into one."""
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
f.writelines(f"file '{path.absolute()}'\n" for path in files)
f.close()
input_ = av.open(f.name, options={"safe": "0"}, format="concat")
input_stream = input_.streams.video[0]
output = av.open(str(dest), mode="w")
output_stream = output.add_stream(
template=input_stream,
)
for packet in input_.demux(input_stream):
# We need to skip the "flushing" packets that `demux` generates.
if packet.dts is None:
continue
# We need to assign the packet to the new stream.
packet.stream = output_stream
output.mux(packet)
input_.close()
output.close()
def merge_basenames(files: List[Path]) -> Path:
"""Merge multiple filenames by concatenating basenames."""
if len(files) == 0:
raise ValueError("Cannot merge an empty list of files!")
dirname: Path = files[0].parent
ext = files[0].suffix
basenames = list(file.stem for file in files)
basenames_str = ",".join(f"{len(b)}:{b}" for b in basenames)
# We use hashes to prevent too-long filenames, see issue #123:
# https://github.com/jeertmans/manim-slides/issues/123
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
logger.debug(f"Generated a new basename for basenames: {basenames} -> '{basename}'")
return dirname.joinpath(basename + ext)
def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
"""Code from https://github.com/PyAV-Org/PyAV/issues/239."""
for c, n in zip(nodes, nodes[1:]):
c.link_to(n)
def reverse_video_file(src: Path, dest: Path) -> None:
"""Reverses a video file, writting the result to `dest`."""
input_ = av.open(str(src))
input_stream = input_.streams.video[0]
output = av.open(str(dest), mode="w")
output_stream = output.add_stream(codec_name="libx264", rate=input_stream.base_rate)
output_stream.width = input_stream.width
output_stream.height = input_stream.height
output_stream.pix_fmt = input_stream.pix_fmt
graph = av.filter.Graph()
link_nodes(
graph.add_buffer(template=input_stream),
graph.add("reverse"),
graph.add("buffersink"),
)
graph.configure()
frames_count = 0
for frame in input_.decode(video=0):
graph.push(frame)
frames_count += 1
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
for _ in range(frames_count):
frame = graph.pull()
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
output.mux(output_stream.encode(frame))
for packet in output_stream.encode():
output.mux(packet)
input_.close()
output.close()

View File

@ -1,85 +0,0 @@
import sys
from pathlib import Path
import click
from ..commons import config_options, verbosity_option
from ..config import Config
from ..defaults import CONFIG_PATH
from ..logger import logger
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Initialize a new default configuration file."""
return _init(config_path, force, merge, skip_interactive=True)
def _init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""
Actual initialization code for configuration file, with optional interactive
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
choice = click.prompt(
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
type=click.Choice(["o", "m", "q"], case_sensitive=False),
)
force = choice == "o"
merge = choice == "m"
if not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
config = Config.from_file(config_path)
from ..qt_utils import qapp
from .wizard import Wizard
app = qapp()
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
if window.closed_without_saving:
sys.exit(0)
config = window.config
if merge:
config = Config.from_file(config_path).merge_with(config)
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")

View File

@ -1,121 +0,0 @@
from functools import partial
from typing import Any
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon, QKeyEvent
from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
QGridLayout,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from ..config import Config, Key
from ..logger import logger
from ..resources import * # noqa: F403
WINDOW_NAME: str = "Configuration Wizard"
keymap = {}
for key in Qt.Key:
keymap[key.value] = key.name.partition("_")[2]
class KeyInput(QDialog): # type: ignore
def __init__(self) -> None:
super().__init__()
self.key = None
self.layout = QVBoxLayout()
self.setWindowTitle("Keyboard Input")
self.label = QLabel("Press any key to register it")
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
self.key = event.key()
self.deleteLater()
event.accept()
class Wizard(QWidget): # type: ignore
def __init__(self, config: Config):
super().__init__()
self.setWindowTitle(WINDOW_NAME)
self.config = config
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.closed_without_saving = False
button = QDialogButtonBox.Save | QDialogButtonBox.Cancel
self.button_box = QDialogButtonBox(button)
self.button_box.accepted.connect(self.save_config)
self.button_box.rejected.connect(self.close_without_saving)
self.buttons = []
self.layout = QGridLayout()
for i, (key, value) in enumerate(self.config.keys.dict().items()):
# Create label for key name information
label = QLabel()
key_info = value["name"] or key
label.setText(key_info.title())
self.layout.addWidget(label, i, 0)
# Create button that will pop-up a dialog and ask to input a new key
value = value["ids"].pop()
button = QPushButton(keymap[value])
button.setToolTip(
f"Click to modify the key associated to action {key_info}"
)
self.buttons.append(button)
button.clicked.connect(
partial(self.open_dialog, i, getattr(self.config.keys, key))
)
self.layout.addWidget(button, i, 1)
self.layout.addWidget(self.button_box, len(self.buttons), 1)
self.setLayout(self.layout)
def close_without_saving(self) -> None:
logger.debug("Closing configuration wizard without saving")
self.closed_without_saving = True
self.deleteLater()
def closeEvent(self, event: Any) -> None: # noqa: N802
self.close_without_saving()
event.accept()
def save_config(self) -> None:
try:
Config.model_validate(self.config.dict())
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Error")
msg.setInformativeText(
"Two or more actions share a common key: make sure actions have distinct key codes."
)
msg.setWindowTitle("Error: duplicated keys")
msg.exec()
return
self.deleteLater()
def open_dialog(self, button_number: int, key: Key) -> None:
button = self.buttons[button_number]
dialog = KeyInput()
dialog.exec()
if dialog.key is not None:
key_name = keymap[dialog.key]
key.set_ids(dialog.key)
button.setText(key_name)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

View File

@ -1,53 +0,0 @@
@online{manim-announcement,
author = {Grant Sanderson},
title = {{Q}\&{A} with {G}rant {S}anderson (3blue1brown)},
year = {2018},
organization = {YouTube},
url = {https://www.youtube.com/watch?v=Qe6o9j4IjTo\&ab_channel=3Blue1Brown}
}
@misc{revealjs,
author = {Hakim El Hattab},
title = {The HTML Presentation Framework},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/hakimel/reveal.js}
}
@misc{manim-presentation,
author = {Federico Galatolo},
title = {Tool for live presentations using manim},
year = {2021},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/galatolofederico/manim-presentation}
}
@misc{manimgl,
author = {Grant Sanderson},
title = {Animation engine for explanatory math videos},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/3b1b/manim}
}
@misc{manim-editor,
author = {Christopher Besch},
title = {Web Presenter for Mathematical Animations using Manim},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
url = {https://github.com/ManimCommunity/manim_editor}
}
@software{manimce,
author = {{The Manim Community Developers}},
license = {MIT},
month = {12},
title = {{Manim Mathematical Animation Framework}},
url = {https://www.manim.community/},
version = {v0.17.2},
year = {2022}
}

View File

@ -1,174 +0,0 @@
---
title: 'Manim Slides: A Python package for presenting Manim content anywhere'
tags:
- Python
- manim
- animations
- teaching
- conference presentations
- tool
authors:
- name: Jérome Eertmans
orcid: 0000-0002-5579-5360
affiliation: 1
affiliations:
- name: ICTEAM, UCLouvain, Belgium
index: 1
date: 2 March 2023
bibliography: paper.bib
---
# Summary
Manim Slides is a Python package that makes presenting Manim animations
straightforward. With minimal changes required to pre-existing code, one can
slide through animations in a *PowerPoint-like* manner, or share its slides
*online* using ReavealJS' power.
# Introduction
Presenting educational content has always been a difficult task, especially
when it uses temporal or iterative concepts. During the last decades, the
presence of computers in classrooms for educational purposes has increased
enormously, allowing teachers to show animated or interactive content.
With the democratization of YouTube, many people have decided to use this
platform to share educational content. Among these people, Grant Sanderson, a
YouTuber presenting videos on the theme of mathematics, quickly became known
for his original and quality animations. In 2018, Grant announced in a video
that he creates his animations using a self-developed Python tool called Manim
[@manim-announcement]. In 2019, he made the Manim source code public [@manimgl],
so anyone can use his tool. Very quickly, the community came together and, in
2020, created a "fork" of the original GitHub repository [@manimce], in order to
create a more accessible and better documented version of this tool. Since then,
the two versions are differentiated by using ManimGL for Grant Sanderson's
version, as it uses OpenGL for rendering, and ManimCE for the community edition
(CE).
Despite the many advantages of the Manim tool in terms of illustrating
mathematical concepts, one cannot help but notice that most presentations,
whether in the classroom or at a conference, are mainly done with PowerPoint
or PDF slides. One of the many advantages of these formats, as opposed to videos
created with Manim, is the ability to pause, rewind, etc., whenever you want.
To face this problem, in 2021, the manim-presentation tool was created
[@manim-presentation]. This tool allows you to present Manim animations as you
would present slides: with pauses, slide jumps, etc. However, this tool has
evolved very little since its inception and does not work with ManimGL.
In 2022, Manim Slides has been created from manim-presentation, with the aim
to make it a more complete tool, better documented, and usable on all platforms
and with ManimCE or ManimGL. After almost a year of existence, Manim Slides has
evolved a lot (see
[comparison section](#comparison-with-manim-presentation)),
has built a small community of contributors, and continues to
provide new features on a regular basis.
# Easy to Use Commitment
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 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://jeertmans.github.io/manim-slides/), see
\autoref{fig:docs}.
![Manim Slides' documentation homepage.\label{fig:docs}](docs.png)
# Example usage
We have used manim-presentation for our presentation at the COST
Interact, hosted in Lyon, 2022, and
[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://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
[video tutorial](https://www.youtube.com/watch?v=Oc9g89VzKsY&ab_channel=TheoremofBeethoven)
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**. 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
documention fixes, and the command-line tool automatically notifies for new
available updates. Therefore, regularly updating Manim Slides is highly
recommended.
# Statement of Need
Similar tools to Manim Slides also exist, such as its predecessor,
manim-presentation [@manim-presentation], or the web-based alternative, Manim
Editor [@manim-editor], but none of them provide the documentation level nor the
amount of features that Manim Slides strives to. This work makes the task of
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.
## Comparison with manim-presentation
Starting from @manim-presentation's original work, Manim Slides now provides
numerous additional features.
A non-exhaustive list of those new features is as follows:
* ManimGL compatibility;
* playing slides in reverse;
* exporting slides to HTML and PowerPoint;
* 3D scene support;
* multiple key inputs can map to the same action
(e.g., useful when using a pointer);
* optionally hiding mouse cursor when presenting;
* recording your presentation;
* multiple video scaling methods (for speed-vs-quality tradeoff);
* and automatic detection of some scene parameters
(e.g., resolution or background color).
The complete and up-to-date set of features Manim Slide supports is
available in the
[online documentation](https://jeertmans.github.io/manim-slides/).
For new feature requests, we highly encourage users to
[create an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
with the appropriate template.
# 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 his tremendous work on Manim, as
well as the Manim Community contributors.
Finally, we also acknowledge contributions from the GitHub contributors on the
Manim Slides repository.
# References

3453
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,147 +0,0 @@
[build-system]
build-backend = "pdm.backend"
requires = ["pdm-backend", "setuptools"]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering",
]
dependencies = [
"av>=9.0.0",
"click>=8.1.3",
"click-default-group>=1.2.2",
"jinja2>=3.1.2",
"lxml>=4.9.2",
"numpy>=1.19",
"opencv-python>=4.6.0.66",
"pillow>=9.5.0",
"pydantic>=2.0.1",
"pydantic-extra-types>=2.0.0",
"python-pptx>=0.6.21",
"qtpy>=2.4.1",
"requests>=2.28.1",
"rich>=13.3.2",
"rtoml>=0.9.0",
"tqdm>=4.64.1",
]
description = "Tool for live presentations using manim"
dynamic = ["version"]
keywords = ["manim", "slides", "plugin", "manimgl"]
license = {text = "MIT"}
name = "manim-slides"
readme = "README.md"
requires-python = ">=3.9,<3.13"
[project.optional-dependencies]
docs = [
"manim-slides[magic,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"sphinx>=7.0.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
]
full = [
"manim-slides[magic,manim,manimgl,sphinx-directive]",
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.6.1"]
pyqt6 = ["pyqt6>=6.6.1"]
pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
[project.scripts]
manim-slides = "manim_slides.__main__:cli"
[project.urls]
Changelog = "https://github.com/jeertmans/manim-slides/releases"
Documentation = "https://eertmans.be/manim-slides"
Founding = "https://github.com/sponsors/jeertmans"
Homepage = "https://github.com/jeertmans/manim-slides"
Repository = "https://github.com/jeertmans/manim-slides"
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
precision = 2
[tool.mypy]
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
strict = true
[tool.pdm.dev-dependencies]
dev = [
"bump2version>=1.0.1",
"pre-commit>=3.5.0",
]
github-action = ["setuptools"]
test = [
"manim-slides[manim,manimgl,pyqt6]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
"pytest-xdist>=3.3.1",
]
[tool.pdm.resolution.overrides]
manimpango = "<1.0.0,>=0.5.0" # This conflicts with ManimGL, hopefully not an issue
skia-pathops = "0.8.0.post1" # From manim 0.18.0 (Python 3.12 support)
[tool.pdm.version]
path = "manim_slides/__version__.py"
source = "file"
[tool.pytest.ini_options]
env = [
"QT_QPA_PLATFORM=offscreen",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
]
[tool.ruff]
extend-exclude = ["manim_slides/resources.py"]
extend-ignore = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D203",
"D205",
"D212",
"E501",
]
extend-include = ["*.ipynb"]
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ["manim_slides", "tests"]}
line-length = 88
target-version = "py38"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

@ -1,53 +0,0 @@
# flake8: noqa: F403, F405
# type: ignore
import os
from manim import *
THEME = os.environ.get("MANIM_SLIDES_THEME", "light").lower().replace("_", "-")
class ManimSlidesLogo(Scene):
def construct(self):
tex_template = TexTemplate()
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
self.camera.background_color = {
"light": "#ffffff",
"dark-docs": "#131416",
"dark-github": "#0d1117",
}[THEME]
logo_green = "#87c2a5"
logo_blue = "#525893"
logo_red = "#e07a5f"
logo_black = {
"light": "#343434",
"dark-docs": "#d0d0d0",
"dark-github": "#c9d1d9",
}[THEME]
ds_m = MathTex(r"\mathbb{M}", fill_color=logo_black).scale(7)
ds_m.shift(2.25 * LEFT + 1.5 * UP)
slides = MathTex(r"\mathbb{S}\text{lides}", fill_color=logo_black).scale(4)
slides.next_to(ds_m, DOWN)
slides.shift(DOWN)
play = Tex(
r"\faStepBackward\faStepForward",
fill_color=logo_black,
tex_template=tex_template,
).scale(4)
play.next_to(ds_m, LEFT)
play.shift(LEFT + 0.5 * DOWN)
comment = Tex(
r"\reflectbox{\faComment*[regular]}",
fill_color=logo_black,
tex_template=tex_template,
).scale(9)
comment.move_to(play)
comment.shift(0.4 * DOWN)
circle = Circle(color=logo_green, fill_opacity=1).shift(LEFT)
square = Square(color=logo_blue, fill_opacity=1).shift(UP)
triangle = Triangle(color=logo_red, fill_opacity=1).shift(RIGHT)
logo = VGroup(
triangle, square, circle, ds_m, slides, comment, play
) # order matters
logo.move_to(ORIGIN)
self.add(logo)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Some files were not shown because too many files have changed in this diff Show More