Compare commits

...

354 Commits

Author SHA1 Message Date
fc200f22f5 chore(version): bump 5.0.0 to 5.1.0-rc1
chore(version): bump 5.0.0-rc1 to 5.1.0

chore(version): bump 5.1.0 to 5.1.0-rc1
2023-11-28 15:21:03 +01:00
050ee0ae78 feat(lib): enhance notes support (#324)
* feat(lib): enhance notes support

TODO: fix keyboard inputs and window order with `present`.

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

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

* chore(lint): allow too complex

* wip: presenter mode

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

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

* feat(cli): add presenter view

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-28 15:15:56 +01:00
a9b8081167 [pre-commit.ci] pre-commit autoupdate (#326)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.7.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-28 09:48:44 +01:00
dc58d498a8 feat(cli): automatically detect conversion format from DEST (#325)
* feat(cli): automatically detect conversion format from DEST

As titled

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

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

* fix(cli): fix and add tests

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

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

* fix(ci): ?

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-27 16:13:40 +01:00
f898dd3054 feat(lib): add support for presenter notes (#322)
* feat(lib): add support for presenter notes

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

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

* fix(test): typo

* Update test_slide.py

* Update convert.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-23 18:20:10 +01:00
b09a000c17 chore(lib): simplify how to add config options (#321)
* chore(lib): simplify how to add config options

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

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

* chore(lint): some fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-23 15:38:49 +01:00
eb8efa8e3d feat(lib): add playback rate config options (#320)
* feat(lib): add playback rate config options

Basic playback rate config options, closes #309

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-23 12:26:25 +01:00
1f0c94dc5c fix(cli): typo 2023-11-23 00:10:56 +01:00
2771aac4d0 chore(cli): reduce timeout for version check 2023-11-22 18:28:57 +01:00
ce799aeded feat(cli): add render command as alias to manim render (#317)
* feat(cli): add render command as alias to `manim render`

* feat(cli): add render command as alias to `manim render`

* chore(test): add test

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

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

* chore(lib): simplify how ManimGL rendering is called

* chore(lib): test

* chore(ci): try identify error

* wip

* chore(test): add result output

* try: checkout

* Revert "try: checkout"

This reverts commit 60985f0b612ca0c6bff9726ebcccf07b1b1c4832.

* fix(deps): don't update lock

* chore(lib): simplify code and improve usage

* chore(cli): better cli

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

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

* fix: does this fix?

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-22 18:26:44 +01:00
891273b2fc [pre-commit.ci] pre-commit autoupdate (#318)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-21 17:42:41 +01:00
2b25b6a89d chore(docs): warn users about #314 (#319)
* chore(docs): warn users about #314

Closes #316

* chore(lint): apply suggested changes
2023-11-21 17:37:30 +01:00
fc594533e9 [pre-commit.ci] pre-commit autoupdate (#310)
updates:
- [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0)
- [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.5)
- [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-19 10:42:18 +01:00
422102524f fix(lib): remove dummy content 2023-11-16 13:07:29 +01:00
739cbcee0a feat(cli): added --hide-info-window option (#313)
* feat(cli): added `--hide-info-window` option

This was suggested by Discord use #dung_14424 on the Manim Discord.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-16 12:51:59 +01:00
3b3e3109a3 [pre-commit.ci] pre-commit autoupdate (#306)
updates:
- [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1)
- [github.com/astral-sh/ruff-pre-commit: v0.1.1 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.1...v0.1.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-12 11:01:36 +01:00
8921c3b8f9 chore(README): fix relative link for Windows image 2023-11-01 10:06:00 +01:00
5eb23dc5c1 chore(version): update changelog version 2023-10-30 10:54:14 +01:00
a890832a4d chore(version): bump 5.0.0-rc3 to 5.0.0 2023-10-30 10:52:53 +01:00
106c7d4c06 feat(cli): add next-terminates-loop CLI option (#299)
* feat(cli): add `next-terminates-loop` CLI option

Closes #254

* chore(lib): fix `--next-terminates-loop`

* chore(CHANGELOG): document changes
2023-10-30 10:50:26 +01:00
6c52906037 feat(lib): add auto_next option (#304)
* feat(lib): add `auto_next` option

As suggested in #302, you can now automatically skip a slide. It works both with `present` and `convert --to=html`!

Closes #302

* chore(ci): add trigger on push on main
2023-10-30 10:12:59 +01:00
2853ed08e1 chore(tests): adding tests for Qt widgets (#300)
* chore(tests): adding tests for Qt widgets

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

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

* fix: reset WINDOW name

* chore(tests): addign tests

* chore: adding more tests

* fix how bin existence is checked

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

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

* fix: which takes str

* more tests!

* todo: fix

* change verbosity in tests

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-27 16:40:05 +02:00
760ceb8ce1 [pre-commit.ci] pre-commit autoupdate (#298)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/psf/black: 23.9.1 → 23.10.0](https://github.com/psf/black/compare/23.9.1...23.10.0)
- [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.1)
- [github.com/pre-commit/mirrors-mypy: v1.6.0 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.0...v1.6.1)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-23 22:59:52 +02:00
2fa0301935 Merge remote-tracking branch 'origin/main' into main 2023-10-23 17:39:01 +02:00
61b983db3a chore(version): bump 5.0.0-rc2 to 5.0.0-rc3 2023-10-23 17:38:49 +02:00
56b1ffe430 fix(deps): PySide6 player black screen (#297)
Fixes #293
2023-10-23 17:38:24 +02:00
541bf96945 feat(lib): add Slide.next_section method (#295)
* feat(lib): add `Slide.next_section` method

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-23 17:34:20 +02:00
626764146a chore(docs): add explicit targets 2023-10-20 09:26:08 +02:00
0322dae743 feat(lib): add loop option to next_slide and remove start/end_loop (#294)
* feat(lib): add `loop` option to `next_slide` and remove `start/end_loop`

* fix(docs): PR number
2023-10-19 15:37:54 +02:00
7928f6020c Merge branch 'color' into main 2023-10-19 14:32:37 +02:00
f3dfa782b0 fix(convert): ClassVar was ignored by Pydantic (#292)
* fix(convert): ClassVar was ignored by Pydantic

Using default factory function instead.

* update comments
2023-10-19 14:31:39 +02:00
e13ca7e0dc chore(lib): use pydantic color 2023-10-19 14:17:30 +02:00
6c9505b98a fix(lib): use optional instead of mutable defaults (#291)
* fix(lib): use optional instead of mutable defaults

* fix: add missing check for none
2023-10-19 11:52:14 +02:00
7b3a5c4824 fix(ci): OpenGL issue on Windows server (#290)
* fix(ci): OpenGL issue on Windows server

Attempt at fixing OpenGL issue on Windows server

* fix(ci): unmark xfail

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-19 11:39:16 +02:00
5daa94b823 chore(ci): enhance current lint rules (#289)
* chore(ci): enhance current lint rules

* run linter

* fix all warnings

* fix(convert): release VideoCapture

* fix(ci): properly close file

* better tests

* fix(ci): setup opengl

* Revert "fix(ci): setup opengl"

This reverts commit a33f53a1c04f909d7660f2b5221c763a9ef97d53.

* fix(ci): skipif Windows in workflows

* actually xfail
2023-10-18 19:24:14 +02:00
498e33ad8d chore(deps): bump urllib3 from 2.0.6 to 2.0.7 (#288)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 23:33:25 +02:00
860ab231b5 chore(tests): add tests for converter methods (#283)
* chore(tests): add tests for converter methods

* chore(tests): update ppt converter test

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2023-10-17 22:33:01 +02:00
c075904a27 fix(ci): wrong logic 2023-10-17 16:58:59 +02:00
387d0f76b5 fix(convert): disallow empty list 2023-10-17 16:50:48 +02:00
1126a20785 fix(docs): remove return type 2023-10-17 16:22:56 +02:00
802f6406ae refactor(lib): change how manim API is imported (#285)
* refactor(lib): change how manim API is imported

* chore(lib): delete old files

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

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

* wip: moving all commands

* adding animations

* fix tests

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

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

* fix mypy

* fixes

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

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

* trying to fix docs

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

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

* wip: docs

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

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

* make it work

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

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

* wip test

* tests are working

* improving docs

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

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

* fix index

* docs: nicer shift

* docs: nicer quickstart example

* fix tests

* change tests

* move coverage to test workflow

* fix(tests): remove resolve

* strict resolve

* change local path test

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

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

* documented changes

* cleanup docs

* cleanup files

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

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

* fix(ci): set type

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-17 16:06:19 +02:00
685f871186 [pre-commit.ci] pre-commit autoupdate (#286)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.10.0 → v2.11.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.10.0...v2.11.0)
- [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-17 12:52:17 +02:00
da14b5f24a [pre-commit.ci] pre-commit autoupdate (#284)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-14 10:15:38 +02:00
18a9906ae5 chore(deps): bump urllib3 from 2.0.4 to 2.0.6 (#280)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.4...2.0.6)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-05 10:04:34 +02:00
a0ee723c89 [pre-commit.ci] pre-commit autoupdate (#281)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.291 → v0.0.292](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.291...v0.0.292)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-05 10:04:22 +02:00
2b7bd0a68d Add github template to sharing page (#279)
* chore(docs): add github template to sharing page

* chore(docs): update according to style requirements
2023-10-05 10:03:55 +02:00
497e4e964f fix(docs): changelog link 2023-10-01 21:43:09 +02:00
86fc774a3d [pre-commit.ci] pre-commit autoupdate (#274)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.290 → v0.0.291](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.290...v0.0.291)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-27 18:46:02 +02:00
ce14c79230 [pre-commit.ci] pre-commit autoupdate (#273)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.288 → v0.0.290](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.288...v0.0.290)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-19 08:44:45 +02:00
6272d3f7ec fix(cli/convert): ensure dest path can be written (#262)
Fixes the issue that non-existing parent(s) in the dest path would raise an error.
2023-09-15 11:29:30 +02:00
bbe8b96030 chore(version): bump 5.0.0-rc1 to 5.0.0-rc2 2023-09-14 11:31:13 +02:00
1bc8423381 chore(deps): bump RevealJS' default version to 4.6.1 (#272)
Add 3 new themes: dracula, dark-contrast and white-contrast.
2023-09-14 11:30:13 +02:00
67147442f3 chore(convert): use Jinja2 for templating (#271)
* chore(convert): use Jinja2 for templating

Use Jinja2 templating for more customizable rendering

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

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

* fix(deps): jinja2 is no more optional

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-14 11:21:20 +02:00
859d48ad2e [pre-commit.ci] pre-commit autoupdate (#270)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1)
- [github.com/astral-sh/ruff-pre-commit: v0.0.287 → v0.0.288](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.287...v0.0.288)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-12 16:03:46 +02:00
9a23296fa2 fix(docs): change example to not use LaTeX 2023-09-06 17:54:17 +02:00
0f5b374bce fix(docs): remove TeX deps in examples 2023-09-06 15:26:54 +02:00
2dc4c1ab99 chore(docs): update docs about data URI 2023-09-06 15:23:54 +02:00
f2ee29ad58 chore(docs): render subclass example 2023-09-06 15:12:29 +02:00
d127af9dd2 fix(docs): typo in rst directive 2023-09-06 10:45:38 +02:00
05ebf40543 chore(deps): bump actions/checkout from 3 to 4 (#264)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 19:22:43 +02:00
9cc7957e35 [pre-commit.ci] pre-commit autoupdate (#263)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.286 → v0.0.287](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.286...v0.0.287)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-09-05 19:22:24 +02:00
b72b7bc256 fix(docs): 404 file not found fixed 2023-09-05 19:21:58 +02:00
4a1b8aea87 fix(docs): casing 2023-08-29 23:22:46 +02:00
c2b12d16eb fix(docs): syntax hl 2023-08-29 16:36:11 +02:00
bb5b294f40 feat(lib): Sphinx directive can now read files (#261)
* feat(lib): Sphinx directive can now read files

You can optionally read a file instead of the Sphinx directive's content

* fix(lib): rst syntax and docs

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-29 16:23:54 +02:00
9a3a343231 [pre-commit.ci] pre-commit autoupdate (#260)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.285 → v0.0.286](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.285...v0.0.286)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-29 14:50:56 +02:00
0f07d36f52 fix(deps): patch color for future manim versions (#255) 2023-08-28 17:09:19 +02:00
806b7d00f6 fix(lib): correctly format enums on Python>=3.11 (#257)
Closes #256

fix(tests): update tests and fix


chore(lib): simplify fix and more tests


chore(docs): document patch
2023-08-24 13:22:04 +02:00
3d9522cbb0 chore(deps): remove notebook dependencies (#248)
* chore(deps): remove notebook dependencies

Unused optional dependency.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-22 11:25:33 +02:00
28c5336b83 [pre-commit.ci] pre-commit autoupdate (#247)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.284 → v0.0.285](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.284...v0.0.285)
- [github.com/pre-commit/mirrors-mypy: v1.5.0 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.0...v1.5.1)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-22 11:06:37 +02:00
48614105bd chore(version): bump 4.16.0 to 5.0.0-rc1 2023-08-21 22:15:15 +02:00
933afdd465 fix 2023-08-21 22:08:34 +02:00
599f9f22ae chore(ci): update bump config
fix(ci): regex


fix(ci): bump regex


fix


fix
2023-08-21 22:07:39 +02:00
5490a0a5ef chore(docs): add changelog to online docs 2023-08-21 21:34:37 +02:00
c875363b40 [ImgBot] Optimize images (#246)
*Total -- 5,933.58kb -> 1,375.26kb (76.82%)

/paper/docs.png -- 4,390.51kb -> 157.54kb (96.41%)
/static/example.gif -- 669.78kb -> 484.93kb (27.6%)
/static/logo_dark_github.png -- 112.68kb -> 86.31kb (23.4%)
/static/logo_dark_docs.png -- 111.55kb -> 86.00kb (22.9%)
/static/logo.png -- 109.63kb -> 89.99kb (17.91%)
/static/windows_quality_fix.png -- 34.61kb -> 29.19kb (15.65%)
/static/docs.png -- 209.36kb -> 177.73kb (15.11%)
/static/logo_light_transparent.png -- 116.05kb -> 102.36kb (11.79%)
/static/icon.png -- 2.03kb -> 1.79kb (11.61%)
/static/logo_dark_transparent.png -- 123.63kb -> 109.45kb (11.46%)
/static/wizard_light.png -- 26.28kb -> 24.23kb (7.8%)
/static/wizard_dark.png -- 27.48kb -> 25.72kb (6.43%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2023-08-21 20:53:39 +02:00
bd9bf06876 chore(cli): remove unused PERF verbosity level (#245)
* chore(cli): remove unused PERF verbosity level

As titled.

* fix(tests): remove perf in tests
2023-08-21 18:00:56 +02:00
4d76f2ccc1 chore(deps): bump tornado from 6.3.2 to 6.3.3 (#244)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.3.2 to 6.3.3.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.3.2...v6.3.3)

---
updated-dependencies:
- dependency-name: tornado
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 17:10:29 +02:00
8cf05ea44d chore(docs): correctly quote code 2023-08-21 17:10:13 +02:00
638616c94f feat(cli): rewrite presentation player (#243)
* wip: rewrite player

* wip(cli): new player

* wip(player): allow to close

* Auto stash before merge of "rewrite-player" and "origin/rewrite-player"

* feat(cli): new player

* chore(docs): document changes

* feat(cli): add info window
2023-08-21 16:50:03 +02:00
b321161717 chore(lib/cli): one video per slide (#242)
* chore(lib/cli): one video per slide

As titled, this PR changes how Manim Slides used to work by only storing one video file per slide.

Previously, a slide would store all animations that occur during the given slide. Up to now, the only "advantage" of this was that it would allow the user to know which animation is played.
But, at the cost of a very complex logic in present, just especially for reversed slides.

On top of top, all converter actually need to concatenate the animations from each slide into one, so it is now performed at rendering time.

To migrate from previous Manim Slides versions, the best is the render the slides again, using `manim render` or `manimgl render`.

Currently, it is not possible to start at a given animation anymore. However, if wanted, I may re-implement this, but this would require to change the config file again.

* fix(ci): trying to fix tests

* chore(test): renaming files

* chore(docs): remove old line from changelog

* fix(docs): typo

* fix(ci): manimgl and smarter comparison

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-20 19:40:39 +02:00
7363281ff0 [pre-commit.ci] pre-commit autoupdate (#241)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.282 → v0.0.284](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.282...v0.0.284)
- [github.com/pre-commit/mirrors-mypy: v1.4.1 → v1.5.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.4.1...v1.5.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-15 09:50:02 +02:00
d056d8d8b1 chore(version): bump 4.15.0 to 4.16.0 2023-08-08 14:45:03 +02:00
c95929dc7d fix(ci): version bump 2023-08-08 14:44:59 +02:00
e08edb6fe1 chore(docs): make 2nd example more different 2023-08-08 14:30:43 +02:00
efc3017df8 chore(paper): add paper's doi, update badges and add CFF 2023-08-08 14:13:29 +02:00
788727ea22 Merge branch 'optional' into main 2023-08-08 10:47:37 +02:00
0bb0285b18 fix(docs): typo in magics name 2023-08-08 10:46:44 +02:00
ce878bece2 [pre-commit.ci] pre-commit autoupdate (#239)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.281 → v0.0.282](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.281...v0.0.282)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-08 09:47:39 +02:00
1275e9119f chore(deps): make all groups optional (#238)
* chore(deps): make all groups optional

* chore(deps): update lock file

* chore(ci): run apt-get update
2023-08-07 18:25:06 +02:00
5555ac2e54 chore(ci): run apt-get update 2023-08-07 18:14:42 +02:00
838de83c75 chore(deps): update lock file 2023-08-07 18:07:22 +02:00
f1f7146c7e chore(deps): make all groups optional 2023-08-07 18:06:12 +02:00
fb02764bb7 feat(lib): add Jupyter magic (#237)
* feat(lib): add Jupyter magic

And also use the same logger level as manim (by default)

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

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

* fix(lib): remove deleted module

* chore(lib): fix typing issues

* chore(docs): document magic

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

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

* fix(ci): install kernel

* fix(ci): spawning is not necessary (and fails)

* chore(ci): add ipykernel

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-07 17:47:03 +02:00
f6f2e4090f fix(lib): fix import * (typo in name) 2023-08-07 10:11:16 +02:00
146a2f7839 chore(paper): fix brackets
As proposed by @labarba
2023-08-06 21:38:08 +02:00
d282766f2d feat(convert): support base64 encoded videos (#236)
* feat(convert): support base64 encoded videos

Thanks to @t-fritsch, Manim Slides can now convert to a fully self-contained HTML presentation using base64 encoded videos!

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

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

* chore(lib): explicit decode type

* feat(lib): auto detect mime type

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-06 19:06:43 +02:00
dec2f5e724 chore(docs): actually apply my own recomm. in example :-) 2023-08-04 16:49:37 +02:00
4d44166677 chore(version): bump 4.14.0 to 4.15.0 2023-08-04 16:17:50 +02:00
455f104a11 fix(present): finish up state machine (#234) 2023-08-04 16:17:21 +02:00
0f9048a27b fix(present): finish up state machine 2023-08-04 16:03:52 +02:00
9e1e0f2367 fix(lib): correctly caching and simplified structure (#233) 2023-08-04 13:37:27 +02:00
097ae8ffdd feat(present): fix presentation size and add screen option (#232)
* feat(present): fix presentation size and add screen option

This kinda fixes the issue of weird resizes between two presentations

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

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

* fix(ci): typing

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-04 13:33:21 +02:00
aace5dea11 fix(lib): correctly caching and simplified structure 2023-08-04 13:32:51 +02:00
738c54f9a6 chore(ci): ignore type 2023-08-04 11:15:38 +02:00
f01f811639 chore(present): inform about SIGINT 2023-08-04 11:14:24 +02:00
94265f6842 chore(present): warn about old configuration file 2023-08-04 10:46:35 +02:00
6ed76ffd01 fix(lib): correct key attribute 2023-08-04 10:45:19 +02:00
71df62d79b chore(docs): line length too long 2023-08-04 09:55:01 +02:00
fc36909688 chore(tests): zoom example 2023-08-04 09:54:42 +02:00
580da4a885 chore(docs): add some more indications 2023-08-02 16:59:02 +02:00
0de877e43a fix(docs): typo in RST 2023-08-02 16:48:59 +02:00
7313e3e0d4 feat(lib): add zoom animation (#230)
* feat(lib): add zoom animation

* chore(lib): fix zoom animation
2023-08-02 13:47:38 +02:00
1e967894db fix(lib): check if reversed file exist (#231)
This fixes a bug that would block the process forever because FFMPEG was asking if it could overwrite or not.
2023-08-02 12:54:00 +02:00
98fa5349d3 [pre-commit.ci] pre-commit autoupdate (#229)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.280 → v0.0.281](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.280...v0.0.281)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-08-01 20:56:40 +02:00
0e3ed3f9eb chore(deps): bump certifi from 2023.5.7 to 2023.7.22 (#228)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 09:33:12 +02:00
a45242236d chore(docs): add sphinx extension link 2023-07-25 14:39:47 +02:00
66451473b8 [pre-commit.ci] pre-commit autoupdate (#226)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.278 → v0.0.280](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.278...v0.0.280)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-25 10:29:38 +02:00
72152bd625 docs: fixed typo (#227)
Change link titled "HTML Presenetation" (typo) to "HTML Presentation".
2023-07-25 10:29:21 +02:00
88bffe0f0b chore(lib): remove all os.path in favor to pathlib (#225)
* chore(lib): remove all os.path in favor to pathlib

* fix(lib): str to path
2023-07-24 15:53:30 +02:00
a7719dbb8b feat(lib): change config file from json to toml (#224)
This PR introduces a **BREAKING CHANGE**: the general config file is now written in the TOML format. Keys are also placed under a shared subsection. Previous config files will be ignore.

Migration from the previous format can be easily performed with `manim-slides init` and copy/pasting the key codes if necessary.
2023-07-24 14:46:15 +02:00
529a6c534f fix(lib/cli): relative paths, empty slides, and tests (#223)
* fix(lib/cli): relative paths, empty slides, and tests

This fixes two issues:

1. Empty slides are now reported as error, to prevent indexing error;
2. Changing the folder path will now produce an absolute path to slides, which was not the case before and would lead to a "file does not exist error".

A few tests were also added to cover those

* fix(lib): fix from_file, remove useless field, and more

* chore(tests): remove print

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-24 13:58:54 +02:00
2b6240c4d3 chore(tests): test commons and defaults (#222)
Add new tests to improve overall coverage
2023-07-24 11:28:40 +02:00
d98d41aaa8 fix(lib/cli): fix validation error (#221) 2023-07-24 10:38:35 +02:00
d892a4e77d chore(docs): add fadeout 2023-07-20 17:36:55 +02:00
8069ab5405 chore(docs): improve example animation 2023-07-20 17:35:36 +02:00
f9e22fe63c chore(tests): improve test coverage (#220)
* chore(tests): improve test coverage

* chore(ci): fix display connection for manimgl

* chore(ci): same but for code coverage
2023-07-20 16:17:51 +02:00
b195b823ba chore(docs): improve directive's documentation 2023-07-20 13:59:28 +02:00
fc8717fa9c chore(docs): select docs members 2023-07-20 13:48:54 +02:00
dbbd6813ec feat(lib): add canvas and features (#219)
* feat(lib): add canvas and features

Add canvas to slide

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-20 13:38:18 +02:00
a10902eaaa feat(lib): add wait-time-between-slides attribute (#218)
Add a new attribute that will allow to automatically call `self.wait(...)` between two slides, to fix graphical issues.
2023-07-20 12:02:59 +02:00
f4c1c34994 feat(lib): improve wipe animation (#217)
* feat(lib): improve wipe animation

Improve wipe transition by using FadeIn and FadeOut. The also allows to support introducers and removers.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-20 11:27:15 +02:00
540c7034c8 feat(lib/docs): add sphinx directive (#216)
This PR adds a new Sphinx directive, inspired by the Manim one. This directive is also used within the docs to display examples.
2023-07-19 17:11:58 +02:00
86aeeb861b feat(lib): add wip transition (#215) 2023-07-19 15:13:08 +02:00
979e2c549a [pre-commit.ci] pre-commit autoupdate (#212)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.9.0 → v2.10.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.9.0...v2.10.0)
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
- [github.com/astral-sh/ruff-pre-commit: v0.0.276 → v0.0.278](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.276...v0.0.278)

* chore(ci): apply ruff's suggestion

* [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>
2023-07-19 12:20:01 +02:00
4e4e29380b chore(lib): fix typo in warning message (#214) 2023-07-19 12:19:27 +02:00
caa4c48fe7 chore(deps): bump actions/upload-pages-artifact from 1 to 2 (#213)
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 1 to 2.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 12:02:31 +02:00
a353de270e chore(docs): fix typo 2023-07-07 17:18:55 +02:00
60f284e748 Update index.md (#210)
Small typo
2023-07-06 23:28:21 +02:00
501af3b658 chore(README): update token 2023-07-05 14:50:32 +02:00
f820819896 chore(ci): add coverage report 2023-07-05 14:46:42 +02:00
9279d2a22a chore(deps): bumping pydantic to V2 (#207)
* chore(deps): bumping pydantic to V2

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

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

* fix(lib): validators

* fix(ci): add tests and fixes

* fix(lib): add missing mode arg

* fix(lib): change function name

* chore(tests): add more tests and use pytest

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

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

* chore(deps): change test deps

* chore(ci): install manim deps

* fix(ci): move to right place

* fix(lib): add custom schema

* fix(lib): validators

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-05 14:35:58 +02:00
e1d5fb732c [pre-commit.ci] pre-commit autoupdate (#206)
updates:
- https://github.com/charliermarsh/ruff-pre-commithttps://github.com/astral-sh/ruff-pre-commit
- [github.com/astral-sh/ruff-pre-commit: v0.0.275 → v0.0.276](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.275...v0.0.276)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-07-05 10:05:55 +02:00
384af332d8 chore(deps): bump actions/checkout from 1 to 3 (#203)
Bumps [actions/checkout](https://github.com/actions/checkout) from 1 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-29 14:02:21 +02:00
c231f4d100 [pre-commit.ci] pre-commit autoupdate (#204)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.272 → v0.0.275](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.272...v0.0.275)
- [github.com/pre-commit/mirrors-mypy: v1.3.0 → v1.4.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.3.0...v1.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-29 14:02:12 +02:00
751eae74e9 chore(deps): bump actions/upload-artifact from 1 to 3 (#201)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 10:27:41 +02:00
97fe80caa2 chore(deps): bump actions/configure-pages from 2 to 3 (#200)
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 2 to 3.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 10:27:29 +02:00
a7eea6fbea [pre-commit.ci] pre-commit autoupdate (#199)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.270 → v0.0.272](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.270...v0.0.272)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-13 10:27:16 +02:00
04e2f265f6 chore(deps): bump actions/deploy-pages from 1 to 2 (#202)
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 1 to 2.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 10:27:02 +02:00
8096636cf1 chore(version): bump 4.13.2 to 4.14.0 2023-06-12 14:40:38 +02:00
c7e38bfb38 feat(convert): add to PDF conversion (#197)
* feat(convert): add to PDF conversion

Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page.

Closes #196

* chore(ci): remove experimental installer

* feat(convert): add to PDF conversion

Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page.

Closes #196

* feat(convert): add to PDF conversion

Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page.

Closes #196

* chore(deps): update lockfile
2023-06-12 14:28:20 +02:00
421cad3038 chore(deps): bump requests from 2.29.0 to 2.31.0 (#190)
Bumps [requests](https://github.com/psf/requests) from 2.29.0 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.29.0...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 13:53:31 +02:00
9edf23856c chore(deps): upgrading to qt6.5 (#198)
* chore(deps): upgrading to qt6.5

This PR proposes to update to PySide6.5.

On Ubuntu, you may face this problem (solution included): https://bugreports.qt.io/browse/PYSIDE-2306

* chore(ci): remove experimental installer
2023-06-12 13:46:15 +02:00
62236f5796 [pre-commit.ci] pre-commit autoupdate (#195)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.269 → v0.0.270](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.269...v0.0.270)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-09 11:33:48 +02:00
1e28d70c0e chore(README): add zenodo badge 2023-05-25 18:00:11 +02:00
6a96b3ab8c chore(version): bump 4.13.1 to 4.13.2 2023-05-25 17:47:50 +02:00
a1c041db80 chore(paper): add comparison section (#192)
Closes #188
2023-05-24 09:04:05 +02:00
4fd3452f95 chore(cli): fix and improve help messages (#191)
A very small PR to fix an error in an help message, and improve the verbosity one.
2023-05-23 10:42:23 +02:00
ff2be6851b [pre-commit.ci] pre-commit autoupdate (#189)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.267 → v0.0.269](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.267...v0.0.269)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-23 10:41:57 +02:00
95289ee7a5 [pre-commit.ci] pre-commit autoupdate (#187)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.8.0 → v2.9.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.8.0...v2.9.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.265 → v0.0.267](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.265...v0.0.267)
- [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-18 17:19:31 +02:00
f1a026208a chore(docs): document scene subclassing (#186) 2023-05-11 19:43:32 +02:00
b3fd1d209e [pre-commit.ci] pre-commit autoupdate (#184)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.263 → v0.0.265](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.263...v0.0.265)

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

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

* fix(ci): correct path

* fix(ci): fix path..

* chore(ci): add debug

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

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

Closes #171

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

* chore(ci): publish wheels too

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

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

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

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

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

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

---------

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

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

Closes #169

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

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

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

---------

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

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

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

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

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

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

* chore(version): setup dummy version


chore(version): fix

* chore(ci): udpate version in __version__ too

* chore(version): revert changes

---------

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

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

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

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

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

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

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

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

* Minor changes to feature: Read scene background

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

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

* Small fix for feature "Read bg color"

* chore(ci): add typing ignore

* fix(ci): typo

---------

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

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

Closes #161

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

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

---------

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

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

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

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

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

* feat(cli): add start at index options

* fix(ci): correct callback

* chore(ci): fix type hint

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

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

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

---------

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

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

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-10 17:49:37 +01:00
149b12fd01 chore(lib): raise error if skipping
Temporary error before implementing this
2023-03-09 17:29:26 +01:00
e01be300a0 chore(version): bump 4.9.2 to 4.10.0 2023-03-09 14:16:43 +01:00
940916d4aa chore(lib): some fixes before new release 2023-03-09 14:06:02 +01:00
3da8fab145 chore(deps): remove pkg_resources in favor to importlib (#153)
* feat(convert): PowerPoint conversion 

You can now convert your presentations to PowerPoint

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

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

* chore(convert): autoplay media

* chore(deps): remove pkg_resources in favor to importlib

This is what pkg_resources' team recommends

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

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

* fix(deps): if case for Python 3.8 (<3.9)

* fix(convert): use correct pkg path

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

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

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

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

* fix(deps): remove duplicate deps

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

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

* fix(lib): add __init__.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-09 13:41:25 +01:00
f0c5d48107 feat(convert): PowerPoint conversion (#152)
* feat(convert): PowerPoint conversion 

You can now convert your presentations to PowerPoint

* fix(deps): push poetry lock

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

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

* fix(lint): ignore attr-defined because Windows-only

* chore(convert): autoplay media

* fix(convert): autoplay and autoloop

* chore(deps): update lock file

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

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

* feat(convert): use first frame as default poster frame

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

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

* chore(docs): document new feature

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-09 12:12:45 +01:00
426470ef3c chore(lib): use next_slide not pause (#151)
* chore(lib): use `next_slide` not `pause`

This deprecates `pause` function in favor to `next_slide`, that will also be called by `next_section` in the future.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-08 16:57:10 +01:00
700584cbcc chore(lib): reduce import overhead (#147)
* chore(lib): reduce import overhead

This PR should reduce the import time overhead caused by manim imports. To solve this, manim is only imported when Slide or ThreeDSlide is needed. A custom logger is now defined, which copies the one from Manim Community. FFMPEG_BIN is now hardcoded, but should be configurable in the future via the CLI or some config file.

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

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

* fix(lib): remove last .manim import

* fix(lib): remove print

* chore(lib): fix typo

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-08 16:56:51 +01:00
a440da9468 [pre-commit.ci] pre-commit autoupdate (#148)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.253 → v0.0.254](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.253...v0.0.254)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-07 11:53:09 +01:00
6486ce147c fix(docs): small typo 2023-03-04 10:12:35 +01:00
b258deeb31 fix(style): no backstick 2023-03-03 23:01:02 +01:00
a32773c50f chore(docs): adding GUI and HTML documentation pages (#145)
* chore(docs): adding GUI and HTML documentation pages

As titled, this adds two pages to the docs

* fix(typo): languagetool suggestion
2023-03-03 21:56:00 +01:00
a16aa93ee6 chore(misc): JOSE paper (#144)
* chore(misc): JOSE paper

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

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

* fix(ci): update path

* fix(ci): tab error

* fix(ci): path to paper

* fix(paper): updates

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

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

* fix(paper): typos

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-03 16:34:35 +01:00
e809e64f9a fix(docs): typo in commands 2023-03-02 13:36:14 +01:00
5967760dc3 feat(cli): using cached files when possible (#142)
* feat(cli): using cached files when possible

This should improve a bit the overall performances

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-02 13:33:58 +01:00
7f824be682 chore(README): force links in badges 2023-03-02 13:32:57 +01:00
9346f199d7 chore(README): add docs badge and update wizard (#143) 2023-03-02 13:24:58 +01:00
5c40dc69d8 chore(docs): transparent logo and symbolic link (#141) 2023-03-02 12:47:42 +01:00
bf10068cfc [pre-commit.ci] pre-commit autoupdate (#140)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.249 → v0.0.253](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.249...v0.0.253)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-28 08:02:49 +01:00
2f307225d1 chore(ci): clear cache (#138)
* chore(ci): clear cache

We must force cache clearing to save a new media/ directory

* chore(ci): clear cache when PR is closed

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

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

* fix(ci): add write permission

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-27 11:24:50 +01:00
8b5db4b2fd chore(docs): add dark-themed logo (#137)
* chore(docs): add dark-themed logo

* fix(docs): swap themes
2023-02-26 12:29:43 +01:00
855c74de34 chore(version): bump 4.9.1 to 4.9.2 2023-02-26 00:17:12 +01:00
a70876d696 fix(convert): relative path in HTML files (#136)
This fixes an error introduced by #133
2023-02-26 00:11:12 +01:00
3cb0085f24 chore(version): bump 4.9.0 to 4.9.1 2023-02-25 17:41:09 +01:00
42d70380b0 chore(docs): add embed instructions (#135) 2023-02-25 17:29:42 +01:00
dc1be25e6e chore(lib): use pathlib.Path instead of str (#133)
* wip(lib): change os.path to pathlib.Path

* chore(lib): use pathlib.Path instead of str

* fix(logger): convert Path to str

* chore(lint): add type hint to prevent future errors

* fix(lib): correct suffix addition
2023-02-25 17:21:50 +01:00
4cd433b35a chore(docs): document sharing slides (#134)
* chore(docs): remove deprecated "last animation" mention

* chore(docs): document sharing slides
2023-02-25 13:30:12 +01:00
e83df48c5d chore(version): bump 4.8.4 to 4.9.0 2023-02-25 11:37:17 +01:00
ed30e2136a Add Feature BackgroundSize (#132) 2023-02-24 17:51:33 +01:00
a9f5355595 chore(deps): bump ipython from 8.9.0 to 8.10.0 (#126)
Bumps [ipython](https://github.com/ipython/ipython) from 8.9.0 to 8.10.0.
- [Release notes](https://github.com/ipython/ipython/releases)
- [Commits](https://github.com/ipython/ipython/compare/8.9.0...8.10.0)

---
updated-dependencies:
- dependency-name: ipython
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-24 12:44:49 +01:00
1ef42ec82a chore(deps): bump markdown-it-py from 2.1.0 to 2.2.0 (#129)
Bumps [markdown-it-py](https://github.com/executablebooks/markdown-it-py) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/executablebooks/markdown-it-py/releases)
- [Changelog](https://github.com/executablebooks/markdown-it-py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/markdown-it-py/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: markdown-it-py
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-24 09:04:56 +01:00
b5f6a165db [pre-commit.ci] pre-commit autoupdate (#127)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.6.0 → v2.7.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.6.0...v2.7.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.243 → v0.0.249](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.243...v0.0.249)
- [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-21 08:38:54 +01:00
7b9c9b0c39 fix(lib): a class can now have zero slide and work (#125)
* fix(lib): a class can now have zero slide and work

This fixes a previous issue that occured when a class didn't have any `pause`

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-08 18:58:54 +01:00
ac23949043 chore(version): bump 4.8.3 to 4.8.4 2023-02-08 11:01:38 +01:00
71564a4c2e fix(convert): use hash to restrict the length of new filenames (#124)
Closes #123
2023-02-08 11:00:53 +01:00
b06250056d [pre-commit.ci] pre-commit autoupdate (#121)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
- [github.com/charliermarsh/ruff-pre-commit: v0.0.237 → v0.0.243](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.237...v0.0.243)

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-08 09:32:03 +01:00
43c24d7ae1 fix(docs): update base site url 2023-02-01 11:47:02 +01:00
35195f89e4 chore(version): bump 4.8.2 to 4.8.3 (#119)
* chore(version): bump 4.8.2 to 4.8.3

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

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

* chore(ci): always use apt-get update

See comment https://github.com/actions/runner-images/issues/6488#issuecomment-1295789463

* chore(docs): add opengraph image and update description

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-01 11:29:00 +01:00
2d7a67addf chore(license): change to MIT (#118)
* chore(license): change to MIT

After acceptance from owner of manim-presentation (via Telegram), I am changing the license to MIT.

* chore(ci): change license info
2023-01-31 17:58:13 +01:00
5b212ecccb fix(docs): add missing argument 2023-01-31 10:44:55 +01:00
f15a3e9b59 chore(version): bump to 4.8.2 2023-01-31 10:41:55 +01:00
205972125c chore(docs): improving the docs a bit more (#116)
* chore(docs): improving the docs a bit more

This adds some useful extensions, links, and add. information.

* fix(deps): update lockfiles no-cache
2023-01-31 10:29:21 +01:00
e9d28dc0a8 feat(lib): remove constraint on last animation and update readme (#117)
This removes the constraint that required a Slide class to end with a animation. This was actually not needed, and could lead to confusion since `self.wait` is not an animation with ManimGL, but well with Manim.

This fix, however, still means that a calls to `self.wait` with ManimGL, after last `self.pause` call, will be ignored.
2023-01-31 10:07:59 +01:00
70b5ee39c3 chore(docs): fix missing image, app. order and link 2023-01-30 22:45:43 +01:00
616e025867 fix(ci): missing indent 2023-01-30 22:25:40 +01:00
0ce4c18519 chore(docs): improving the docs (#115)
* chore(docs): improving the docs

This improves the docs as suggested by some reddit user. Now, most of Manim is documented in one place

* try: update lockfile

* chore(deps): remove duplicate key

* fix(ci): isort issue

See https://github.com/home-assistant/core/issues/86892

* fix(ci): bad identation

* fix(ci): fixing lock file

* fix(example): issue when ManimGL does not count wait as animation
2023-01-30 22:23:53 +01:00
68ff5269eb chore(ci): add yaml and toml formatters (#114)
* chore(ci): add yaml and toml formatters

* fix(ci): do not format poetry.lock

* chore(lib): update poetry.lock
2023-01-30 19:11:58 +01:00
753f4e788b chore(ci): improve docs build (#113)
* chore(ci): improve docs build

* fix(ci): trying to fix cache dir

* chore(ci): split caching in two steps
2023-01-30 14:05:31 +01:00
f1f98bf241 fix(ci): build docs using poetry env (#112) 2023-01-30 09:27:15 +01:00
4b413c1528 [pre-commit.ci] pre-commit autoupdate (#110)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.223 → v0.0.230](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.223...v0.0.230)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-24 19:01:34 +01:00
478e1d7d76 [pre-commit.ci] pre-commit autoupdate (#108)
updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.0.219 → v0.0.223](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.219...v0.0.223)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-01-17 07:02:23 +01:00
2b224530ab chore(lint): remove flake8 in favor to ruff (#103)
* chore(lint): apply suggested for flake8-black compat.

This applies the changes suggested by the Black documentation.

* from flake8 to ruff
2023-01-12 17:42:10 +01:00
cd7a054cf1 chore(version): bump to 4.8.1 (#102) 2023-01-01 19:27:53 +01:00
1ff2330ff2 fix(convert): correctly quote paths when using ffmpeg on Windows
With the current version of ffmpeg on Windows, the list file must enclosed by single quotes.
2023-01-01 19:12:45 +01:00
1e150bbb84 [pre-commit.ci] pre-commit autoupdate (#100)
updates:
- [github.com/pycqa/isort: v5.11.3 → 5.11.4](https://github.com/pycqa/isort/compare/v5.11.3...5.11.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-27 08:47:36 +01:00
13f19649aa [pre-commit.ci] pre-commit autoupdate (#99)
updates:
- [github.com/pycqa/isort: 5.11.0 → v5.11.3](https://github.com/pycqa/isort/compare/5.11.0...v5.11.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-20 11:09:07 +01:00
4c97bdd3a3 [pre-commit.ci] pre-commit autoupdate (#97)
updates:
- [github.com/pycqa/isort: 5.10.1 → 5.11.0](https://github.com/pycqa/isort/compare/5.10.1...5.11.0)
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-14 08:51:54 +01:00
777ff444a3 chore(cli): clean debug print 2022-12-09 09:48:36 +01:00
9cb1c35f00 chore(ci): move mypy config to pyproject.toml (#94) 2022-12-08 11:53:56 +01:00
1fed193cb3 chore(ci): move black config to pyproject.toml (#93)
* chore(ci): move black config to pyproject.toml

* chore(deps): update target version
2022-12-08 11:34:20 +01:00
9f227936f7 chore(lib): add link for audio blocking 2022-12-08 11:19:47 +01:00
2fe6139d18 chore(ci): move isort config to pyproject.toml (#92)
* chore(ci): move isort config to pyproject.toml

* fix(ci): typo in config
2022-12-08 11:16:54 +01:00
54f2c60c4e chore(deps): use carret version requirements (#91) 2022-12-08 10:25:16 +01:00
9810425ff2 chore(version): update version 2022-12-07 17:19:19 +01:00
3dc543e3a6 chore(docs): update README and fix typo 2022-12-07 17:17:07 +01:00
c0c73ad4d4 feat(cli): feally featured RevealJS template (#80)
* feat(cli): feally featured RevealJS template

This adds an option for every possible RevealJS option. Error messages are also improved.

RevealJS version is bumped to latest (4.4.0).

* feat(cli): add primitive support for arbitrary JS functions

* fix(cli): some typos / issues in template

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

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

* fix(lib): pickling issue

* fix(lib): ignore typing error due to __reduce_ex__

* feat(cli): add template selection, fixes first slide bug, and rm stuff

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-12-07 16:27:23 +01:00
a82ca81dc5 chore(deps): add dependency groups (#89)
This uses poetry's dependency groups to be more rigorous about modules version.
2022-12-06 14:56:18 +01:00
a68a4e1517 feat(cli): speedup scaling with painter scaling (#88)
As suggested by PySide6's documentation, we avoid using pixmap.scaled() on every frame, but use scaled content to resize the image. This seems to ignore ratio, explaining why we need a new option flag.
2022-12-06 12:06:35 +01:00
519dd47ac6 chore(version): update version 2022-12-05 22:15:03 +01:00
0565a99639 chore(version): fixing all CI tests (#76)
* fix(ci): add Ubuntu dep and change pip install

* fix(ci): typo

* fix(ci): add missing deps for Ubuntu

* fix(ci): typo in pkg name

* fix(ci): trying to install manimpango

* fix(ci): append bin to path on MacOS

* fix(ci): install Python setup tool

* try(ci): trying some stuff

* fix(ci): remove useless pkgs

* fix(ci): check manimpango and remove pyopengl

* try(ci): try fix

* try(ci): cleaner workflow

* fix(ci): missing dollar

* try(ci): minimal platform

* try(ci): test

* fix(ci): reset offscreen option

* fix(ci): add opengl dep

* fix(ci): xvfb for pyglet

* fix(ci): correctly set env var

* try(ci): set DISPLAY env var at the beginning

* test(ci): switch minimal to offscreen

* test(ci): remove QT debug env var

* try(ci): fixing manimgl issue

* Revert "try(ci): fixing manimgl issue"

This reverts commit f76c05897013bd804810c7153953bde4c8715af7.

* try(ci): manimgl to manim-render

* try(ci): verbose

* fix(ci): update linux deps

* fix(ci): typo

* fix(ci): typo in deps

* fix(ci): typo

* fix(ci): test other deps

* test(ci): install manimpango

* try(ci): test

* test(ci): print help

* test(ci): reset verbose mode

* fix(ci): typo

* chore(setup): use poetry

* chore(setup): cache installs

* fix(ci): swap order

* fix(ci): poetry install

* chore(setup): add manim/manimgl to dev-deps

* try(ci): some test

* try(ci): import two maybe conflicting packages

* fix(ci): typo in cmd

* fix(ci): only check if manimgl

* fix(ci): remove useless check
2022-12-05 22:13:44 +01:00
8dfe600656 chore(version): release 4.7.0 2022-12-03 16:09:09 +01:00
03107867ab chore(deps): drop support for Python 3.7 (#84)
* chore(deps): drop support for Python 3.7

In link with the recent release of [Manim v0.17.0](https://github.com/ManimCommunity/manim/releases/tag/v0.17.0), Manim Slides drops support for Python 3.7.

* feat(cli): check for newer Manim Slides version

Uses same logic as manim to check for a new version

* fix(lib): reset correct version

* chore(setup): remove 3.7 classifier
2022-12-03 14:51:41 +01:00
bf64962c46 feat(cli): check for newer Manim Slides version (#85)
* chore(deps): drop support for Python 3.7

In link with the recent release of [Manim v0.17.0](https://github.com/ManimCommunity/manim/releases/tag/v0.17.0), Manim Slides drops support for Python 3.7.

* feat(cli): check for newer Manim Slides version

Uses same logic as manim to check for a new version

* fix(lib): reset correct version

* fix(ci): add type stubs for requests
2022-12-03 14:51:00 +01:00
97e7bf8cb0 chore(speed): avoid unnecessary color conversion (#83)
* chore(speed): avoid unnecessary color conversion

This speeds up a bit the presentation by avoiding color conversion from BGR (OpenCV) to RGB.

* [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-12-02 18:02:54 +01:00
1bca2683e1 refactor(lib): rename main to __main__ (#82)
* refactor(lib): rename `main` to `__main__`

This allows to call the module using `python -m manim-slides`, useful for profiling, etc.

* fix(setup): update name
2022-12-02 17:24:57 +01:00
d6bb82261c chore(setup): move setup config to pyproject.toml (#78)
* chore(setup): move setup config to pyproject.toml

* fix(ci): modify build steps

* fix(ci): remove / exclude useless stuff

* fix(ci): exclude does not work
2022-11-29 10:57:25 +01:00
0c682e4ec9 [pre-commit.ci] pre-commit autoupdate (#77)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-29 08:54:41 +01:00
2f0453c9a6 chore(lib): add missing type hints and mypy is happy! (#75)
* chore(lib): add missing type hints and mypy is happy!

Closes #34

* fix(ci): add missing dep for mypy
2022-11-28 14:28:42 +01:00
85ea9f3096 Fix flake8 check errors: unused imports, tab warning (#68)
* fix: unused imports, hide tab warning for the str

* refactor(defaults.py): move revealjs_template to separate file

* fix(lib): move data files and use pkg_resources

* fix(lib): remove unused and unexisting import

* fix(ci): only test conversion with Manim

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

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

* fix(ci): test ManimGL on Python 3.10, not 3.11

* fix(lib): include package data in setup.py

* fix(ci): no fail-fast

* fix(ci): typo

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-11-28 11:39:34 +01:00
1ae8db7966 chore(python): support and tests for Python 3.11 (#74)
Closes #71
2022-11-28 10:26:13 +01:00
82cccc3fc2 [pre-commit.ci] pre-commit autoupdate (#73)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-23 12:43:35 +01:00
726b0abf5a docs: correct virtual environment information (#72)
* docs: correct virtual environment information

* fix: format

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2022-11-18 07:29:24 +01:00
80f4f4e3f7 Remove redundant black hook & specify minimal supported python version for hooks (#70)
* fix(ci): remove duplicated hook

* feat(ci): specify minimal supported python version - hooks
2022-11-16 23:48:12 +01:00
82eebae686 fix(ci): setup minimal python version for flake8 (#69) 2022-11-16 10:07:37 +01:00
7367cc2cb5 [pre-commit.ci] pre-commit autoupdate (#67)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-15 08:07:14 +01:00
f26541eb32 chore(version): update version 2022-11-09 16:14:34 +01:00
06890ceacd fix(ci): typo in keyword args 2022-11-09 16:06:50 +01:00
9aa715a0e4 feat(cli): add convert option to generate html presentations (#66)
* wip(cli): convert slides to html using RevealJS

* wip: convert - almost fully working

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

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

* fix: remove unused file

* fix: add last slides in now performed during rendering

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

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

* chore(ci): testing ConvertExample too

* fix: ManimGL does not consider wait as an animation

* [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-11-09 15:59:19 +01:00
a373bdb460 feat(cli): add app icon (#64)
* feat: add icon

* feat: add app icon

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

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

* fix(lint): ignore resources file with mypy

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-02 10:47:37 +01:00
e3e79617c0 feat(cli): read environ variable for verbosity (#63)
* feat(cli): read environ variable for verbosity

* [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 21:48:17 +01:00
668de2c023 fix(ci): try fix 2022-10-31 16:22:58 +01:00
929caec018 fix(ci): another try 2022-10-31 15:59:39 +01:00
48cc3343bd fix(ci): try installing opengl 2022-10-31 15:54:06 +01:00
144e7dac5b fix(ci): trying another solution 2022-10-31 15:37:06 +01:00
8b56f42183 fix(ci): trying to fix build problem 2022-10-31 15:33:34 +01:00
534bc21672 fix(ci): remove impossible caching 2022-10-31 15:26:39 +01:00
b1a8768963 fix(ci): fixing workflows and caching 2022-10-31 15:21:58 +01:00
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
1da3492732 chore(ci): test builds on multiple oses (#7)
* chore(ci): test builds on multiple oses

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

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

* fix: add tqdm to setup

* chore(ci): test builds on multiple oses

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

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

* fix: install deps in two steps

* fix(bug): allows shape to be 2 or 3 dims

* release new version

* chore(ci): test builds on multiple oses

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

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

* chore(ci): test builds on multiple oses

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

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

* fix: install deps in two steps

* fix: deps string

* fix: ci

* fix: single quote strings

* fix: choco install manimce

* Add pyopengl

* Update test_examples.yml

* fix: custom 3d example for manimgl

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

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

* fix: virtual frame buffer

* fix: add ffmpeg dep

* feat(cli): add skip-all option for testing

* chore(lint): black fmt

* fix: typo in deps

* fix: python -m pip install

* fix: typo

* Update test_examples.yml

* fix: try fix

* fix: pip install --user

* pip install -e

* only on windows

* fix: tmp fix

* fix: typo in int parsing

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update test_examples.yml

* Update __version__.py

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-12 12:30:33 +02:00
0d97bdabb8 release new version 2022-09-11 12:26:46 +02:00
8e50adf0ba Merge pull request #8 from jeertmans/fix-shape
fix(bug): allows shape to be 2 or 3 dims
2022-09-11 12:26:01 +02:00
d583d591c1 fix(bug): allows shape to be 2 or 3 dims 2022-09-11 12:25:09 +02:00
c243fe3481 fix: add tqdm to setup 2022-09-11 12:14:54 +02:00
135 changed files with 13624 additions and 1664 deletions

20
.bumpversion.cfg Normal file
View File

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

3
.github/FUNDING.yml vendored 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

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

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

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

34
.github/workflows/clearcache.yml vendored Normal file
View File

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

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

33
.github/workflows/draft-pdf.yml vendored Normal file
View File

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

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@v4
- uses: reviewdog/action-languagetool@v1
with:
reporter: github-pr-review
level: warning

86
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,86 @@
# 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]
pull_request:
# 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:
permissions: write-all
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
cache: poetry
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install Linux Dependencies
run: |
sudo apt-get update
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Setup Pandoc
uses: nikeee/setup-pandoc@v1
- name: Install local Python package
run: poetry install --with docs
- name: Install IPython kernel
run: poetry run ipython kernel install --name "manim-slides" --user
- name: Restore cached media
id: cache-media-restore
uses: actions/cache/restore@v3
with:
path: docs/media
key: ${{ runner.os }}-docs-media
- name: Clear cache
run: |
gh extension install actions/gh-actions-cache
gh actions-cache delete ${{ steps.cache-media-restore.outputs.cache-primary-key }} --confirm || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save media to cache
id: cache-media-save
uses: actions/cache/save@v3
with:
path: docs/media
key: ${{ steps.cache-media-restore.outputs.cache-primary-key }}
- name: Build docs
run: cd docs && poetry run make html
- name: Upload artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v2
with:
# Upload docs/build/html dir
path: docs/build/html/
- name: Show docs/build/html/_static/ dir content (video only)
run: tree -L 3 docs/build/html/_static/ -P '*.mp4'
- name: Deploy to GitHub Pages
id: deployment
if: github.event_name != 'pull_request'
uses: actions/deploy-pages@v2

View File

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

91
.github/workflows/tests.yml vendored Normal file
View File

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

48
.gitignore vendored
View File

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

View File

@ -1,17 +1,32 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.11.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
- id: pretty-format-toml
exclude: poetry.lock
args: [--autofix]
- repo: https://github.com/keewis/blackdoc
rev: v0.3.9
hooks:
- id: blackdoc
additional_dependencies: [black==23.10.1]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.1
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]

157
CHANGELOG.md Normal file
View File

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

32
CITATION.cff Normal file
View File

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

View File

@ -1,674 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
MIT License
Copyright (c) 2023 Jérome Eertmans
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

301
README.md
View File

@ -1,143 +1,278 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
</picture>
[![Latest Release][pypi-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][pypi-download-badge]][pypi-version-url]
[![Documentation][documentation-badge]][documentation-url]
[![DOI][doi-badge]][doi-url]
[![JOSE Paper][jose-badge]][jose-url]
[![codecov][codecov-badge]][codecov-url]
# 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)
* [Interactive Tutorial](#interactive-tutorial)
* [Other Examples](#other-examples)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [F.A.Q](#faq)
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
- [Contributing](#contributing)
* [Reporting an Issue](#reporting-an-issue)
* [Seeking for Help](#seeking-for-help)
* [Contact](#contact)
```
## Installation
<!-- start install -->
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a virtual environment (e.g., [venv](https://docs.python.org/3/tutorial/venv.html)) for a local installation.
### Dependencies
<!-- start deps -->
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)
> **NOTE**: if you install Manim from its git repository, as suggested by ManimGL,
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
for ManimGL), otherwise it might install an unsupported version of Manim!
See [#314](https://github.com/jeertmans/manim-slides/issues/314) for an example.
<!-- end deps -->
### Pip Install
The recommended way to install the latest release is to use pip:
```bash
pip install manim-slides
```
## Usage
Optionally, you can also install Manim or ManimGL using extras[^1]:
Use the class `Slide` as your scenes base class:
```python
from manim_slides import Slide
class Example(Slide):
def construct(self):
...
```bash
pip install manim-slides[manim] # For Manim
# or
pip install manim-slides[manimgl] # For ManimGL
```
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings).
[^1]: NOTE: you still need to have Manim or ManimGL platform-specific dependencies installed on your computer.
### Install From Repository
An alternative way to install Manim Slides is to clone the git repository, and install from there: read the [contributing guide](https://eertmans.be/manim-slides/contributing/workflow.html) to know how.
<!-- end install -->
## Usage
<!-- start usage -->
Using Manim Slides is a two-step process:
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.next_slide()` everytime you want to create a new slide.
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
The documentation is available [online](https://eertmans.be/manim-slides/).
### Basic Example
Call `self.next_slide()` everytime you want to create a pause between
animations, and `self.next_slide(loop=True)` if you want the next slide to loop
over animations until the user presses continue:
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
```python
from manim import *
# or: from manimlib import *
# example.py
from manim import * # or: from manimlib import *
from manim_slides import Slide
class Example(Slide):
class BasicExample(Slide):
def construct(self):
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
self.pause()
self.next_slide() # Waits user to press continue to go to the next slide
self.start_loop()
self.next_slide(loop=True) # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.next_slide() # This will start a new non-looping slide
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.wait()
```
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(..)`.
First, render the animation files:
```bash
manim example.py BasicExample
# or
manimgl example.py BasicExample
```
To start the presentation using `Scene1`, `Scene2` and so on simply run:
```
manim-slides Scene1 Scene2...
```bash
manim-slides [OPTIONS] Scene1 Scene2...
```
## Keybindings
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:
Or in this example:
```bash
manim-slides BasicExample
```
<!-- end usage -->
## Key Bindings
The default key bindings to control the presentation are:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/wizard_dark.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/wizard_light.png">
<img alt="Manim Slides Wizard" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/wizard_light.png">
</picture>
You can run the **configuration wizard** to change those key bindings:
```bash
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:
```
```bash
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
## Interactive Tutorial
Clone this repository:
```
git clone https://github.com/jeertmans/manim-slides.git
cd manim-slides
```
Click on the image to watch a slides presentation that explains you how to use Manim Slides.
Install `manim` and `manim-slides`:
```
pip install manim manim-slides
# or
pip install manimgl manim-slides
```
[![Manim Slides Docs](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/docs.png)](https://eertmans.be/manim-slides/)
Render the example scene:
```
manim -qh example.py Example
# or
manimgl --hd example.py Example
```
## Other Examples
Run the presentation
```
manim-slides Example
```
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.
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)
## Comparison with original `manim-presentation`
## Comparison with Similar Tools
Here are a few things that I implemented (or that I'm planning to implement) on top of the original work:
There exists are variety of tools that allows to create slides presentations containing Manim animations.
- [x] Allowing multiple keys to control one action (useful when you use a laser pointer)
- [x] More robust config files checking
- [x] Dependencies are installed with the package
- [x] Only one cli (to rule them all)
- [x] User can easily generate dummy config file
- [x] Config file path can be manually set
- [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
- [x] Handle 3D scenes out of the box
- [x] Support for both `manim` and `manimgl` modules
- [ ] Generate docs online
- [x] Fix the quality problem on Windows platforms with `fullscreen` flag
Below is a comparison of the most used ones with Manim Slides:
## Contributions and license
| Project name | Manim Slides | Manim Presentation | Manim Editor | Jupyter Notebooks |
|:------------:|:------------:|:------------------:|:------------:|:-----------------:|
| Link | [![GitHub Repo stars](https://img.shields.io/github/stars/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/stars/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/stars/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/stars/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Activity | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jeertmans/manim-slides?style=social)](https://github.com/jeertmans/manim-slides) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/galatolofederico/manim-presentation?style=social)](https://github.com/galatolofederico/manim-presentation) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/ManimCommunity/manim_editor?style=social)](https://github.com/ManimCommunity/manim_editor) | [![GitHub Repo stars](https://img.shields.io/github/last-commit/jupyter/notebook?style=social)](https://github.com/jupyter/notebook) |
| Usage | Command-line | Command-line | Web Browser | Notebook |
| Note | Requires minimal modif. in scenes files | Requires minimal modif. in scenes files | Requires the usage of sections, and configuration through graphical interface | Relies on `nbconvert` to create slides from a Notebook |
| Support for ManimGL | Yes | No | No | No |
| Web Browser presentations | Yes | No | Yes | No |
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
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.
## F.A.Q
### How to increase quality on Windows
On Windows platform, one may encounter a lower image resolution than expected. Usually, this is observed because Windows rescales every application to fit the screen.
As found by [@arashash](https://github.com/arashash), in [#20](https://github.com/jeertmans/manim-slides/issues/20), the problem can be addressed by changing the scaling factor to 100%:
![Windows Fix Scaling](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png)
in *Settings*->*Display*.
## Contributing
Contributions are more than welcome! Please read through [our contributing section](https://eertmans.be/manim-slides/contributing/index.html).
### Reporting an Issue
<!-- start reporting-an-issue -->
If you think you found a bug,
an error in the documentation,
or wish there was some feature that is currently missing,
we would love to hear from you!
The best way to reach us is via the
[GitHub issues](https://github.com/jeertmans/manim-slides/issues).
If your problem is not covered by an already existing (closed or open) issue,
then we suggest you create a
[new issue](https://github.com/jeertmans/manim-slides/issues/new/choose).
You can choose from a list of templates, or open a
[blank issue](https://github.com/jeertmans/manim-slides/issues/new)
if your issue does not fit one of the proposed topics.
The more precise you are in the description of your problem, the faster we will
be able to help you!
<!-- end reporting-an-issue -->
### Seeking for help
<!-- start seeking-for-help -->
Sometimes, you may have a question about Manim Slides,
not necessarily an issue.
There are two ways you can reach us for questions:
- via the `Question/Help/Support` topic when
[choosing an issue template](https://github.com/jeertmans/manim-slides/issues/new/choose);
- or via
[GitHub discussions](https://github.com/jeertmans/manim-slides/discussions).
<!-- end seeking-for-help -->
### Contact
<!-- start contact -->
Finally, if you do not have any GitHub account,
or just wish to contact the author of Manim Slides,
you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
<!-- end contact -->
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
[pypi-version-url]: https://pypi.org/project/manim-slides/
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/manim-slides
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
[documentation-badge]: https://img.shields.io/website?down_color=lightgrey&down_message=offline&label=documentation&up_color=green&up_message=online&url=https%3A%2F%2Feertmans.be%2Fmanim-slides%2F
[documentation-url]: https://eertmans.be/manim-slides/
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.8215167.svg
[doi-url]: https://doi.org/10.5281/zenodo.8215167
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
[jose-url]: https://doi.org/10.21105/jose.00206
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
[codecov-url]: https://codecov.io/gh/jeertmans/manim-slides

2
custom_config.yml Normal file
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6
docs/source/changelog.md Normal file
View File

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

83
docs/source/conf.py Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,83 @@
# Workflow
This document is there to help you recreate a working environment for Manim Slides.
## Dependencies
```{include} ../../../README.md
:start-after: <!-- start deps -->
:end-before: <!-- end deps -->
```
## Forking the repository and cloning it locally
We use GitHub to host Manim Slides' repository, and we encourage contributors to use git.
Useful links:
* [GitHub's Hello World](https://docs.github.com/en/get-started/quickstart/hello-world).
* [GitHub Pull Request in 100 Seconds](https://www.youtube.com/watch?v=8lGpZkjnkt4&ab_channel=Fireship).
Once you feel comfortable with git and GitHub, [fork](https://github.com/jeertmans/manim-slides/fork) the repository, and clone it locally.
As for every Python project, using virtual environment is recommended to avoid conflicts between modules. For Manim Slides, we use [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer). If not already, please install it.
## Installing Python modules
With Poetry, installation becomes straightforward:
```bash
poetry install
```
This, however, only installs the minimal set of dependencies to run the package.
If you would like to install Manim or ManimGL, as documented in the [quickstart](../quickstart),
you can use the `--extras` option:
```bash
poetry install --extras manim # For Manim
# or
poetry install --extras manimgl # For ManimGL
```
Additionnally, Manim Slides comes with group dependencies for development purposes:
```bash
poetry install --with dev # For linters and formatters
# or
poetry install --with docs # To build the documentation locally
```
Another group is `test`, but it is only used for
[GitHub actions](https://github.com/jeertmans/manim-slides/blob/main/.github/workflows/test_examples.yml).
:::{note}
You can combine any number of groups or extras when installing the package locally.
:::
## Running commands
As modules were installed in a new Python environment, you cannot use them directly in the shell.
Instead, you either need to prepend `poetry run` to any command, e.g.:
```bash
poetry run manim-slides wizard
```
or enter a new shell that uses this new Python environment:
```
poetry shell
manim-slides wizard
```
## Testing your code
Most of the tests are done with GitHub actions, thus not on your computer. The only command you should run locally is `pre-commit run --all-files`: this runs a few linter and formatter to make sure the code quality and style stay constant across time. If a warning or an error is displayed, please fix it before going to next step.
## Proposing changes
Once you feel ready and think your contribution is ready to be reviewed, create a [pull request](https://github.com/jeertmans/manim-slides/pulls) and wait for a reviewer to check your work!
Many thanks to you!

View File

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

View File

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

55
docs/source/index.md Normal file
View File

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

5
docs/source/license.md Normal file
View File

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

View File

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

25
docs/source/quickstart.md Normal file
View File

@ -0,0 +1,25 @@
# Quickstart
## Installation
```{include} ../../README.md
:start-after: <!-- start install -->
:end-before: <!-- end install -->
```
## Creating your first slides
```{include} ../../README.md
:start-after: <!-- start usage -->
:end-before: <!-- end usage -->
```
The output slides should look this this:
```{eval-rst}
.. manim-slides:: ../../example.py:BasicExample
:hide_source:
:quality: high
```
For more advanced examples, see the [Examples](reference/examples) section.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6
manim-slides.qrc Normal file
View File

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

View File

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

73
manim_slides/__main__.py Normal file
View File

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

View File

@ -1 +1 @@
__version__ = "3.2.1"
__version__ = "5.1.0-rc1"

View File

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

View File

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

712
manim_slides/convert.py Normal file
View File

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

View File

@ -1,11 +1,5 @@
import platform
from pathlib import Path
FOLDER_PATH: str = "./slides"
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
FOLDER_PATH: Path = Path("./slides")
CONFIG_PATH: Path = Path(".manim-slides.toml")
FFMPEG_BIN: Path = Path("ffmpeg")

View File

View File

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

View File

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

49
manim_slides/logger.py Normal file
View File

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

View File

@ -1,22 +0,0 @@
import click
from click_default_group import DefaultGroup
from . import __version__
from .present import list_scenes, present
from .wizard import init, wizard
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
@click.version_option(__version__, "-v", "--version")
@click.help_option("-h", "--help")
def cli():
pass
cli.add_command(list_scenes)
cli.add_command(present)
cli.add_command(wizard)
cli.add_command(init)
if __name__ == "__main__":
cli()

View File

@ -1,45 +0,0 @@
import sys
from importlib.util import find_spec
MANIM_PACKAGE_NAME = "manim"
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
MANIMGL_PACKAGE_NAME = "manimlib"
MANIMGL_AVAILABLE = find_spec(MANIMGL_PACKAGE_NAME) is not None
MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
if MANIM_IMPORTED and MANIMGL_IMPORTED:
from manim import logger
logger.warn(
"Both manim and manimgl are installed, therefore `manim-slide` needs to need which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
)
MANIM = True
MANIMGL = False
elif MANIM_AVAILABLE and not MANIMGL_IMPORTED:
MANIM = True
MANIMGL = False
elif MANIMGL_AVAILABLE:
MANIM = False
MANIMGL = True
else:
raise ImportError(
"Either manim (community) or manimgl (3b1b) package must be installed"
)
FFMPEG_BIN = None
if MANIMGL:
from manimlib import Scene, ThreeDScene, config
from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log as logger
else:
from manim import Scene, ThreeDScene, config, logger
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable

View File

@ -1,416 +0,0 @@
import json
import math
import os
import platform
import sys
import time
from enum import IntEnum, auto, unique
if platform.system() == "Windows":
import ctypes
import click
import cv2
import numpy as np
from .commons import config_path_option
from .config import Config
from .defaults import CONFIG_PATH, FOLDER_PATH
from .slide import reverse_video_path
@unique
class State(IntEnum):
PLAYING = auto()
PAUSED = auto()
WAIT = auto()
END = auto()
def __str__(self):
return self.name.capitalize()
def now() -> int:
return round(time.time() * 1000)
def fix_time(x: float) -> float:
return x if x > 0 else 1
class Presentation:
def __init__(self, config, last_frame_next: bool = False):
self.last_frame_next = last_frame_next
self.slides = config["slides"]
self.files = [path for path in config["files"]]
self.reverse = False
self.reversed_slide = -1
self.lastframe = []
self.caps = [None for _ in self.files]
self.reset()
self.add_last_slide()
def add_last_slide(self):
last_slide_end = self.slides[-1]["end_animation"]
last_animation = len(self.files)
self.slides.append(
dict(
start_animation=last_slide_end,
end_animation=last_animation,
type="last",
number=len(self.slides) + 1,
terminated=False,
)
)
def reset(self):
self.current_animation = 0
self.load_this_cap(0)
self.current_slide_i = 0
self.slides[-1]["terminated"] = False
def next(self):
if self.current_slide["type"] == "last":
self.current_slide["terminated"] = True
else:
self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1)
self.rewind_slide()
def prev(self):
self.current_slide_i = max(0, self.current_slide_i - 1)
self.rewind_slide()
def reverse_slide(self):
self.rewind_slide(reverse=True)
def rewind_slide(self, reverse: bool = False):
self.reverse = reverse
self.current_animation = self.current_slide["start_animation"]
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def load_this_cap(self, cap_number: int):
if (
self.caps[cap_number] is None
or (self.reverse and self.reversed_slide != cap_number)
or (not self.reverse and self.reversed_slide == cap_number)
):
# unload other caps
for i in range(len(self.caps)):
if not self.caps[i] is None:
self.caps[i].release()
self.caps[i] = None
# load this cap
file = self.files[cap_number]
if self.reverse:
self.reversed_slide = cap_number
file = "{}_reversed{}".format(*os.path.splitext(file))
else:
self.reversed_slide = -1
self.caps[cap_number] = cv2.VideoCapture(file)
@property
def current_slide(self):
return self.slides[self.current_slide_i]
@property
def current_cap(self):
self.load_this_cap(self.current_animation)
return self.caps[self.current_animation]
@property
def fps(self):
return self.current_cap.get(cv2.CAP_PROP_FPS)
# This function updates the state given the previous state.
# It does this by reading the video information and checking if the state is still correct.
# It returns the frame to show (lastframe) and the new state.
def update_state(self, state):
if state == State.PAUSED:
if len(self.lastframe) == 0:
_, self.lastframe = self.current_cap.read()
return self.lastframe, state
still_playing, frame = self.current_cap.read()
if still_playing:
self.lastframe = frame
elif state in [state.WAIT, state.PAUSED]:
return self.lastframe, state
elif self.current_slide["type"] == "last" and self.current_slide["terminated"]:
return self.lastframe, State.END
if not still_playing:
if self.current_slide["end_animation"] == self.current_animation + 1:
if self.current_slide["type"] == "slide":
# To fix "it always ends one frame before the animation", uncomment this.
# But then clears on the next slide will clear the stationary after this slide.
if self.last_frame_next:
self.load_this_cap(self.next_cap)
self.next_cap = self.caps[self.current_animation + 1]
self.next_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
_, self.lastframe = self.next_cap.read()
state = State.WAIT
elif self.current_slide["type"] == "loop":
self.current_animation = self.current_slide["start_animation"]
state = State.PLAYING
self.rewind_slide()
elif self.current_slide["type"] == "last":
self.current_slide["terminated"] = True
elif (
self.current_slide["type"] == "last"
and self.current_slide["end_animation"] == self.current_animation
):
state = State.WAIT
else:
# Play next video!
self.current_animation += 1
self.load_this_cap(self.current_animation)
# Reset video to position zero if it has been played before
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
return self.lastframe, state
class Display:
def __init__(self, presentations, config, start_paused=False, fullscreen=False):
self.presentations = presentations
self.start_paused = start_paused
self.config = config
self.state = State.PLAYING
self.lastframe = None
self.current_presentation_i = 0
self.lag = 0
self.last_time = now()
if platform.system() == "Windows":
user32 = ctypes.windll.user32
self.screen_width, self.screen_height = user32.GetSystemMetrics(
0
), user32.GetSystemMetrics(1)
if fullscreen:
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty(
"Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
)
def resize_frame_to_screen(self, frame: np.ndarray):
frame_height, frame_width = frame.shape
scale_height = self.screen_height / frame_height
scale_width = self.screen_width / frame_width
scale = min(scale_height, scale_width)
return cv2.resize(frame, (int(scale * frame_height, scale * frame_width)))
@property
def current_presentation(self):
return self.presentations[self.current_presentation_i]
def run(self):
while True:
self.lastframe, self.state = self.current_presentation.update_state(
self.state
)
if self.state == State.PLAYING or self.state == State.PAUSED:
if self.start_paused:
self.state = State.PAUSED
self.start_paused = False
if self.state == State.END:
if self.current_presentation_i == len(self.presentations) - 1:
self.quit()
else:
self.current_presentation_i += 1
self.state = State.PLAYING
self.handle_key()
self.show_video()
self.show_info()
def show_video(self):
self.lag = now() - self.last_time
self.last_time = now()
frame = self.lastframe
if platform.system() == "Windows":
frame = self.resize_frame_to_screen(frame)
cv2.imshow("Video", frame)
def show_info(self):
info = np.zeros((130, 420), np.uint8)
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
grid_x = [30, 230]
grid_y = [30, 70, 110]
cv2.putText(
info,
f"Animation: {self.current_presentation.current_animation}",
(grid_x[0], grid_y[0]),
*font_args,
)
cv2.putText(info, f"State: {self.state}", (grid_x[1], grid_y[0]), *font_args)
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
(grid_x[0], grid_y[1]),
*font_args,
)
cv2.putText(
info,
f"Slide Type: {self.current_presentation.current_slide['type']}",
(grid_x[1], grid_y[1]),
*font_args,
)
cv2.putText(
info,
f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}",
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
*font_args,
)
cv2.imshow("Info", info)
def handle_key(self):
sleep_time = math.ceil(1000 / self.current_presentation.fps)
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
if self.config.QUIT.match(key):
self.quit()
elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key):
self.state = State.PAUSED
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
self.state = State.PLAYING
elif self.state == State.WAIT and (
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
):
self.current_presentation.next()
self.state = State.PLAYING
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
self.current_presentation.next()
elif self.config.BACK.match(key):
if self.current_presentation.current_slide_i == 0:
self.current_presentation_i = max(0, self.current_presentation_i - 1)
self.current_presentation.reset()
self.state = State.PLAYING
else:
self.current_presentation.prev()
self.state = State.PLAYING
elif self.config.REVERSE.match(key):
self.current_presentation.reverse_slide()
self.state = State.PLAYING
elif self.config.REWIND.match(key):
self.current_presentation.rewind_slide()
self.state = State.PLAYING
def quit(self):
cv2.destroyAllWindows()
sys.exit()
@click.command()
@click.option(
"--folder",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
)
@click.help_option("-h", "--help")
def list_scenes(folder):
"""List available scenes."""
for i, scene in enumerate(_list_scenes(folder), start=1):
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder):
scenes = []
for file in os.listdir(folder):
if file.endswith(".json"):
scenes.append(os.path.basename(file)[:-5])
return scenes
@click.command()
@click.argument("scenes", nargs=-1)
@config_path_option
@click.option(
"--folder",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False),
help="Set slides folder.",
)
@click.option("--start-paused", is_flag=True, help="Start paused.")
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
@click.option(
"--last-frame-next",
is_flag=True,
help="Show the next animation first frame as last frame (hack).",
)
@click.help_option("-h", "--help")
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
"""Present the different scenes."""
if len(scenes) == 0:
scene_choices = _list_scenes(folder)
scene_choices = dict(enumerate(scene_choices, start=1))
for i, scene in scene_choices.items():
click.secho(f"{i}: {scene}", fg="green")
click.echo()
click.echo("Choose number corresponding to desired scene/arguments.")
click.echo("(Use comma separated list for multiple entries)")
def value_proc(value: str):
indices = list(map(int, value.strip().replace(" ", "").split(",")))
if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
raise click.UsageError(
"Please only enter numbers displayed on the screen."
)
return [scene_choices[i] for i in indices]
if len(scene_choices) == 0:
raise click.UsageError(
"No scenes were found, are you in the correct directory?"
)
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
break
except ValueError as e:
raise click.UsageError(e)
presentations = list()
for scene in scenes:
config_file = os.path.join(folder, f"{scene}.json")
if not os.path.exists(config_file):
raise click.UsageError(
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
)
config = json.load(open(config_file))
presentations.append(Presentation(config, last_frame_next=last_frame_next))
if os.path.exists(config_path):
config = Config.parse_file(config_path)
else:
config = Config()
display = Display(
presentations, config=config, start_paused=start_paused, fullscreen=fullscreen
)
display.run()

View File

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

View File

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

14
manim_slides/qt_utils.py Normal file
View File

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

54
manim_slides/render.py Normal file
View File

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

171
manim_slides/resources.py Normal file
View File

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

View File

@ -1,194 +0,0 @@
import json
import os
import platform
import shutil
import subprocess
from tqdm import tqdm
from .defaults import FOLDER_PATH
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
def reverse_video_path(src: str) -> str:
file, ext = os.path.splitext(src)
return f"{file}_reversed{ext}"
def reverse_video_file(src: str, dst: str):
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate()
class Slide(Scene):
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
if MANIMGL:
if not os.path.isdir("videos"):
os.mkdir("videos")
kwargs["file_writer_config"] = {
"break_into_partial_movies": True,
"output_directory": "",
"write_to_movie": True,
}
kwargs["preview"] = False
super().__init__(*args, **kwargs)
self.output_folder = output_folder
self.slides = list()
self.current_slide = 1
self.current_animation = 0
self.loop_start_animation = None
self.pause_start_animation = 0
@property
def partial_movie_files(self):
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
else:
return self.renderer.file_writer.partial_movie_files
@property
def show_progress_bar(self):
if MANIMGL:
return getattr(super(Scene, self), "show_progress_bar", True)
else:
return config["progress_bar"] != "none"
@property
def leave_progress_bar(self):
if MANIMGL:
return getattr(super(Scene, self), "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
def play(self, *args, **kwargs):
super().play(*args, **kwargs)
self.current_animation += 1
def pause(self):
self.slides.append(
dict(
type="slide",
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
self.current_slide += 1
self.pause_start_animation = self.current_animation
def start_loop(self):
assert self.loop_start_animation is None, "You cannot nest loops"
self.loop_start_animation = self.current_animation
def end_loop(self):
assert (
self.loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
dict(
type="loop",
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
)
)
self.current_slide += 1
self.loop_start_animation = None
self.pause_start_animation = self.current_animation
def save_slides(self, use_cache=True):
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
files_folder = os.path.join(self.output_folder, "files")
if not os.path.exists(files_folder):
os.mkdir(files_folder)
scene_name = type(self).__name__
scene_files_folder = os.path.join(files_folder, scene_name)
old_animation_files = set()
if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder)
elif not use_cache:
shutil.rmtree(scene_files_folder)
os.mkdir(scene_files_folder)
else:
old_animation_files.update(os.listdir(scene_files_folder))
files = list()
for src_file in tqdm(
self.partial_movie_files,
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
leave=self.leave_progress_bar,
ascii=True if platform.system() == "Windows" else None,
disable=not self.show_progress_bar,
):
filename = os.path.basename(src_file)
_hash, ext = os.path.splitext(filename)
rev_filename = f"{_hash}_reversed{ext}"
dst_file = os.path.join(scene_files_folder, filename)
# We only copy animation if it was not present
if filename in old_animation_files:
old_animation_files.remove(filename)
else:
shutil.copyfile(src_file, dst_file)
# We only reverse video if it was not present
if rev_filename in old_animation_files:
old_animation_files.remove(rev_filename)
else:
rev_file = os.path.join(scene_files_folder, rev_filename)
reverse_video_file(src_file, rev_file)
files.append(dst_file)
logger.info(
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
)
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
f = open(slide_path, "w")
json.dump(dict(slides=self.slides, files=files), f)
f.close()
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
)
def run(self, *args, **kwargs):
"""MANIMGL renderer"""
super().run(*args, **kwargs)
self.save_slides(use_cache=False)
def render(self, *args, **kwargs):
"""MANIM render"""
# We need to disable the caching limit since we rely on intermidiate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
super().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
self.save_slides()
class ThreeDSlide(Slide, ThreeDScene):
pass

View File

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

View File

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

607
manim_slides/slide/base.py Normal file
View File

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

158
manim_slides/slide/manim.py Normal file
View File

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

View File

@ -0,0 +1,72 @@
from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Tuple
from manimlib import Scene, ThreeDCamera
from manimlib.utils.file_ops import get_sorted_integer_files
from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs.setdefault("file_writer_config", {}).update(
skip_animations=True,
break_into_partial_movies=True,
write_to_movie=True,
)
kwargs["preview"] = False # Avoid opening a preview window
super().__init__(*args, **kwargs)
@property
def _frame_height(self) -> float:
return self.camera.frame.get_height() # type: ignore
@property
def _frame_width(self) -> float:
return self.camera.frame.get_width() # type: ignore
@property
def _background_color(self) -> str:
return self.camera_config["background_color"].hex # type: ignore
@property
def _resolution(self) -> Tuple[int, int]:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
@property
def _partial_movie_files(self) -> List[Path]:
kwargs = {
"remove_non_integer_files": True,
"extension": self.file_writer.movie_file_extension,
}
return [
Path(file)
for file in get_sorted_integer_files(
self.file_writer.partial_movie_directory, **kwargs
)
]
@property
def _show_progress_bar(self) -> bool:
return True
@property
def _leave_progress_bar(self) -> bool:
return self.leave_progress_bars # type: ignore
@property
def _start_at_animation_number(self) -> Optional[int]:
return self.start_at_animation_number # type: ignore
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer."""
super().run(*args, **kwargs)
self._save_slides(use_cache=False)
class ThreeDSlide(Slide):
CONFIG: ClassVar[Dict[str, Any]] = {
"camera_class": ThreeDCamera,
}
pass

View File

View File

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

77
manim_slides/utils.py Normal file
View File

@ -0,0 +1,77 @@
import hashlib
import subprocess
import tempfile
from pathlib import Path
from typing import List
from .logger import logger
def concatenate_video_files(ffmpeg_bin: Path, files: List[Path], dest: Path) -> None:
"""Concatenate multiple video files into one."""
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
f.writelines(f"file '{path.absolute()}'\n" for path in files)
f.close()
command: List[str] = [
str(ffmpeg_bin),
"-f",
"concat",
"-safe",
"0",
"-i",
f.name,
"-c",
"copy",
str(dest),
"-y",
]
logger.debug(" ".join(command))
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
if output:
logger.debug(output.decode())
if error:
logger.debug(error.decode())
if not dest.exists():
raise ValueError(
"could not properly concatenate files, use `-v DEBUG` for more details"
)
def merge_basenames(files: List[Path]) -> Path:
"""Merge multiple filenames by concatenating basenames."""
if len(files) == 0:
raise ValueError("Cannot merge an empty list of files!")
dirname: Path = files[0].parent
ext = files[0].suffix
basenames = list(file.stem for file in files)
basenames_str = ",".join(f"{len(b)}:{b}" for b in basenames)
# We use hashes to prevent too-long filenames, see issue #123:
# https://github.com/jeertmans/manim-slides/issues/123
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
logger.debug(f"Generated a new basename for basenames: {basenames} -> '{basename}'")
return dirname.joinpath(basename + ext)
def reverse_video_file(ffmpeg_bin: Path, src: Path, dst: Path) -> None:
"""Reverses a video file, writting the result to `dst`."""
command = [str(ffmpeg_bin), "-y", "-i", str(src), "-vf", "reverse", str(dst)]
logger.debug(" ".join(command))
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = process.communicate()
if output:
logger.debug(output.decode())
if error:
logger.debug(error.decode())

View File

@ -1,43 +1,160 @@
import os
import sys
from functools import partial
from pathlib import Path
from typing import Any
import click
import cv2
import numpy as np
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QKeyEvent
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QGridLayout,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from .commons import config_options
from .config import Config
from .commons import config_options, verbosity_option
from .config import Config, Key
from .defaults import CONFIG_PATH
from .logger import logger
from .qt_utils import qapp
from .resources import * # noqa: F403
WINDOW_NAME: str = "Configuration Wizard"
keymap = {}
for key in Qt.Key:
keymap[key.value] = key.name.partition("_")[2]
def prompt(question: str) -> int:
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
display = np.zeros((130, 420), np.uint8)
class KeyInput(QDialog): # type: ignore
def __init__(self) -> None:
super().__init__()
self.key = None
cv2.putText(display, "* Manim Slides Wizard *", (70, 33), *font_args)
cv2.putText(display, question, (30, 85), *font_args)
self.layout = QVBoxLayout()
cv2.imshow("Manim Slides Configuration Wizard", display)
return cv2.waitKeyEx(-1)
self.setWindowTitle("Keyboard Input")
self.label = QLabel("Press any key to register it")
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
self.key = event.key()
self.deleteLater()
event.accept()
class Wizard(QWidget): # type: ignore
def __init__(self, config: Config):
super().__init__()
self.setWindowTitle(WINDOW_NAME)
self.config = config
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.closed_without_saving = False
button = QDialogButtonBox.Save | QDialogButtonBox.Cancel
self.button_box = QDialogButtonBox(button)
self.button_box.accepted.connect(self.save_config)
self.button_box.rejected.connect(self.close_without_saving)
self.buttons = []
self.layout = QGridLayout()
for i, (key, value) in enumerate(self.config.keys.dict().items()):
# Create label for key name information
label = QLabel()
key_info = value["name"] or key
label.setText(key_info.title())
self.layout.addWidget(label, i, 0)
# Create button that will pop-up a dialog and ask to input a new key
value = value["ids"].pop()
button = QPushButton(keymap[value])
button.setToolTip(
f"Click to modify the key associated to action {key_info}"
)
self.buttons.append(button)
button.clicked.connect(
partial(self.open_dialog, i, getattr(self.config.keys, key))
)
self.layout.addWidget(button, i, 1)
self.layout.addWidget(self.button_box, len(self.buttons), 1)
self.setLayout(self.layout)
def close_without_saving(self) -> None:
logger.debug("Closing configuration wizard without saving")
self.closed_without_saving = True
self.deleteLater()
def closeEvent(self, event: Any) -> None: # noqa: N802
self.close_without_saving()
event.accept()
def save_config(self) -> None:
try:
Config.model_validate(self.config.dict())
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)
msg.setText("Error")
msg.setInformativeText(
"Two or more actions share a common key: make sure actions have distinct key codes."
)
msg.setWindowTitle("Error: duplicated keys")
msg.exec()
return
self.deleteLater()
def open_dialog(self, button_number: int, key: Key) -> None:
button = self.buttons[button_number]
dialog = KeyInput()
dialog.exec()
if dialog.key is not None:
key_name = keymap[dialog.key]
key.set_ids(dialog.key)
button.setText(key_name)
@click.command()
@config_options
def wizard(config_path, force, merge):
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
def init(config_path, force, merge, skip_interactive=False):
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Initialize a new default configuration file."""
return _init(config_path, force, merge, skip_interactive=True)
def _init(config_path, force, merge, skip_interactive=False):
if os.path.exists(config_path):
def _init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""
Actual initialization code for configuration file, with optional interactive
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
@ -49,27 +166,35 @@ def _init(config_path, force, merge, skip_interactive=False):
force = choice == "o"
merge = choice == "m"
if force:
click.secho("Overwriting.")
elif merge:
click.secho("Merging.")
else:
click.secho("Exiting.")
if not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
config = Config.from_file(config_path)
prompt("Press any key to continue")
app = qapp()
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
for _, key in config:
key.ids = [prompt(f"Press the {key.name} key")]
if window.closed_without_saving:
sys.exit(0)
config = window.config
if merge:
config = Config.parse_file(config_path).merge_with(config)
config = Config.from_file(config_path).merge_with(config)
with open(config_path, "w") as config_file:
config_file.write(config.json(indent=4))
config.to_file(config_path)
click.echo(f"Configuration file successfully save to `{config_path}`")
click.secho(f"Configuration file successfully saved to `{config_path}`")

BIN
paper/docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

53
paper/paper.bib Normal file
View File

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

174
paper/paper.md Normal file
View File

@ -0,0 +1,174 @@
---
title: 'Manim Slides: A Python package for presenting Manim content anywhere'
tags:
- Python
- manim
- animations
- teaching
- conference presentations
- tool
authors:
- name: Jérome Eertmans
orcid: 0000-0002-5579-5360
affiliation: 1
affiliations:
- name: ICTEAM, UCLouvain, Belgium
index: 1
date: 2 March 2023
bibliography: paper.bib
---
# Summary
Manim Slides is a Python package that makes presenting Manim animations
straightforward. With minimal changes required to pre-existing code, one can
slide through animations in a *PowerPoint-like* manner, or share its slides
*online* using ReavealJS' power.
# Introduction
Presenting educational content has always been a difficult task, especially
when it uses temporal or iterative concepts. During the last decades, the
presence of computers in classrooms for educational purposes has increased
enormously, allowing teachers to show animated or interactive content.
With the democratization of YouTube, many people have decided to use this
platform to share educational content. Among these people, Grant Sanderson, a
YouTuber presenting videos on the theme of mathematics, quickly became known
for his original and quality animations. In 2018, Grant announced in a video
that he creates his animations using a self-developed Python tool called Manim
[@manim-announcement]. In 2019, he made the Manim source code public [@manimgl],
so anyone can use his tool. Very quickly, the community came together and, in
2020, created a "fork" of the original GitHub repository [@manimce], in order to
create a more accessible and better documented version of this tool. Since then,
the two versions are differentiated by using ManimGL for Grant Sanderson's
version, as it uses OpenGL for rendering, and ManimCE for the community edition
(CE).
Despite the many advantages of the Manim tool in terms of illustrating
mathematical concepts, one cannot help but notice that most presentations,
whether in the classroom or at a conference, are mainly done with PowerPoint
or PDF slides. One of the many advantages of these formats, as opposed to videos
created with Manim, is the ability to pause, rewind, etc., whenever you want.
To face this problem, in 2021, the manim-presentation tool was created
[@manim-presentation]. This tool allows you to present Manim animations as you
would present slides: with pauses, slide jumps, etc. However, this tool has
evolved very little since its inception and does not work with ManimGL.
In 2022, Manim Slides has been created from manim-presentation, with the aim
to make it a more complete tool, better documented, and usable on all platforms
and with ManimCE or ManimGL. After almost a year of existence, Manim Slides has
evolved a lot (see
[comparison section](#comparison-with-manim-presentation)),
has built a small community of contributors, and continues to
provide new features on a regular basis.
# Easy to Use Commitment
Manim Slides is commited to be an easy-to-use tool, with minimal installation
procedure and few modifications required. It can either be used locally with its
graphical user interface (GUI), or shared via one of the two formats it can
convert to:
* an HTML page thanks to the RevealJS Javascript package [@revealjs];
* or a PowerPoint (`.pptx`) file.
This work has a very similar syntax to Manim and offers a comprehensive
documentation hosted on [GitHub pages](https://jeertmans.github.io/manim-slides/), see
\autoref{fig:docs}.
![Manim Slides' documentation homepage.\label{fig:docs}](docs.png)
# Example usage
We have used manim-presentation for our presentation at the COST
Interact, hosted in Lyon, 2022, and
[available online](https://web.archive.org/web/20230507184944/https://eertmans.be/posts/cost-interact-presentation/).
This experience highly motivated the development of Manim Slides, and our
EuCAP 2023 presentation slides are already
[available online](https://web.archive.org/web/20230507211243/https://eertmans.be/posts/eucap-presentation/), thanks
to Manim Slides' HTML feature.
Also, one of our users created a short
[video tutorial](https://www.youtube.com/watch?v=Oc9g89VzKsY&ab_channel=TheoremofBeethoven)
and posted it on YouTube.
# Stability and releases
Manim Slides is continously tested on most recent Python versions, both ManimCE
and ManimGL, and on all major platforms: **Ubuntu**, **macOS** and **Windows**. Due to Manim
Slide's exposed API being very minimal, and the variety of tests that are
performed, this tool can be considered stable over time.
New releases are very frequent, as they mostly introduce enhancements or small
documention fixes, and the command-line tool automatically notifies for new
available updates. Therefore, regularly updating Manim Slides is highly
recommended.
# Statement of Need
Similar tools to Manim Slides also exist, such as its predecessor,
manim-presentation [@manim-presentation], or the web-based alternative, Manim
Editor [@manim-editor], but none of them provide the documentation level nor the
amount of features that Manim Slides strives to. This work makes the task of
presenting Manim content in front of an audience much easier than before,
allowing presenters to focus more on the content of their slides, rather than on
how to actually present them efficiently.
## Target Audience
Manim Slides was developed with the goal of making educational content more
accessible than ever. We believe that researchers, professors, teaching
assistants and anyone else who needs to teach scientific content can benefit
from using this tool. The ability to pace your presentation yourself is
essential, and Manim Slides gives you that ability.
## A Need for Portability
One of the major concerns with presenting content in a non-standard format
(i.e., not just a plain PDF) is the issue of portability.
Depending on the programs available, the power of the target computer,
or the access to the internet, not all solutions are equal.
From the same configuration file, Manim Slides offers a series of solutions to
share your slides, which we discuss on our
[Sharing your slides](https://jeertmans.github.io/manim-slides/reference/sharing.html)
page.
## Comparison with manim-presentation
Starting from @manim-presentation's original work, Manim Slides now provides
numerous additional features.
A non-exhaustive list of those new features is as follows:
* ManimGL compatibility;
* playing slides in reverse;
* exporting slides to HTML and PowerPoint;
* 3D scene support;
* multiple key inputs can map to the same action
(e.g., useful when using a pointer);
* optionally hiding mouse cursor when presenting;
* recording your presentation;
* multiple video scaling methods (for speed-vs-quality tradeoff);
* and automatic detection of some scene parameters
(e.g., resolution or background color).
The complete and up-to-date set of features Manim Slide supports is
available in the
[online documentation](https://jeertmans.github.io/manim-slides/).
For new feature requests, we highly encourage users to
[create an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
with the appropriate template.
# Acknowledgements
We acknowledge the work of @manim-presentation that paved the initial structure
of Manim Slides with the manim-presentation Python package.
We also acknowledge Grant Sanderson for his tremendous work on Manim, as
well as the Manim Community contributors.
Finally, we also acknowledge contributions from the GitHub contributors on the
Manim Slides repository.
# References

4109
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

140
pyproject.toml Normal file
View File

@ -0,0 +1,140 @@
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["setuptools", "poetry-core>=1.0.0"]
[tool.black]
target-version = ["py38"]
[tool.mypy]
disallow_untyped_decorators = false
install_types = true
python_version = "3.8"
strict = true
[tool.poetry]
authors = [
"Jérome Eertmans <jeertmans@icloud.com>"
]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
"Topic :: Multimedia :: Graphics",
"Topic :: Scientific/Engineering"
]
description = "Tool for live presentations using manim"
documentation = "https://eertmans.be/manim-slides"
exclude = ["docs/", "static/"]
homepage = "https://github.com/jeertmans/manim-slides"
keywords = ["manim", "slides", "plugin", "manimgl"]
license = "MIT"
name = "manim-slides"
packages = [
{include = "manim_slides"}
]
readme = "README.md"
repository = "https://github.com/jeertmans/manim-slides"
version = "5.1.0-rc1"
[tool.poetry.dependencies]
click = "^8.1.3"
click-default-group = "^1.2.2"
docutils = {version = "^0.20.1", optional = true}
ipython = {version = ">=8.12.2", optional = true}
jinja2 = "^3.1.2"
lxml = "^4.9.2"
manim = {version = "^0.17.3", optional = true}
manimgl = {version = "^1.6.1", optional = true}
numpy = "^1.19"
opencv-python = "^4.6.0.66"
pillow = "^9.5.0"
pydantic = "^2.0.1"
pydantic-extra-types = "^2.0.0"
pyside6 = "6.5.2"
python = ">=3.8.1,<3.12"
python-pptx = "^0.6.21"
requests = "^2.28.1"
rich = "^13.3.2"
rtoml = "^0.9.0"
tqdm = "^4.64.1"
[tool.poetry.extras]
magic = ["manim", "ipython"]
manim = ["manim"]
manimgl = ["manimgl"]
sphinx-directive = ["docutils", "manim"]
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
bump2version = "^1.0.1"
pre-commit = "^3.0.2"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
furo = "^2023.5.20"
ipykernel = "^6.25.1"
manim = "^0.17.3"
myst-parser = "^2.0.0"
nbsphinx = "^0.9.2"
pandoc = "^2.3"
sphinx = "^7.0.1"
sphinx-click = "^4.4.0"
sphinx-copybutton = "^0.5.1"
sphinxext-opengraph = "^0.7.5"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies]
manim = "^0.17.3"
manimgl = "^1.6.1"
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-env = "^0.8.2"
pytest-qt = "^4.2.0"
pytest-xdist = "^3.3.1"
[tool.poetry.plugins]
[tool.poetry.plugins."console_scripts"]
manim-slides = "manim_slides.__main__:cli"
[tool.pytest.ini_options]
env = [
"QT_QPA_PLATFORM=offscreen"
]
filterwarnings = [
"error",
"ignore::DeprecationWarning"
]
[tool.ruff]
extend-exclude = ["manim_slides/resources.py"]
extend-ignore = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D203",
"D205",
"D212",
"E501"
]
extend-include = ["*.ipynb"]
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ['manim_slides', 'tests']}
line-length = 88
target-version = "py38"

View File

@ -1,45 +0,0 @@
import sys
import setuptools
from manim_slides import __version__ as version
if sys.version_info < (3, 7):
raise RuntimeError("This package requires Python 3.7+")
with open("README.md", "r") as f:
long_description = f.read()
setuptools.setup(
name="manim-slides",
version=version,
author="Jérome Eertmans (previously, Federico A. Galatolo)",
author_email="jeertmans@icloud.com (resp., federico.galatolo@ing.unipi.it)",
description="Tool for live presentations using manim",
url="https://github.com/jeertmans/manim-slides",
long_description=long_description,
long_description_content_type="text/markdown",
packages=setuptools.find_packages(),
entry_points={
"console_scripts": [
"manim-slides=manim_slides.main:cli",
],
},
python_requires=">=3.7",
install_requires=[
"click>=8.0",
"click-default-group>=1.2",
"numpy>=1.19.3",
"pydantic>=1.9.1",
"opencv-python>=4.6",
],
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
],
)

BIN
static/docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 KiB

After

Width:  |  Height:  |  Size: 485 KiB

BIN
static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

53
static/logo.py Normal file
View File

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

BIN
static/logo_dark_docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
static/logo_dark_github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

21
static/make_logo.sh Executable file
View File

@ -0,0 +1,21 @@
#! /bin/bash
MANIM_SLIDES_THEME=light poetry run manim render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo.png
ln -f -r -s static/logo.png docs/source/_static/logo.png
MANIM_SLIDES_THEME=dark_docs poetry run manim render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_docs.png
ln -f -r -s static/logo_dark_docs.png docs/source/_static/logo_dark_docs.png
MANIM_SLIDES_THEME=dark_github poetry run manim render -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_github.png
ln -f -r -s static/logo_dark_github.png docs/source/_static/logo_dark_github.png
MANIM_SLIDES_THEME=light poetry run manim render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_light_transparent.png
ln -f -r -s static/logo_light_transparent.png docs/source/_static/logo_light_transparent.png
MANIM_SLIDES_THEME=dark_docs poetry run manim render -t -qk -s --format png --resolution 2560,1280 static/logo.py && mv media/images/logo/*.png static/logo_dark_transparent.png
ln -f -r -s static/logo_dark_transparent.png docs/source/_static/logo_dark_transparent.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

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