mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-19 03:26:17 +08:00
Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
422e355758 | |||
3eb9fa0b74 | |||
8f519ed134 | |||
916e2aa2ab | |||
4d5f664348 | |||
cb6a5bb35f | |||
bba05cce16 | |||
ad02c8296b | |||
0778cebef7 | |||
163260415b | |||
241419a781 | |||
bac21815b2 | |||
2f8f7561a6 | |||
a489dfd0e8 | |||
76ef16d98b | |||
88125bf1ae | |||
cffc4ebbc5 | |||
d717bc651d | |||
bc3d55fce2 | |||
5b9cb1523c | |||
51a87840ce | |||
42550e8b29 | |||
501813483c | |||
0ae99c0f4d | |||
c2315928bd | |||
f3c8f3cc24 | |||
14a266b139 | |||
b697442fc0 | |||
4f8fae75cf | |||
d6ec0d3da9 | |||
546451e019 | |||
2457ca8a05 | |||
9900b3123e | |||
ee92e0aa88 | |||
cbee6320f5 | |||
382084f9ef | |||
068484b828 | |||
91f8d97acf | |||
49cdedc6fe | |||
fe1fa059f6 | |||
3f6d2e5e57 | |||
99ad798155 | |||
84c25f1ed5 | |||
7fb3fa01dd | |||
2d2a225afe | |||
b9d2cd92b5 | |||
620bb30960 | |||
b35a87befe | |||
84eb562f1b | |||
138cf014d2 | |||
4816fc9a41 | |||
f6f851bd09 | |||
87dba671ac | |||
c6c19bce89 | |||
58999d0681 | |||
8696fca829 | |||
2856aeb89b | |||
2ba0d48ac1 | |||
5f730593fb | |||
dfc5c9eb6c | |||
14c17e1d24 | |||
449ff4cd00 | |||
606c521573 | |||
b199fc7023 | |||
4b05f22c8c |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [jeertmans]
|
40
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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
|
59
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
59
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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
|
26
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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
|
14
.github/ISSUE_TEMPLATE/support.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/support.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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
|
26
.github/pull_request_template.md
vendored
Normal file
26
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!-- 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 -->
|
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# 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@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
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@v2
|
||||||
|
|
||||||
|
# ℹ️ 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@v2
|
15
.github/workflows/languagetool.yml
vendored
Normal file
15
.github/workflows/languagetool.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
name: LanguageTool check
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
languagetool_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: reviewdog/action-languagetool@v1
|
||||||
|
with:
|
||||||
|
reporter: github-pr-review
|
||||||
|
level: warning
|
50
.github/workflows/pages.yml
vendored
Normal file
50
.github/workflows/pages.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Simple workflow for deploying static content to GitHub Pages
|
||||||
|
name: Deploy static content to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Runs on pushes targeting the default branch
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Single deploy job since we're just deploying
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v2
|
||||||
|
- name: Install Linux Dependencies
|
||||||
|
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: pip install manim sphinx sphinx_click furo
|
||||||
|
- name: Install local Python package
|
||||||
|
run: pip install -e .
|
||||||
|
- name: Build docs
|
||||||
|
run: cd docs && make html
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
# Upload docs/build/html dir
|
||||||
|
path: 'docs/build/html/'
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
3
.github/workflows/test_examples.yml
vendored
3
.github/workflows/test_examples.yml
vendored
@ -6,6 +6,9 @@ on:
|
|||||||
|
|
||||||
name: Test Examples
|
name: Test Examples
|
||||||
|
|
||||||
|
env:
|
||||||
|
QT_QPA_PLATFORM: offscreen
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-examples:
|
build-examples:
|
||||||
strategy:
|
strategy:
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -17,3 +17,5 @@ slides/
|
|||||||
videos/
|
videos/
|
||||||
|
|
||||||
images/
|
images/
|
||||||
|
|
||||||
|
docs/build/
|
||||||
|
@ -6,7 +6,7 @@ repos:
|
|||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.6.0
|
rev: 22.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
@ -15,3 +15,48 @@ repos:
|
|||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
args: ["--profile", "black"]
|
args: ["--profile", "black"]
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.10.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 5.0.4
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies:
|
||||||
|
- flake8-bugbear
|
||||||
|
- flake8-comprehensions
|
||||||
|
- flake8-tidy-imports
|
||||||
|
- flake8-typing-imports
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: 'v0.982'
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
args:
|
||||||
|
- --install-types
|
||||||
|
- --non-interactive
|
||||||
|
- --ignore-missing-imports
|
||||||
|
# Disallow dynamic typing
|
||||||
|
- --disallow-any-unimported
|
||||||
|
- --disallow-any-generics
|
||||||
|
- --disallow-subclassing-any
|
||||||
|
|
||||||
|
# Disallow untyped definitions and calls
|
||||||
|
- --disallow-untyped-defs
|
||||||
|
- --disallow-incomplete-defs
|
||||||
|
- --check-untyped-defs
|
||||||
|
|
||||||
|
# None and optional handling
|
||||||
|
- --no-implicit-optional
|
||||||
|
|
||||||
|
# Configuring warnings
|
||||||
|
- --warn-unused-ignores
|
||||||
|
- --warn-no-return
|
||||||
|
- --warn-return-any
|
||||||
|
- --warn-redundant-casts
|
||||||
|
|
||||||
|
# Strict equality
|
||||||
|
- --strict-equality
|
||||||
|
|
||||||
|
# Config file
|
||||||
|
- --warn-unused-configs
|
||||||
|
196
README.md
196
README.md
@ -1,33 +1,74 @@
|
|||||||
|

|
||||||
|
|
||||||
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
||||||
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
||||||

|

|
||||||
# Manim Slides
|
# Manim Slides
|
||||||
|
|
||||||
Tool for live presentations using either [manim-community](https://www.manim.community/) or [manimgl](https://3b1b.github.io/manim/). `manim-slides` will automatically detect the one you are using!
|
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!
|
> **_NOTE:_** This project extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
|
||||||
|
|
||||||
## Install
|
- [Installation](#installation)
|
||||||
|
* [Dependencies](#dependencies)
|
||||||
|
* [Pip install](#pip-install)
|
||||||
|
* [Install From Repository](#install-from-repository)
|
||||||
|
- [Usage](#usage)
|
||||||
|
* [Basic Example](#basic-example)
|
||||||
|
* [Key Bindings](#key-bindings)
|
||||||
|
* [Other Examples](#other-examples)
|
||||||
|
- [Features and Comparison with Original manim-presentation](#features-and-comparison-with-original-manim-presentation)
|
||||||
|
- [F.A.Q](#faq)
|
||||||
|
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
```
|
## Installation
|
||||||
|
|
||||||
|
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a [virtualenv](https://docs.python.org/3/tutorial/venv.html) for a local installation.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Manim Slides requires either Manim or ManimGL to be installed. 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)
|
||||||
|
|
||||||
|
### Pip Install
|
||||||
|
|
||||||
|
The recommended way to install the latest release is to use pip:
|
||||||
|
|
||||||
|
```bash
|
||||||
pip install manim-slides
|
pip install manim-slides
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
### Install From Repository
|
||||||
|
|
||||||
Use the class `Slide` as your scenes base class:
|
An alternative way to install Manim Slides is to clone the git repository, and install from there:
|
||||||
```python
|
|
||||||
from manim_slides import Slide
|
|
||||||
|
|
||||||
class Example(Slide):
|
```bash
|
||||||
def construct(self):
|
git clone https://github.com/jeertmans/manim-slides
|
||||||
...
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings).
|
> *Note:* the `-e` flag allows you to edit the files, and observe the changes directly when using Manim Slides
|
||||||
|
|
||||||
|
## 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.pause()` everytime you want to create a new slide.
|
||||||
|
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
|
||||||
|
|
||||||
|
The command-line documentation is available [online](https://eertmans.be/manim-slides/).
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
|
|
||||||
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
|
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# example.py
|
||||||
|
|
||||||
from manim import *
|
from manim import *
|
||||||
# or: from manimlib import *
|
# or: from manimlib import *
|
||||||
from manim_slides import Slide
|
from manim_slides import Slide
|
||||||
@ -38,105 +79,106 @@ class Example(Slide):
|
|||||||
dot = Dot()
|
dot = Dot()
|
||||||
|
|
||||||
self.play(GrowFromCenter(circle))
|
self.play(GrowFromCenter(circle))
|
||||||
self.pause()
|
self.pause() # Waits user to press continue to go to the next slide
|
||||||
|
|
||||||
self.start_loop()
|
self.start_loop() # Start loop
|
||||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||||
self.end_loop()
|
self.end_loop() # This will loop until user inputs a key
|
||||||
|
|
||||||
self.play(dot.animate.move_to(ORIGIN))
|
self.play(dot.animate.move_to(ORIGIN))
|
||||||
self.pause()
|
self.pause() # Waits user to press continue to go to the next slide
|
||||||
|
|
||||||
self.wait()
|
self.wait()
|
||||||
```
|
```
|
||||||
|
|
||||||
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(..)`.
|
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(...)`.
|
||||||
|
|
||||||
|
First, render the animation files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
manim example.py
|
||||||
|
# or
|
||||||
|
manimgl example.py
|
||||||
|
```
|
||||||
|
|
||||||
To start the presentation using `Scene1`, `Scene2` and so on simply run:
|
To start the presentation using `Scene1`, `Scene2` and so on simply run:
|
||||||
```
|
|
||||||
manim-slides Scene1 Scene2...
|
```bash
|
||||||
|
manim-slides [OPTIONS] Scene1 Scene2...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Keybindings
|
Or in this example:
|
||||||
|
|
||||||
Default keybindings to control the presentation:
|
|
||||||
|
|
||||||
| Keybinding | Action |
|
|
||||||
|:-----------:|:------------------------:|
|
|
||||||
| Right Arrow | Continue/Next Slide |
|
|
||||||
| Left Arrow | Previous Slide |
|
|
||||||
| R | Re-Animate Current Slide |
|
|
||||||
| V | Reverse Current Slide |
|
|
||||||
| Spacebar | Play/Pause |
|
|
||||||
| Q | Quit |
|
|
||||||
|
|
||||||
|
|
||||||
You can run the **configuration wizard** with:
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
manim-slides Example
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Key Bindings
|
||||||
|
|
||||||
|
The default key bindings to control the presentation are:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
You can run the **configuration wizard** to change those key bindings:
|
||||||
|
|
||||||
|
```bash
|
||||||
manim-slides wizard
|
manim-slides wizard
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively you can specify different keybindings creating a file named `.manim-slides.json` with the keys: `QUIT` `CONTINUE` `BACK` `REVERSE` `REWIND` and `PLAY_PAUSE`.
|
|
||||||
|
|
||||||
A default file can be created with:
|
A default file can be created with:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
manim-slides init
|
manim-slides init
|
||||||
```
|
```
|
||||||
|
|
||||||
> **_NOTE:_** `manim-slides` uses `cv2.waitKeyEx()` to wait for keypresses, and directly registers the key code.
|
> **_NOTE:_** `manim-slides` uses key codes, which are platform dependent. Using the configuration wizard is therefore highly recommended.
|
||||||
|
|
||||||
## Run Example
|
## Other Examples
|
||||||
|
|
||||||
Clone this repository:
|
Other examples are available in the [`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py) file, if you downloaded the git repository.
|
||||||
```
|
|
||||||
git clone https://github.com/jeertmans/manim-slides.git
|
|
||||||
cd manim-slides
|
|
||||||
```
|
|
||||||
|
|
||||||
Install `manim` and `manim-slides`:
|
|
||||||
```
|
|
||||||
pip install manim manim-slides
|
|
||||||
# or
|
|
||||||
pip install manimgl manim-slides
|
|
||||||
```
|
|
||||||
|
|
||||||
Render the example scene:
|
|
||||||
```
|
|
||||||
manim -qh example.py Example
|
|
||||||
# or
|
|
||||||
manimgl --hd example.py Example
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the presentation
|
|
||||||
```
|
|
||||||
manim-slides Example
|
|
||||||
```
|
|
||||||
|
|
||||||
Below is a small recording of me playing with the slides back and forth.
|
Below is a small recording of me playing with the slides back and forth.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Comparison with original `manim-presentation`
|
## Features and Comparison with original manim-presentation
|
||||||
|
|
||||||
Here are a few things that I implemented (or that I'm planning to implement) on top of the original work:
|
Below is a non-exhaustive list of features:
|
||||||
|
|
||||||
- [x] Allowing multiple keys to control one action (useful when you use a laser pointer)
|
| Feature | `manim-slides` | `manim-presentation` |
|
||||||
- [x] More robust config files checking
|
|:--------|:--------------:|:--------------------:|
|
||||||
- [x] Dependencies are installed with the package
|
| Support for Manim | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
- [x] Only one cli (to rule them all)
|
| Support for ManimGL | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
- [x] User can easily generate dummy config file
|
| Configurable key bindings | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
- [x] Config file path can be manually set
|
| Configurable paths | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
|
| Play / Pause slides | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
- [x] Handle 3D scenes out of the box
|
| Next / Previous slide | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
- [x] Support for both `manim` and `manimgl` modules
|
| Replay slide | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
- [ ] Generate docs online
|
| Reverse slide | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
|
| Multiple key per actions | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
| Hide mouse cursor | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||||
|
|
||||||
## Contributions and license
|
## F.A.Q
|
||||||
|
|
||||||
The code is released as Free Software under the [GNU/GPLv3](https://choosealicense.com/licenses/gpl-3.0/) license. Copying, adapting and republishing it is not only consent but also encouraged.
|
### 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%:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
in *Settings*->*Display*.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are more than welcome!
|
||||||
|
|
||||||
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
|
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
|
||||||
[pypi-version-url]: https://pypi.org/project/manim-slides/
|
[pypi-version-url]: https://pypi.org/project/manim-slides/
|
||||||
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 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)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@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
|
BIN
docs/source/_static/logo.png
Normal file
BIN
docs/source/_static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
27
docs/source/conf.py
Normal file
27
docs/source/conf.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = "Manim Slides"
|
||||||
|
copyright = "2022, Jérome Eertmans"
|
||||||
|
author = "Jérome Eertmans"
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = ["sphinx.ext.autodoc", "sphinx_click"]
|
||||||
|
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- 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"]
|
20
docs/source/index.rst
Normal file
20
docs/source/index.rst
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.. manim-slides documentation master file, created by
|
||||||
|
sphinx-quickstart on Wed Sep 21 15:07:28 2022.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
.. image:: _static/logo.png
|
||||||
|
:width: 600
|
||||||
|
:align: center
|
||||||
|
:alt: Manim Slide logo
|
||||||
|
|
||||||
|
Welcome to Manim Slide's CLI documentation!
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
This page contains an exhaustive list of all the commands available with `manim-slides`.
|
||||||
|
|
||||||
|
If you need help installing or using Manim Slide, please refer to the `GitHub README <https://github.com/jeertmans/manim-slides>`_.
|
||||||
|
|
||||||
|
.. click:: manim_slides.main:cli
|
||||||
|
:prog: manim-slides
|
||||||
|
:nested: full
|
@ -1,3 +1,5 @@
|
|||||||
|
# flake8: noqa: F403, F405
|
||||||
|
# type: ignore
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if "manim" in sys.modules:
|
if "manim" in sys.modules:
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
# flake8: noqa: F401
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
from .slide import Slide, ThreeDSlide
|
from .slide import Slide, ThreeDSlide
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "3.2.3"
|
__version__ = "4.5.0"
|
||||||
|
@ -1,20 +1,28 @@
|
|||||||
|
from typing import Callable
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from click import Context, Parameter
|
||||||
|
|
||||||
from .defaults import CONFIG_PATH
|
from .defaults import CONFIG_PATH
|
||||||
|
from .manim import logger
|
||||||
|
|
||||||
|
|
||||||
def config_path_option(function):
|
def config_path_option(function: Callable) -> Callable:
|
||||||
|
"""Wraps a function to add configuration path option."""
|
||||||
return click.option(
|
return click.option(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
"config_path",
|
"config_path",
|
||||||
|
metavar="FILE",
|
||||||
default=CONFIG_PATH,
|
default=CONFIG_PATH,
|
||||||
type=click.Path(dir_okay=False),
|
type=click.Path(dir_okay=False),
|
||||||
help="Set path to configuration file.",
|
help="Set path to configuration file.",
|
||||||
|
show_default=True,
|
||||||
)(function)
|
)(function)
|
||||||
|
|
||||||
|
|
||||||
def config_options(function):
|
def config_options(function: Callable) -> Callable:
|
||||||
|
"""Wraps a function to add configuration options."""
|
||||||
function = config_path_option(function)
|
function = config_path_option(function)
|
||||||
function = click.option(
|
function = click.option(
|
||||||
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."
|
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."
|
||||||
@ -26,3 +34,27 @@ def config_options(function):
|
|||||||
help="Merge any existing configuration file with the new configuration.",
|
help="Merge any existing configuration file with the new configuration.",
|
||||||
)(function)
|
)(function)
|
||||||
return function
|
return function
|
||||||
|
|
||||||
|
|
||||||
|
def verbosity_option(function: Callable) -> Callable:
|
||||||
|
"""Wraps a function to add verbosity option."""
|
||||||
|
|
||||||
|
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||||
|
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.setLevel(value)
|
||||||
|
|
||||||
|
return 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,
|
||||||
|
callback=callback,
|
||||||
|
)(function)
|
||||||
|
@ -1,31 +1,47 @@
|
|||||||
from typing import Optional, Set
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
from pydantic import BaseModel, root_validator, validator
|
from pydantic import BaseModel, root_validator, validator
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
|
from .manim import logger
|
||||||
|
|
||||||
|
|
||||||
class Key(BaseModel):
|
class Key(BaseModel):
|
||||||
|
"""Represents a list of key codes, with optionally a name."""
|
||||||
|
|
||||||
ids: Set[int]
|
ids: Set[int]
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
def set_ids(self, *ids: int) -> None:
|
||||||
|
self.ids = set(ids)
|
||||||
|
|
||||||
@validator("ids", each_item=True)
|
@validator("ids", each_item=True)
|
||||||
def id_is_posint(cls, v: int):
|
def id_is_posint(cls, v: int) -> int:
|
||||||
if v < 0:
|
if v < 0:
|
||||||
raise ValueError("Key ids cannot be negative integers")
|
raise ValueError("Key ids cannot be negative integers")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def match(self, key_id: int):
|
def match(self, key_id: int) -> bool:
|
||||||
return key_id in self.ids
|
m = key_id in self.ids
|
||||||
|
|
||||||
|
if m:
|
||||||
|
logger.debug(f"Pressed key: {self.name}")
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
QUIT: Key = Key(ids=[ord("q")], name="QUIT")
|
"""General Manim Slides config"""
|
||||||
CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT")
|
|
||||||
BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK")
|
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
|
||||||
REVERSE: Key = Key(ids=[ord("v")], name="REVERSE")
|
CONTINUE: Key = Key(ids=[Qt.Key_Right], name="CONTINUE / NEXT")
|
||||||
REWIND: Key = Key(ids=[ord("r")], name="REWIND")
|
BACK: Key = Key(ids=[Qt.Key_Left], name="BACK")
|
||||||
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
|
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
|
||||||
|
REWIND: Key = Key(ids=[Qt.Key_R], name="REWIND")
|
||||||
|
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
|
||||||
|
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
|
||||||
|
|
||||||
@root_validator
|
@root_validator
|
||||||
def ids_are_unique_across_keys(cls, values):
|
def ids_are_unique_across_keys(cls, values):
|
||||||
@ -34,7 +50,7 @@ class Config(BaseModel):
|
|||||||
for key in values.values():
|
for key in values.values():
|
||||||
if len(ids.intersection(key.ids)) != 0:
|
if len(ids.intersection(key.ids)) != 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Two or more keys share a common key code: please make sure each key has distinc key codes"
|
"Two or more keys share a common key code: please make sure each key has distinct key codes"
|
||||||
)
|
)
|
||||||
ids.update(key.ids)
|
ids.update(key.ids)
|
||||||
|
|
||||||
@ -47,3 +63,91 @@ class Config(BaseModel):
|
|||||||
key.name = other_key.name or key.name
|
key.name = other_key.name or key.name
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class SlideType(str, Enum):
|
||||||
|
slide = "slide"
|
||||||
|
loop = "loop"
|
||||||
|
last = "last"
|
||||||
|
|
||||||
|
|
||||||
|
class SlideConfig(BaseModel):
|
||||||
|
type: SlideType
|
||||||
|
start_animation: int
|
||||||
|
end_animation: int
|
||||||
|
number: int
|
||||||
|
terminated: bool = False
|
||||||
|
|
||||||
|
@validator("start_animation", "end_animation")
|
||||||
|
def index_is_posint(cls, v: int):
|
||||||
|
if v < 0:
|
||||||
|
raise ValueError("Animation index (start or end) cannot be negative")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator("number")
|
||||||
|
def number_is_strictly_posint(cls, v: int):
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Slide number cannot be negative or zero")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@root_validator
|
||||||
|
def start_animation_is_before_end(cls, values):
|
||||||
|
if values["start_animation"] >= values["end_animation"]:
|
||||||
|
|
||||||
|
if values["start_animation"] == values["end_animation"] == 0:
|
||||||
|
raise ValueError(
|
||||||
|
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting."
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"Start animation index must be strictly lower than end animation index"
|
||||||
|
)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def is_slide(self):
|
||||||
|
return self.type == SlideType.slide
|
||||||
|
|
||||||
|
def is_loop(self):
|
||||||
|
return self.type == SlideType.loop
|
||||||
|
|
||||||
|
def is_last(self):
|
||||||
|
return self.type == SlideType.last
|
||||||
|
|
||||||
|
|
||||||
|
class PresentationConfig(BaseModel):
|
||||||
|
slides: List[SlideConfig]
|
||||||
|
files: List[str]
|
||||||
|
|
||||||
|
@validator("files", pre=True, each_item=True)
|
||||||
|
def is_file_and_exists(cls, v):
|
||||||
|
if not os.path.exists(v):
|
||||||
|
raise ValueError(
|
||||||
|
f"Animation file {v} does not exist. Are you in the right directory?"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.isfile(v):
|
||||||
|
raise ValueError(f"Animation file {v} is not a file")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
@root_validator
|
||||||
|
def animation_indices_match_files(cls, values):
|
||||||
|
files = values.get("files")
|
||||||
|
slides = values.get("slides")
|
||||||
|
|
||||||
|
if files is None or slides is None:
|
||||||
|
return values
|
||||||
|
|
||||||
|
n_files = len(files)
|
||||||
|
|
||||||
|
for slide in slides:
|
||||||
|
if slide.end_animation > n_files:
|
||||||
|
raise ValueError(
|
||||||
|
f"The following slide's contains animations not listed in files {files}: {slide}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = Config()
|
||||||
|
@ -1,11 +1,2 @@
|
|||||||
import platform
|
|
||||||
|
|
||||||
FOLDER_PATH: str = "./slides"
|
FOLDER_PATH: str = "./slides"
|
||||||
CONFIG_PATH: str = ".manim-slides.json"
|
CONFIG_PATH: str = ".manim-slides.json"
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
RIGHT_ARROW_KEY_CODE = 2555904
|
|
||||||
LEFT_ARROW_KEY_CODE = 2424832
|
|
||||||
else:
|
|
||||||
RIGHT_ARROW_KEY_CODE = 65363
|
|
||||||
LEFT_ARROW_KEY_CODE = 65361
|
|
||||||
|
@ -9,7 +9,12 @@ from .wizard import init, wizard
|
|||||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||||
@click.version_option(__version__, "-v", "--version")
|
@click.version_option(__version__, "-v", "--version")
|
||||||
@click.help_option("-h", "--help")
|
@click.help_option("-h", "--help")
|
||||||
def cli():
|
def cli() -> None:
|
||||||
|
"""
|
||||||
|
Manim Slides command-line utilities.
|
||||||
|
|
||||||
|
If no command is specified, defaults to `present`.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,36 @@
|
|||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
from importlib.util import find_spec
|
from importlib.util import find_spec
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"MANIM",
|
||||||
|
"MANIM_PACKAGE_NAME",
|
||||||
|
"MANIM_AVAILABLE",
|
||||||
|
"MANIM_IMPORTED",
|
||||||
|
"MANIMGL",
|
||||||
|
"MANIMGL_PACKAGE_NAME",
|
||||||
|
"MANIMGL_AVAILABLE",
|
||||||
|
"MANIMGL_IMPORTED",
|
||||||
|
"logger",
|
||||||
|
"Scene",
|
||||||
|
"ThreeDScene",
|
||||||
|
"config",
|
||||||
|
"FFMPEG_BIN",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def suppress_stdout() -> Iterator[None]:
|
||||||
|
with open(os.devnull, "w") as devnull:
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
sys.stdout = devnull
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
|
|
||||||
MANIM_PACKAGE_NAME = "manim"
|
MANIM_PACKAGE_NAME = "manim"
|
||||||
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
|
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
|
||||||
@ -17,29 +48,34 @@ if MANIM_IMPORTED and MANIMGL_IMPORTED:
|
|||||||
)
|
)
|
||||||
MANIM = True
|
MANIM = True
|
||||||
MANIMGL = False
|
MANIMGL = False
|
||||||
elif MANIM_AVAILABLE and not MANIMGL_IMPORTED:
|
elif MANIM_IMPORTED:
|
||||||
|
MANIM = True
|
||||||
|
MANIMGL = False
|
||||||
|
elif MANIMGL_IMPORTED:
|
||||||
|
MANIM = False
|
||||||
|
MANIMGL = True
|
||||||
|
elif MANIM_AVAILABLE:
|
||||||
MANIM = True
|
MANIM = True
|
||||||
MANIMGL = False
|
MANIMGL = False
|
||||||
elif MANIMGL_AVAILABLE:
|
elif MANIMGL_AVAILABLE:
|
||||||
MANIM = False
|
MANIM = False
|
||||||
MANIMGL = True
|
MANIMGL = True
|
||||||
else:
|
else:
|
||||||
raise ImportError(
|
raise ModuleNotFoundError(
|
||||||
"Either manim (community) or manimgl (3b1b) package must be installed"
|
"Either manim (community) or manimgl (3b1b) package must be installed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
FFMPEG_BIN = None
|
|
||||||
|
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
from manimlib import Scene, ThreeDScene, config
|
from manimlib import Scene, ThreeDScene, config
|
||||||
from manimlib.constants import FFMPEG_BIN
|
from manimlib.constants import FFMPEG_BIN
|
||||||
from manimlib.logger import log as logger
|
from manimlib.logger import log as logger
|
||||||
|
|
||||||
else:
|
else:
|
||||||
from manim import Scene, ThreeDScene, config, logger
|
with suppress_stdout(): # Avoids printing "Manim Community v..."
|
||||||
|
from manim import Scene, ThreeDScene, config, logger
|
||||||
|
|
||||||
try: # For manim<v0.16.0.post0
|
try: # For manim<v0.16.0.post0
|
||||||
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
|
from manim.constants import FFMPEG_BIN
|
||||||
except ImportError:
|
except ImportError:
|
||||||
FFMPEG_BIN = config.ffmpeg_executable
|
FFMPEG_BIN = config.ffmpeg_executable
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,28 +1,31 @@
|
|||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from .config import PresentationConfig, SlideConfig, SlideType
|
||||||
from .defaults import FOLDER_PATH
|
from .defaults import FOLDER_PATH
|
||||||
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
||||||
|
|
||||||
|
|
||||||
def reverse_video_path(src: str) -> str:
|
def reverse_video_file(src: str, dst: str) -> None:
|
||||||
file, ext = os.path.splitext(src)
|
"""Reverses a video file, writting the result to `dst`."""
|
||||||
return f"{file}_reversed{ext}"
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_video_file(src: str, dst: str):
|
|
||||||
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
||||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
|
|
||||||
|
|
||||||
class Slide(Scene):
|
class Slide(Scene):
|
||||||
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
|
"""
|
||||||
|
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args: Any, output_folder: str = FOLDER_PATH, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
if not os.path.isdir("videos"):
|
if not os.path.isdir("videos"):
|
||||||
os.mkdir("videos")
|
os.mkdir("videos")
|
||||||
@ -37,14 +40,15 @@ class Slide(Scene):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.output_folder = output_folder
|
self.output_folder = output_folder
|
||||||
self.slides = list()
|
self.slides: List[SlideConfig] = []
|
||||||
self.current_slide = 1
|
self.current_slide = 1
|
||||||
self.current_animation = 0
|
self.current_animation = 0
|
||||||
self.loop_start_animation = None
|
self.loop_start_animation: Optional[int] = None
|
||||||
self.pause_start_animation = 0
|
self.pause_start_animation = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def partial_movie_files(self):
|
def partial_movie_files(self) -> List[str]:
|
||||||
|
"""Returns a list of partial movie files, a.k.a animations."""
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||||
|
|
||||||
@ -59,27 +63,31 @@ class Slide(Scene):
|
|||||||
return self.renderer.file_writer.partial_movie_files
|
return self.renderer.file_writer.partial_movie_files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def show_progress_bar(self):
|
def show_progress_bar(self) -> bool:
|
||||||
|
"""Returns True if progress bar should be displayed."""
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
return getattr(super(Scene, self), "show_progress_bar", True)
|
return getattr(super(Scene, self), "show_progress_bar", True)
|
||||||
else:
|
else:
|
||||||
return config["progress_bar"] != "none"
|
return config["progress_bar"] != "none"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def leave_progress_bar(self):
|
def leave_progress_bar(self) -> bool:
|
||||||
|
"""Returns True if progress bar should be left after completed."""
|
||||||
if MANIMGL:
|
if MANIMGL:
|
||||||
return getattr(super(Scene, self), "leave_progress_bars", False)
|
return getattr(super(Scene, self), "leave_progress_bars", False)
|
||||||
else:
|
else:
|
||||||
return config["progress_bar"] == "leave"
|
return config["progress_bar"] == "leave"
|
||||||
|
|
||||||
def play(self, *args, **kwargs):
|
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Overloads `self.play` and increment animation count."""
|
||||||
super().play(*args, **kwargs)
|
super().play(*args, **kwargs)
|
||||||
self.current_animation += 1
|
self.current_animation += 1
|
||||||
|
|
||||||
def pause(self):
|
def pause(self) -> None:
|
||||||
|
"""Creates a new slide with previous animations."""
|
||||||
self.slides.append(
|
self.slides.append(
|
||||||
dict(
|
SlideConfig(
|
||||||
type="slide",
|
type=SlideType.slide,
|
||||||
start_animation=self.pause_start_animation,
|
start_animation=self.pause_start_animation,
|
||||||
end_animation=self.current_animation,
|
end_animation=self.current_animation,
|
||||||
number=self.current_slide,
|
number=self.current_slide,
|
||||||
@ -88,17 +96,19 @@ class Slide(Scene):
|
|||||||
self.current_slide += 1
|
self.current_slide += 1
|
||||||
self.pause_start_animation = self.current_animation
|
self.pause_start_animation = self.current_animation
|
||||||
|
|
||||||
def start_loop(self):
|
def start_loop(self) -> None:
|
||||||
|
"""Starts a loop."""
|
||||||
assert self.loop_start_animation is None, "You cannot nest loops"
|
assert self.loop_start_animation is None, "You cannot nest loops"
|
||||||
self.loop_start_animation = self.current_animation
|
self.loop_start_animation = self.current_animation
|
||||||
|
|
||||||
def end_loop(self):
|
def end_loop(self) -> None:
|
||||||
|
"""Ends an existing loop."""
|
||||||
assert (
|
assert (
|
||||||
self.loop_start_animation is not None
|
self.loop_start_animation is not None
|
||||||
), "You have to start a loop before ending it"
|
), "You have to start a loop before ending it"
|
||||||
self.slides.append(
|
self.slides.append(
|
||||||
dict(
|
SlideConfig(
|
||||||
type="loop",
|
type=SlideType.loop,
|
||||||
start_animation=self.loop_start_animation,
|
start_animation=self.loop_start_animation,
|
||||||
end_animation=self.current_animation,
|
end_animation=self.current_animation,
|
||||||
number=self.current_slide,
|
number=self.current_slide,
|
||||||
@ -108,8 +118,12 @@ class Slide(Scene):
|
|||||||
self.loop_start_animation = None
|
self.loop_start_animation = None
|
||||||
self.pause_start_animation = self.current_animation
|
self.pause_start_animation = self.current_animation
|
||||||
|
|
||||||
def save_slides(self, use_cache=True):
|
def save_slides(self, use_cache: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Saves slides, optionally using cached files.
|
||||||
|
|
||||||
|
Note that cached files only work with Manim.
|
||||||
|
"""
|
||||||
if not os.path.exists(self.output_folder):
|
if not os.path.exists(self.output_folder):
|
||||||
os.mkdir(self.output_folder)
|
os.mkdir(self.output_folder)
|
||||||
|
|
||||||
@ -130,7 +144,7 @@ class Slide(Scene):
|
|||||||
else:
|
else:
|
||||||
old_animation_files.update(os.listdir(scene_files_folder))
|
old_animation_files.update(os.listdir(scene_files_folder))
|
||||||
|
|
||||||
files = list()
|
files = []
|
||||||
for src_file in tqdm(
|
for src_file in tqdm(
|
||||||
self.partial_movie_files,
|
self.partial_movie_files,
|
||||||
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
|
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
|
||||||
@ -139,9 +153,7 @@ class Slide(Scene):
|
|||||||
disable=not self.show_progress_bar,
|
disable=not self.show_progress_bar,
|
||||||
):
|
):
|
||||||
filename = os.path.basename(src_file)
|
filename = os.path.basename(src_file)
|
||||||
_hash, ext = os.path.splitext(filename)
|
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
|
||||||
|
|
||||||
rev_filename = f"{_hash}_reversed{ext}"
|
|
||||||
|
|
||||||
dst_file = os.path.join(scene_files_folder, filename)
|
dst_file = os.path.join(scene_files_folder, filename)
|
||||||
# We only copy animation if it was not present
|
# We only copy animation if it was not present
|
||||||
@ -165,19 +177,19 @@ class Slide(Scene):
|
|||||||
|
|
||||||
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
|
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
|
||||||
|
|
||||||
f = open(slide_path, "w")
|
with open(slide_path, "w") as f:
|
||||||
json.dump(dict(slides=self.slides, files=files), f)
|
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
|
||||||
f.close()
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""MANIMGL renderer"""
|
"""MANIMGL renderer"""
|
||||||
super().run(*args, **kwargs)
|
super().run(*args, **kwargs)
|
||||||
self.save_slides(use_cache=False)
|
self.save_slides(use_cache=False)
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
def render(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""MANIM render"""
|
"""MANIM render"""
|
||||||
# We need to disable the caching limit since we rely on intermidiate files
|
# We need to disable the caching limit since we rely on intermidiate files
|
||||||
max_files_cached = config["max_files_cached"]
|
max_files_cached = config["max_files_cached"]
|
||||||
@ -191,4 +203,10 @@ class Slide(Scene):
|
|||||||
|
|
||||||
|
|
||||||
class ThreeDSlide(Slide, ThreeDScene):
|
class ThreeDSlide(Slide, ThreeDScene):
|
||||||
|
"""
|
||||||
|
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
|
||||||
|
|
||||||
|
Note that ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
@ -1,28 +1,132 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from functools import partial
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import cv2
|
from PySide6.QtCore import Qt
|
||||||
import numpy as np
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QGridLayout,
|
||||||
|
QLabel,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
from .commons import config_options
|
from .commons import config_options, verbosity_option
|
||||||
from .config import Config
|
from .config import Config, Key
|
||||||
from .defaults import CONFIG_PATH
|
from .defaults import CONFIG_PATH
|
||||||
|
from .manim import logger
|
||||||
|
|
||||||
|
WINDOW_NAME: str = "Configuration Wizard"
|
||||||
|
|
||||||
|
keymap = {}
|
||||||
|
for key in Qt.Key:
|
||||||
|
keymap[key.value] = key.name.partition("_")[2]
|
||||||
|
|
||||||
|
|
||||||
def prompt(question: str) -> int:
|
class KeyInput(QDialog):
|
||||||
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
|
def __init__(self) -> None:
|
||||||
display = np.zeros((130, 420), np.uint8)
|
super().__init__()
|
||||||
|
self.key = None
|
||||||
|
|
||||||
cv2.putText(display, "* Manim Slides Wizard *", (70, 33), *font_args)
|
self.layout = QVBoxLayout()
|
||||||
cv2.putText(display, question, (30, 85), *font_args)
|
|
||||||
|
|
||||||
cv2.imshow("Manim Slides Configuration Wizard", display)
|
self.setWindowTitle("Keyboard Input")
|
||||||
return cv2.waitKeyEx(-1)
|
self.label = QLabel("Press any key to register it")
|
||||||
|
self.layout.addWidget(self.label)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def keyPressEvent(self, event: Any) -> None:
|
||||||
|
self.key = event.key()
|
||||||
|
self.deleteLater()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class Wizard(QWidget):
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.setWindowTitle(WINDOW_NAME)
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
QBtn = QDialogButtonBox.Save | QDialogButtonBox.Cancel
|
||||||
|
|
||||||
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
|
self.buttonBox.accepted.connect(self.saveConfig)
|
||||||
|
self.buttonBox.rejected.connect(self.closeWithoutSaving)
|
||||||
|
|
||||||
|
self.buttons = []
|
||||||
|
|
||||||
|
self.layout = QGridLayout()
|
||||||
|
|
||||||
|
for i, (key, value) in enumerate(self.config.dict().items()):
|
||||||
|
# Create label for key name information
|
||||||
|
label = QLabel()
|
||||||
|
key_info = value["name"] or key
|
||||||
|
label.setText(key_info)
|
||||||
|
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.openDialog, i, getattr(self.config, key))
|
||||||
|
)
|
||||||
|
self.layout.addWidget(button, i, 1)
|
||||||
|
|
||||||
|
self.layout.addWidget(self.buttonBox, len(self.buttons), 1)
|
||||||
|
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def closeWithoutSaving(self) -> None:
|
||||||
|
logger.debug("Closing configuration wizard without saving")
|
||||||
|
self.deleteLater()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def closeEvent(self, event: Any) -> None:
|
||||||
|
self.closeWithoutSaving()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def saveConfig(self) -> None:
|
||||||
|
try:
|
||||||
|
Config.parse_obj(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 openDialog(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)
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@config_options
|
@config_options
|
||||||
|
@click.help_option("-h", "--help")
|
||||||
|
@verbosity_option
|
||||||
def wizard(config_path, force, merge):
|
def wizard(config_path, force, merge):
|
||||||
"""Launch configuration wizard."""
|
"""Launch configuration wizard."""
|
||||||
return _init(config_path, force, merge, skip_interactive=False)
|
return _init(config_path, force, merge, skip_interactive=False)
|
||||||
@ -30,12 +134,15 @@ def wizard(config_path, force, merge):
|
|||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@config_options
|
@config_options
|
||||||
|
@click.help_option("-h", "--help")
|
||||||
|
@verbosity_option
|
||||||
def init(config_path, force, merge, skip_interactive=False):
|
def init(config_path, force, merge, skip_interactive=False):
|
||||||
"""Initialize a new default configuration file."""
|
"""Initialize a new default configuration file."""
|
||||||
return _init(config_path, force, merge, skip_interactive=True)
|
return _init(config_path, force, merge, skip_interactive=True)
|
||||||
|
|
||||||
|
|
||||||
def _init(config_path, force, merge, skip_interactive=False):
|
def _init(config_path, force, merge, skip_interactive=False):
|
||||||
|
"""Actual initialization code for configuration file, with optional interactive mode."""
|
||||||
|
|
||||||
if os.path.exists(config_path):
|
if os.path.exists(config_path):
|
||||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||||
@ -49,27 +156,33 @@ def _init(config_path, force, merge, skip_interactive=False):
|
|||||||
force = choice == "o"
|
force = choice == "o"
|
||||||
merge = choice == "m"
|
merge = choice == "m"
|
||||||
|
|
||||||
if force:
|
if not force and not merge:
|
||||||
click.secho("Overwriting.")
|
logger.debug("Exiting without doing anything")
|
||||||
elif merge:
|
|
||||||
click.secho("Merging.")
|
|
||||||
else:
|
|
||||||
click.secho("Exiting.")
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
config = Config()
|
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 not skip_interactive:
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
config = Config.parse_file(config_path)
|
||||||
|
|
||||||
prompt("Press any key to continue")
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("Manim Slides Wizard")
|
||||||
|
window = Wizard(config)
|
||||||
|
window.show()
|
||||||
|
app.exec()
|
||||||
|
|
||||||
for _, key in config:
|
config = window.config
|
||||||
key.ids = [prompt(f"Press the {key.name} key")]
|
|
||||||
|
|
||||||
if merge:
|
if merge:
|
||||||
config = Config.parse_file(config_path).merge_with(config)
|
config = Config.parse_file(config_path).merge_with(config)
|
||||||
|
|
||||||
with open(config_path, "w") as config_file:
|
with open(config_path, "w") as config_file:
|
||||||
config_file.write(config.json(indent=4))
|
config_file.write(config.json(indent=2))
|
||||||
|
|
||||||
click.echo(f"Configuration file successfully save to `{config_path}`")
|
click.secho(f"Configuration file successfully saved to `{config_path}`")
|
||||||
|
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[tool.vulture]
|
||||||
|
paths = ["manim_slides"]
|
15
setup.py
15
setup.py
@ -1,18 +1,26 @@
|
|||||||
|
# type: ignore
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
from manim_slides import __version__ as version
|
|
||||||
|
|
||||||
if sys.version_info < (3, 7):
|
if sys.version_info < (3, 7):
|
||||||
raise RuntimeError("This package requires Python 3.7+")
|
raise RuntimeError("This package requires Python 3.7+")
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"__version__", os.path.join("manim_slides", "__version__.py")
|
||||||
|
)
|
||||||
|
version = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(version)
|
||||||
|
|
||||||
|
|
||||||
with open("README.md", "r") as f:
|
with open("README.md", "r") as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name="manim-slides",
|
name="manim-slides",
|
||||||
version=version,
|
version=version.__version__,
|
||||||
author="Jérome Eertmans (previously, Federico A. Galatolo)",
|
author="Jérome Eertmans (previously, Federico A. Galatolo)",
|
||||||
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
|
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
|
||||||
description="Tool for live presentations using manim",
|
description="Tool for live presentations using manim",
|
||||||
@ -31,6 +39,7 @@ setuptools.setup(
|
|||||||
"click-default-group>=1.2",
|
"click-default-group>=1.2",
|
||||||
"numpy>=1.19.3",
|
"numpy>=1.19.3",
|
||||||
"pydantic>=1.9.1",
|
"pydantic>=1.9.1",
|
||||||
|
"pyside6>=6",
|
||||||
"opencv-python>=4.6",
|
"opencv-python>=4.6",
|
||||||
"tqdm>=4.62.3",
|
"tqdm>=4.62.3",
|
||||||
],
|
],
|
||||||
|
BIN
static/logo.png
Normal file
BIN
static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
41
static/logo.py
Normal file
41
static/logo.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# flake8: noqa: F403, F405
|
||||||
|
# type: ignore
|
||||||
|
from manim import *
|
||||||
|
|
||||||
|
|
||||||
|
class ManimSlidesLogo(Scene):
|
||||||
|
def construct(self):
|
||||||
|
tex_template = TexTemplate()
|
||||||
|
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
|
||||||
|
self.camera.background_color = "#ffffff"
|
||||||
|
logo_green = "#87c2a5"
|
||||||
|
logo_blue = "#525893"
|
||||||
|
logo_red = "#e07a5f"
|
||||||
|
logo_black = "#343434"
|
||||||
|
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)
|
BIN
static/windows_quality_fix.png
Normal file
BIN
static/windows_quality_fix.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Reference in New Issue
Block a user