Compare commits

..

65 Commits

Author SHA1 Message Date
422e355758 chore(version): update version and change pyside version dep 2022-10-31 15:07:33 +01:00
3eb9fa0b74 refactor(lib): change PyQT5 to PySide6 (#62)
* refactor(lib): change PyQT5 to PySide6

This, hopefully, should now add support for M1 chips

* chore: update README and change imports
2022-10-31 14:55:03 +01:00
8f519ed134 Create FUNDING.yml 2022-10-31 10:11:32 +01:00
916e2aa2ab chore(version): update version 2022-10-31 09:15:01 +01:00
4d5f664348 chore(cli): change metavar for some options (#61)
This changes some metavars such that `--help` output is closer to the Sphinx documentation.
2022-10-31 09:13:32 +01:00
cb6a5bb35f feat: add option for background color (#60)
* feat: add option for background color

This allows to define the background color used for border when resize mode is set to "keep".

Closes #52

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-31 09:01:14 +01:00
bba05cce16 chore(version): update version 2022-10-24 09:27:14 +02:00
ad02c8296b feat(cli): add resize mode option (#59)
This adds a new option that allows to control how the video is going to be scaled. By default, we now use a smooth rescaling, but fast mode with no interpolation can be used (previously default).
2022-10-24 09:26:48 +02:00
0778cebef7 chore(README): update key bindings documentation 2022-10-24 09:20:14 +02:00
163260415b Mouse Show/Hide feature (#56)
* Mouse Show/Hide feature

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

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

* Mouse Hide/Show Fix

Applied requested changes for PR

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-10-21 12:57:53 +02:00
241419a781 Support template (#57)
* PATCH: Docs template enhacement

* FEAT: Template drafted for Support

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

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

* Update .github/ISSUE_TEMPLATE/documentation.yml

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Update .github/ISSUE_TEMPLATE/support.yml

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

Co-authored-by: Harisai <Harisaipravin_SV@intuit.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-10-21 10:55:39 +02:00
bac21815b2 chore(README): add note for M1 chips and doc. feat 2022-10-20 08:55:00 +02:00
2f8f7561a6 Issue more templates (#54)
* FEAT: Git template for documentation

* PATCH: Space keys updated

* PATCH: Spaccing update

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

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

* UPDATE: Code of conduct removed

* PATCH: Retry CI

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

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

Co-authored-by: Harisai <Harisaipravin_SV@intuit.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-19 18:27:41 +02:00
a489dfd0e8 feat: set application name 2022-10-19 14:32:16 +02:00
76ef16d98b fix(docs): typo 2022-10-19 11:51:43 +02:00
88125bf1ae Update __version__.py 2022-10-19 11:09:23 +02:00
cffc4ebbc5 Update __version__.py 2022-10-19 11:09:14 +02:00
d717bc651d refactor: use PyQT5 for window display (#49)
* wip: use PyQT5 for window display

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

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

* wip: first slide is shown

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

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

* wip: pushing non-working code

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

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

* wip: some logging

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

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

* feat: new configuration wizard working

* fix: prevent key error

* wip: making action work

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

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

* wip: soon done! info + video

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

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

* fix: bugs in sleep and exiting

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

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

* try: offscreen

* fix: pop default value if not present

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

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

* feat: add aspect ratio option

* chore: typing wip

* fix: now() function returns seconds, not milliseconds anymore

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-19 11:08:41 +02:00
bc3d55fce2 feat(cli): add basic logging facilities (#50)
* feat(cli): add basic logging facilities

This PR adds basic logging support, to be extended in the future.

* fix: typo in decorator name
2022-10-17 10:52:42 +02:00
5b9cb1523c chore(ci): pre-commit mypy (#41)
* chore(ci): pre-commit mypy

* chore: ignore non-lib files

* chore: ignore setup.py

* [pre-commit.ci] pre-commit autoupdate (#47)

updates:
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* chore(ci): pre-commit mypy

* chore: ignore non-lib files

* chore: ignore setup.py

* fix: update pre-commit config for mypy

* feat: add some missing type hints

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-11 11:05:58 +02:00
51a87840ce [pre-commit.ci] pre-commit autoupdate (#47)
updates:
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)
- [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-11 10:10:30 +02:00
42550e8b29 Update defaults.py (#46)
fix the key id for macos
2022-10-10 07:31:52 +02:00
501813483c Added Pull Request Template (#43)
* Create feature_request.yml

* Issue template

* Create bug.yml

* Update bug.yml

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

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

* Update bug.yml

* Update bug.yml

* Created Pull Request Template

* Changed directory

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

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

* Update .github/pull_request_template.md

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-10-06 18:07:12 +02:00
0ae99c0f4d fix: typo in issue template 2022-10-06 09:17:07 +02:00
c2315928bd Created Bug Template (#40)
* Create feature_request.yml

* Issue template

* Create bug.yml

* Update bug.yml

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

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

* Update bug.yml

* Update bug.yml

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-05 19:31:31 +02:00
f3c8f3cc24 Add Feature Request Template (#39)
* Create ISSUE_TEMPLATE

* Update feature_request.yml

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-05 17:21:48 +02:00
14a266b139 Update README.md (#33)
Fixed small spelling mistake.
2022-10-05 10:53:34 +02:00
b697442fc0 chore(ci): add more style checks (#32)
* chore(ci): add more style checks

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

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

* chore(ci): remove vulture as flake8 replaces it

* chore(lib): add __all__ list

* chore(ci): make flake8 happy and remove mypy

* fix: config_path is actually used in commons

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-05 10:41:08 +02:00
4f8fae75cf [pre-commit.ci] pre-commit autoupdate (#31)
updates:
- [github.com/jendrikseipp/vulture: v2.3 → v2.6](https://github.com/jendrikseipp/vulture/compare/v2.3...v2.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-28 07:55:24 +02:00
d6ec0d3da9 chore(version): update version 2022-09-26 17:30:10 +02:00
546451e019 fix(lib): add a more meaningful error message (#30)
* fix(lib): add a more meaningful error message

This adds an error message to explain why calling `self.pause()` cannot work if no previous animation was played.

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-26 17:29:17 +02:00
2457ca8a05 fix(cli): properly rewind / previous slide after reverse (#28)
* fix(cli): properly rewind / previous slide after reverse

Closes #24

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-22 19:11:26 +02:00
9900b3123e fix(cli): revert breaking change 2022-09-22 18:28:27 +02:00
ee92e0aa88 chore(docs): improve CLI docs (#27)
* chore(docs): cleanup code & docs

* chore(docs): better document present command

* chore(docs): improve gh page
2022-09-22 18:16:28 +02:00
cbee6320f5 chore(version): update version 2022-09-21 16:11:20 +02:00
382084f9ef feat(cli): record presentation (#25)
* feat(cli): record presentation

As proposed in #21, it is now possible to record a presentation output to a video file, with option `--record-to="some_file.avi"`.

Closes #21

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-21 16:10:37 +02:00
068484b828 chore(docs): add base cli docs 2022-09-21 16:09:08 +02:00
91f8d97acf chore(cli): show default for all cli options (#26) 2022-09-21 15:41:19 +02:00
49cdedc6fe chore(README): add link to doc 2022-09-21 15:36:38 +02:00
fe1fa059f6 chore(ci): auto build docs 2022-09-21 15:30:07 +02:00
3f6d2e5e57 chore(docs): create first documentation 2022-09-21 15:26:10 +02:00
99ad798155 chore(version): update version 2022-09-21 10:08:57 +02:00
84c25f1ed5 fix(ui): enhance window quality (#22)
* fix(ui): enhance window quality

This fix always resizes the frame size, as this seems to be a good cross-platform fix to the quality issue that occurs when the frame does not match the window size.

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

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

* fix: remove unused import

* fix: actually only resize on Windows

* feat(cli): optional interpolation flag

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

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

* chore: add image fox windows quality fix

* chore(README): document Windows fix

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-21 09:15:12 +02:00
7fb3fa01dd chore(ci): check for unused code with vulture (#23)
* chore(ci): check for unused code with vulture

* fix: add paths for vulture
2022-09-20 09:36:39 +02:00
2d2a225afe [pre-commit.ci] pre-commit autoupdate (#19)
updates:
- [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-20 09:18:14 +02:00
b9d2cd92b5 chore(docs): improve docstrings (#18) 2022-09-17 15:49:10 +02:00
620bb30960 feat(ui): much cleaner UI (#17) 2022-09-16 15:28:18 +02:00
b35a87befe chore(ci): add CodeQL check (#16)
* chore(ci): add CodeQL check

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-16 10:22:32 +02:00
84eb562f1b fix: remove dummy print 2022-09-15 15:21:02 +02:00
138cf014d2 fix: apply pypi's versioning 2022-09-15 15:12:17 +02:00
4816fc9a41 feat: large "present" feature refactoring (#15)
* fix(cli): previous presentation starts last slide

This changes the previous behavior that was such that going to previous presentation (scene) would start it from zero.

* fix(README): remove non-working svgs

Seems like GitHub does not support that

* wip: onto a cleaner parser, and more robust presentation

* fix(README): remove non-working svgs

Seems like GitHub does not support that

* wip: onto a cleaner parser, and more robust presentation

* wip: some progress

* fix: this is kinda working now

* fix: repeated reversed slide

* fix: import __version__ without importing package

* fix: typing list -> List

* chore(README): update features
2022-09-15 15:10:12 +02:00
f6f851bd09 chore(README): change link of example file 2022-09-14 18:59:28 +02:00
87dba671ac chore(README): add link to example file 2022-09-14 18:58:19 +02:00
c6c19bce89 misc(logo): update logo 2022-09-14 18:52:39 +02:00
58999d0681 fix(cli): previous presentation starts last slide (#14)
This changes the previous behavior that was such that going to previous presentation (scene) would start it from zero.
2022-09-14 18:35:52 +02:00
8696fca829 chore(README): upgrade documentation in README (#13)
* chore(README): upgrade in the README documentation

* chore(README): upgrade in the README documentation

* fix(README): typo in emoji

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

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

* fix(README): typo in emoji (bis)

* fix: toc link

* fix: few typos found by LT

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-14 16:11:27 +02:00
2856aeb89b Update __version__.py 2022-09-14 13:44:55 +02:00
2ba0d48ac1 fix(cli): resize only on fullscreen (#12)
* fix(cli): resize only on fullscreen

Closes #10

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-14 13:41:25 +02:00
5f730593fb fix: increase logo quality but ratio is the same 2022-09-14 11:59:20 +02:00
dfc5c9eb6c Merge branch 'add-logo' into main 2022-09-14 11:57:25 +02:00
14c17e1d24 fix: better logo resolution
1280x640, according to GitHub
2022-09-14 11:56:20 +02:00
449ff4cd00 misc: create Manim Slides logo (#11)
* misc: create Manim Slides logo

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

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

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-14 11:45:43 +02:00
606c521573 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-09-14 09:42:31 +00:00
b199fc7023 misc: create Manim Slides logo 2022-09-14 11:42:01 +02:00
4b05f22c8c fix(cli): disable printing "Manim Community v..." (#9) 2022-09-13 12:01:02 +02:00
35 changed files with 1665 additions and 389 deletions

4
.flake8 Normal file
View File

@ -0,0 +1,4 @@
[flake8]
extend-ignore =
# E501: line too long
E501,

3
.github/FUNDING.yml vendored Normal file
View File

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

40
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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

View File

@ -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
View File

@ -17,3 +17,5 @@ slides/
videos/ videos/
images/ images/
docs/build/

View File

@ -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
View File

@ -1,33 +1,74 @@
![Manim Slides Logo](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png)
[![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]
![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides) ![PyPI - Downloads](https://img.shields.io/pypi/dm/manim-slides)
# 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:
![manim-wizard](https://user-images.githubusercontent.com/27275099/197468787-19c83a81-d757-47b9-8f68-218427d30298.png)
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.
![](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif) ![](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif)
## 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%:
![Windows Fix Scaling](static/windows_quality_fix.png)
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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

27
docs/source/conf.py Normal file
View 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
View 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

View File

@ -1,3 +1,5 @@
# flake8: noqa: F403, F405
# type: ignore
import sys import sys
if "manim" in sys.modules: if "manim" in sys.modules:

View File

@ -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

View File

@ -1 +1 @@
__version__ = "3.2.3" __version__ = "4.5.0"

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,2 @@
[tool.vulture]
paths = ["manim_slides"]

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

41
static/logo.py Normal file
View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB