Compare commits

...

17 Commits

Author SHA1 Message Date
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
13 changed files with 761 additions and 261 deletions

15
.github/workflows/languagetool.yml vendored Normal file
View File

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

100
.github/workflows/test_examples.yml vendored Normal file
View File

@ -0,0 +1,100 @@
on:
pull_request:
paths:
- '**.py'
workflow_dispatch:
name: Test Examples
jobs:
build-examples:
strategy:
matrix:
manim: [manim, manimgl]
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.7', '3.8', '3.9', '3.10']
exclude:
# excludes manimgl on Windows because if throws errors
# related to OpenGL, which seems hard to fix:
# Your graphics drivers do not support OpenGL 2.0.
- os: windows-latest
manim: manimgl
# manimgl actually requires Python >= 3.8, see:
# https://github.com/3b1b/manim/issues/1808
- manim: manimgl
pyversion: '3.7'
# We only test Python 3.10 on Windows and MacOS
- os: windows-latest
pyversion: '3.7'
- os: windows-latest
pyversion: '3.8'
- os: windows-latest
pyversion: '3.9'
- os: macos-latest
pyversion: '3.7'
- os: macos-latest
pyversion: '3.8'
- os: macos-latest
pyversion: '3.9'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
- name: Append to Path on MacOS and Ubuntu
if: matrix.os == 'macos-latest' || 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
- name: Install MacOS dependencies (manim only)
if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
run: brew install py3cairo
- name: Install MacOS dependencies
if: matrix.os == 'macos-latest'
run: brew install ffmpeg
- name: Install Ubuntu dependencies
if: matrix.os == 'ubuntu-latest'
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
- name: Install manim on MacOs
if: matrix.manim == 'manim' && matrix.os == 'macos-latest'
run: pip3 install --user manim
- name: Install manim on Ubuntu and Windows
if: matrix.manim == 'manim' && (matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest')
run: python -m pip install --user manim
- name: Install manimgl on MacOs
if: matrix.manim == 'manimgl' && matrix.os == 'macos-latest'
run: pip3 install --user manimgl
- name: Install manimgl on Ubuntu and Windows
if: matrix.manim == 'manimgl' && matrix.os != 'macos-latest'
run: python -m pip install --user manimgl
- name: Install manim-slides on MacOS
if: matrix.os == 'macos-latest'
run: pip3 install --user .
- name: Install manim-slides on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: xvfb-run -a -s "-screen 0 1400x900x24" python -m pip install --user .
- name: Install manim-slides on Windows
if: matrix.os == 'windows-latest'
run: pip3 install -e .
- name: Build slides with manim
if: matrix.manim == 'manim'
run: python -m manim -ql example.py Example ThreeDExample
- name: Build slides with manimgl on Ubuntu
if: matrix.manim == 'manimgl' && matrix.os == 'ubuntu-latest'
run: xvfb-run -a -s "-screen 0 1400x900x24" manim-render -l example.py Example ThreeDExample
- name: Build slides with manimgl on MacOS or Windows
if: matrix.manim == 'manimgl' && (matrix.os == 'macos-latest' || matrix.os == 'windows-latest')
run: manimgl -l example.py Example ThreeDExample
- name: Test slides on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: xvfb-run -a -s "-screen 0 1400x900x24" manim-slides Example ThreeDExample --skip-all
- name: Test slides on MacOS or Windows
if: matrix.os == 'macos-latest' || matrix.os == 'windows-latest'
run: manim-slides Example ThreeDExample --skip-all

View File

@ -15,3 +15,7 @@ repos:
- id: isort
name: isort (python)
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 22.8.0
hooks:
- id: black

169
README.md
View File

@ -1,33 +1,70 @@
![Manim Slides Logo](https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png)
[![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)
# 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!
## Install
- [Install](#install)
* [Dependencies](#dependencies)
* [Pip install](#pip-install)
* [Install From Repository](#install-from-repository)
- [Usage](#usage)
* [Basic Example](#basic-example)
* [Key Bindings](#key-bindings)
* [Other Examples](#other-examples)
- [Features and Comparison with Original manim-presentation](#features-and-comparison-with-original-manim-presentation)
- [Contributing](#contributing)
```
## Installation
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a [virtualenv](https://docs.python.org/3/tutorial/venv.html) for a local installation.
### Dependencies
Manim Slides requires either Manim or ManimGL to be installed. Having both packages installed is fine too.
If none of those packages are installed, please refer to their specifc installation guidelines:
- [Manim](https://docs.manim.community/en/stable/installation.html)
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
### Pip Install
The recommended way to install the latest release is to use pip:
```bash
pip install manim-slides
```
## Usage
### Install From Repository
Use the class `Slide` as your scenes base class:
```python
from manim_slides import Slide
An alternative way to install Manim Slides is to clone the git repository, and install from there:
class Example(Slide):
def construct(self):
...
```bash
git clone https://github.com/jeertmans/manim-slides
pip install -e .
```
call `self.pause()` when you want to pause the playback and wait for an input to continue (check the keybindings).
> *Note:* the `-e` flag allows you to edit the files, and observe the changes directly when using Manim Slides
## Usage
Using Manim Slides is a two-step process:
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.pause()` everytime you want to create a new slide.
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
### Basic Example
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
```python
# example.py
from manim import *
# or: from manimlib import *
from manim_slides import Slide
@ -38,105 +75,103 @@ class Example(Slide):
dot = Dot()
self.play(GrowFromCenter(circle))
self.pause()
self.pause() # Waits user to press continue to go to the next slide
self.start_loop()
self.start_loop() # Start loop
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.end_loop() # This will loop until user inputs a key
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.pause() # Waits user to press continue to go to the next slide
self.wait()
self.wait() # The presentation directly exits after last animation
```
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(..)`.
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(...)`.
First, render the animation files:
```bash
manim example.py
# or
manimgl example.py
```
To start the presentation using `Scene1`, `Scene2` and so on simply run:
```
manim-slides Scene1 Scene2...
```bash
manim-slides [OPTIONS] Scene1 Scene2...
```
## Keybindings
Or in this example:
Default keybindings to control the presentation:
```bash
manim-slides Example
```
## Key Bindings
The default key bindings to control the presentation are:
| Keybinding | Action |
|:-----------:|:------------------------:|
| Right Arrow | Continue/Next Slide |
| Left Arrow | Previous Slide |
| R | Re-Animate Current Slide |
| R | Replay Current Slide |
| V | Reverse Current Slide |
| Spacebar | Play/Pause |
| Q | Quit |
You can run the **configuration wizard** to change those key bindings:
You can run the **configuration wizard** with:
```
```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`.
Alternatively you can specify different key bindings 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.
## Run Example
## Other Examples
Clone this repository:
```
git clone https://github.com/jeertmans/manim-slides.git
cd manim-slides
```
Install `manim` and `manim-slides`:
```
pip install manim manim-slides
# or
pip install manimgl manim-slides
```
Render the example scene:
```
manim -qh example.py Example
# or
manimgl --hd example.py Example
```
Run the presentation
```
manim-slides Example
```
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`
## Features and Comparison with original manim-presentation
Here are a few things that I implemented (or that I'm planning to implement) on top of the original work:
Below is a non-exhaustive list of features:
- [x] Allowing multiple keys to control one action (useful when you use a laser pointer)
- [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
| Feature | `manim-slides` | `manim-presentation` |
|:--------|:--------------:|:--------------------:|
| Support for Manim | :heavy_check_mark: | :heavy_check_mark: |
| Support for ManimGL | :heavy_check_mark: | :heavy_multiplication_x: |
| Configurable key bindings | :heavy_check_mark: | :heavy_check_mark: |
| Configurable paths | :heavy_check_mark: | :heavy_multiplication_x: |
| Play / Pause slides | :heavy_check_mark: | :heavy_check_mark: |
| Next / Previous slide | :heavy_check_mark: | :heavy_check_mark: |
| Replay slide | :heavy_check_mark: | :heavy_check_mark: |
| Reverse slide | :heavy_check_mark: | :heavy_multiplication_x: |
| Multiple key per actions | :heavy_check_mark: | :heavy_multiplication_x: |
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
## Contributions and license
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.
## Contributing
Contributions are more than welcome!
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
[pypi-version-url]: https://pypi.org/project/manim-slides/

View File

@ -1,6 +1,15 @@
# If you want to use manimgl, uncomment change
# manim to manimlib
from manimlib import *
import sys
if "manim" in sys.modules:
from manim import *
MANIMGL = False
elif "manimlib" in sys.modules:
from manimlib import *
MANIMGL = True
else:
raise ImportError("This script must be run with either `manim` or `manimgl`")
from manim_slides import Slide, ThreeDSlide
@ -31,37 +40,88 @@ class Example(Slide):
self.play(dot.animate.move_to(ORIGIN))
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
# For ThreeDExample, things are different
self.add(axes)
if not MANIMGL:
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
class ThreeDExample(ThreeDSlide):
def construct(self):
axes = ThreeDAxes()
circle = Circle(radius=3, color=BLUE)
dot = Dot(color=RED)
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.add(axes)
self.pause()
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
self.play(GrowFromCenter(circle))
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.pause()
self.play(dot.animate.move_to(ORIGIN))
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
self.play(dot.animate.move_to(RIGHT * 3))
self.pause()
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
self.end_loop()
self.play(dot.animate.move_to(ORIGIN))
self.pause()
# Each slide MUST end with an animation (a self.wait is considered an animation)
self.play(dot.animate.move_to(ORIGIN))
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))
else:
# WARNING: 3b1b's manim change how ThreeDScene work,
# this is why things have to be managed differently.
class ThreeDExample(Slide):
CONFIG = {
"camera_class": ThreeDCamera,
}
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,
gamma=0,
)
self.play(GrowFromCenter(circle))
updater = lambda m, dt: m.increment_theta((75 * DEGREES / 4) * dt)
frame.add_updater(updater)
self.pause()
self.start_loop()
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
self.end_loop()
frame.remove_updater(updater)
self.play(frame.animate.set_theta(30 * DEGREES))
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))

View File

@ -1 +1 @@
__version__ = "3.2.2"
__version__ = "4.0.0rc1"

View File

@ -1,11 +1,15 @@
from typing import Optional, Set
import os
from enum import Enum
from typing import List, Optional, Set
from pydantic import BaseModel, root_validator, validator
from pydantic import BaseModel, FilePath, root_validator, validator
from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
class Key(BaseModel):
"""Represents a list of key codes, with optionally a name."""
ids: Set[int]
name: Optional[str] = None
@ -20,6 +24,8 @@ class Key(BaseModel):
class Config(BaseModel):
"""General Manim Slides config"""
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")
@ -47,3 +53,83 @@ class Config(BaseModel):
key.name = other_key.name or key.name
return self
class SlideType(str, Enum):
slide = "slide"
loop = "loop"
last = "last"
class SlideConfig(BaseModel):
type: SlideType
start_animation: int
end_animation: int
number: int
terminated: bool = False
@validator("start_animation", "end_animation")
def index_is_posint(cls, v: int):
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
return v
@validator("number")
def number_is_strictly_posint(cls, v: int):
if v <= 0:
raise ValueError("Slide number cannot be negative or zero")
return v
@root_validator
def start_animation_is_before_end(cls, values):
if values["start_animation"] >= values["end_animation"]:
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return values
def is_slide(self):
return self.type == SlideType.slide
def is_loop(self):
return self.type == SlideType.loop
def is_last(self):
return self.type == SlideType.last
class PresentationConfig(BaseModel):
slides: List[SlideConfig]
files: List[str]
@validator("files", pre=True, each_item=True)
def is_file_and_exists(cls, v):
if not os.path.exists(v):
raise ValueError(
f"Animation file {v} does not exist. Are you in the right directory?"
)
if not os.path.isfile(v):
raise ValueError(f"Animation file {v} is not a file")
return v
@root_validator
def animation_indices_match_files(cls, values):
files = values.get("files")
slides = values.get("slides")
if files is None or slides is None:
return values
n_files = len(files)
for slide in slides:
if slide.end_animation > n_files:
raise ValueError(
f"The following slide's contains animations not listed in files {files}: {slide}"
)
return values

View File

@ -1,6 +1,20 @@
import os
import sys
from contextlib import contextmanager
from importlib.util import find_spec
@contextmanager
def suppress_stdout():
with open(os.devnull, "w") as devnull:
old_stdout = sys.stdout
sys.stdout = devnull
try:
yield
finally:
sys.stdout = old_stdout
MANIM_PACKAGE_NAME = "manim"
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
@ -17,14 +31,20 @@ if MANIM_IMPORTED and MANIMGL_IMPORTED:
)
MANIM = True
MANIMGL = False
elif MANIM_AVAILABLE and not MANIMGL_IMPORTED:
elif MANIM_IMPORTED:
MANIM = True
MANIMGL = False
elif MANIMGL_IMPORTED:
MANIM = False
MANIMGL = True
elif MANIM_AVAILABLE:
MANIM = True
MANIMGL = False
elif MANIMGL_AVAILABLE:
MANIM = False
MANIMGL = True
else:
raise ImportError(
raise ModuleNotFoundError(
"Either manim (community) or manimgl (3b1b) package must be installed"
)
@ -37,9 +57,10 @@ if MANIMGL:
from manimlib.logger import log as logger
else:
from manim import Scene, ThreeDScene, config, logger
with suppress_stdout(): # Avoids printing "Manim Community v..."
from manim import Scene, ThreeDScene, config, logger
try: # For manim<v0.16.0.post0
from manim.constants import FFMPEG_BIN as FFMPEG_BIN
except ImportError:
FFMPEG_BIN = config.ffmpeg_executable
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

@ -5,6 +5,7 @@ import platform
import sys
import time
from enum import IntEnum, auto, unique
from typing import List, Tuple
if platform.system() == "Windows":
import ctypes
@ -12,124 +13,197 @@ if platform.system() == "Windows":
import click
import cv2
import numpy as np
from pydantic import ValidationError
from .commons import config_path_option
from .config import Config
from .config import Config, PresentationConfig, SlideConfig, SlideType
from .defaults import CONFIG_PATH, FOLDER_PATH
from .slide import reverse_video_path
WINDOW_NAME = "Manim Slides"
@unique
class State(IntEnum):
"""Represents all possible states of a slide presentation."""
PLAYING = auto()
PAUSED = auto()
WAIT = auto()
END = auto()
def __str__(self):
def __str__(self) -> str:
return self.name.capitalize()
def now() -> int:
"""Returns time.time() in seconds."""
return round(time.time() * 1000)
def fix_time(x: float) -> float:
return x if x > 0 else 1
def fix_time(t: float) -> float:
"""Clips time t such that it is always positive."""
return t if t > 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"]]
"""Creates presentation from a configuration object."""
def __init__(self, config: PresentationConfig):
self.slides: List[SlideConfig] = config.slides
self.files: List[str] = config.files
self.current_slide_index = 0
self.current_animation = self.current_slide.start_animation
self.loaded_animation_cap = -1
self.cap = None # cap = cv2.VideoCapture
self.reverse = False
self.reversed_slide = -1
self.reversed_animation = -1
self.lastframe = []
self.lastframe = None
self.caps = [None for _ in self.files]
self.reset()
self.add_last_slide()
@property
def current_slide(self) -> SlideConfig:
"""Returns currently playing slide."""
return self.slides[self.current_slide_index]
@property
def first_slide(self) -> SlideConfig:
"""Returns first slide."""
return self.slides[0]
@property
def last_slide(self) -> SlideConfig:
"""Returns last slide."""
return self.slides[-1]
def release_cap(self):
"""Releases current Video Capture, if existing."""
if not self.cap is None:
self.cap.release()
self.loaded_animation_cap = -1
def load_animation_cap(self, animation: int):
"""Loads video file of given animation."""
# We must load a new VideoCapture file if:
if (self.loaded_animation_cap != animation) or (
self.reverse and self.reversed_animation != animation
): # cap already loaded
self.release_cap()
file = self.files[animation]
if self.reverse:
file = "{}_reversed{}".format(*os.path.splitext(file))
self.reversed_animation = animation
self.cap = cv2.VideoCapture(file)
self.loaded_animation_cap = animation
@property
def current_cap(self) -> cv2.VideoCapture:
"""Returns current VideoCapture object."""
self.load_animation_cap(self.current_animation)
return self.cap
def rewind_current_slide(self):
"""Rewinds current slide to first frame."""
if self.reverse:
self.current_animation = self.current_slide.end_animation - 1
else:
self.current_animation = self.current_slide.start_animation
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def reverse_current_slide(self):
"""Reverses current slide."""
self.reverse = True
self.rewind_current_slide()
def load_next_slide(self):
"""Loads next slide."""
if self.reverse:
self.reverse = False
self.reversed_animation = -1
self.release_cap()
self.rewind_current_slide()
elif self.current_slide.is_last():
self.current_slide.terminated = True
else:
self.current_slide_index = min(
len(self.slides) - 1, self.current_slide_index + 1
)
self.rewind_current_slide()
def load_previous_slide(self):
"""Loads previous slide."""
self.current_slide_index = max(0, self.current_slide_index - 1)
self.rewind_current_slide()
@property
def fps(self) -> int:
"""Returns the number of frames per second of the current video."""
return self.current_cap.get(cv2.CAP_PROP_FPS)
def add_last_slide(self):
last_slide_end = self.slides[-1]["end_animation"]
last_animation = len(self.files)
"""Add a 'last' slide to the end of slides."""
self.slides.append(
dict(
start_animation=last_slide_end,
end_animation=last_animation,
type="last",
number=len(self.slides) + 1,
terminated=False,
SlideConfig(
start_animation=self.last_slide.end_animation,
end_animation=self.last_slide.end_animation + 1,
type=SlideType.last,
number=self.last_slide.number + 1,
)
)
def reset(self):
"""Rests current presentation."""
self.current_animation = 0
self.load_this_cap(0)
self.current_slide_i = 0
self.slides[-1]["terminated"] = False
self.load_animation_cap(0)
self.current_slide_index = 0
self.slides[-1].terminated = False
def next(self):
if self.current_slide["type"] == "last":
self.current_slide["terminated"] = True
def load_last_slide(self):
"""Loads last slide."""
self.current_slide_index = len(self.slides) - 2
assert (
self.current_slide_index >= 0
), "Slides should be at list of a least two elements"
self.current_animation = self.current_slide.start_animation
self.load_animation_cap(self.current_animation)
self.slides[-1].terminated = False
@property
def next_animation(self) -> int:
"""Returns the next animation."""
if self.reverse:
return self.current_animation - 1
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)
return self.current_animation + 1
@property
def current_slide(self):
return self.slides[self.current_slide_i]
def is_last_animation(self) -> int:
"""Returns True if current animation is the last one of current slide."""
if self.reverse:
return self.current_animation == self.current_slide.start_animation
else:
return self.next_animation == self.current_slide.end_animation
@property
def current_cap(self):
self.load_this_cap(self.current_animation)
return self.caps[self.current_animation]
def update_state(self, state) -> Tuple[np.ndarray, State]:
"""
Updates the current state given the previous one.
@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):
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.
"""
if state == State.PAUSED:
if len(self.lastframe) == 0:
if self.lastframe is None:
_, self.lastframe = self.current_cap.read()
return self.lastframe, state
still_playing, frame = self.current_cap.read()
@ -137,36 +211,31 @@ class Presentation:
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"]:
elif self.current_slide.is_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()
else: # not still playing
if self.is_last_animation:
if self.current_slide.is_slide():
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.is_loop():
if self.reverse:
state = State.WAIT
else:
self.current_animation = self.current_slide.start_animation
state = State.PLAYING
self.rewind_current_slide()
elif self.current_slide.is_last():
self.current_slide.terminated = True
elif (
self.current_slide["type"] == "last"
and self.current_slide["end_animation"] == self.current_animation
self.current_slide.is_last()
and self.current_slide.end_animation == self.current_animation
):
print("HERE")
state = State.WAIT
else:
# Play next video!
self.current_animation += 1
self.load_this_cap(self.current_animation)
self.current_animation = self.next_animation
self.load_animation_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)
@ -174,31 +243,50 @@ class Presentation:
class Display:
def __init__(self, presentations, config, start_paused=False, fullscreen=False):
"""Displays one or more presentations one after each other."""
def __init__(
self,
presentations,
config,
start_paused=False,
fullscreen=False,
skip_all=False,
):
self.presentations = presentations
self.start_paused = start_paused
self.config = config
self.skip_all = skip_all
self.fullscreen = fullscreen
self.is_windows = platform.system() == "Windows"
self.state = State.PLAYING
self.lastframe = None
self.current_presentation_i = 0
self.current_presentation_index = 0
self.exit = False
self.lag = 0
self.last_time = now()
if platform.system() == "Windows":
if self.is_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)
if self.fullscreen:
cv2.namedWindow(WINDOW_NAME, cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty(
"Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
WINDOW_NAME, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
)
def resize_frame_to_screen(self, frame: np.ndarray):
"""
Resizes a given frame to match screen dimensions.
Only works on Windows.
"""
assert self.is_windows, "Only Windows platforms need this method"
frame_height, frame_width = frame.shape[:2]
scale_height = self.screen_height / frame_height
@ -206,14 +294,16 @@ class Display:
scale = min(scale_height, scale_width)
return cv2.resize(frame, (int(scale * frame_height, scale * frame_width)))
return cv2.resize(frame, (int(scale * frame_height), int(scale * frame_width)))
@property
def current_presentation(self):
return self.presentations[self.current_presentation_i]
def current_presentation(self) -> Presentation:
"""Returns the current presentation"""
return self.presentations[self.current_presentation_index]
def run(self):
while True:
"""Runs a series of presentations until end or exit."""
while not self.exit:
self.lastframe, self.state = self.current_presentation.update_state(
self.state
)
@ -222,27 +312,30 @@ class Display:
self.state = State.PAUSED
self.start_paused = False
if self.state == State.END:
if self.current_presentation_i == len(self.presentations) - 1:
if self.current_presentation_index == len(self.presentations) - 1:
self.quit()
continue
else:
self.current_presentation_i += 1
self.current_presentation_index += 1
self.state = State.PLAYING
self.handle_key()
self.show_video()
self.show_info()
def show_video(self):
"""Shows updated video."""
self.lag = now() - self.last_time
self.last_time = now()
frame = self.lastframe
if platform.system() == "Windows":
if self.is_windows and self.fullscreen:
frame = self.resize_frame_to_screen(frame)
cv2.imshow("Video", frame)
cv2.imshow(WINDOW_NAME, frame)
def show_info(self):
"""Shows updated information about presentations."""
info = np.zeros((130, 420), np.uint8)
font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255)
grid_x = [30, 230]
@ -258,27 +351,28 @@ class Display:
cv2.putText(
info,
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
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']}",
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)}",
f"Scene {self.current_presentation_index + 1}/{len(self.presentations)}",
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
*font_args,
)
cv2.imshow("Info", info)
cv2.imshow(f"{WINDOW_NAME}: Info", info)
def handle_key(self):
"""Handles key strokes."""
sleep_time = math.ceil(1000 / self.current_presentation.fps)
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
@ -291,28 +385,34 @@ class Display:
elif self.state == State.WAIT and (
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
):
self.current_presentation.next()
self.current_presentation.load_next_slide()
self.state = State.PLAYING
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
self.current_presentation.next()
elif (
self.state == State.PLAYING and self.config.CONTINUE.match(key)
) or self.skip_all:
self.current_presentation.load_next_slide()
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()
if self.current_presentation.current_slide_index == 0:
if self.current_presentation_index == 0:
self.current_presentation.rewind_current_slide()
else:
self.current_presentation_index -= 1
self.current_presentation.load_last_slide()
self.state = State.PLAYING
else:
self.current_presentation.prev()
self.current_presentation.load_previous_slide()
self.state = State.PLAYING
elif self.config.REVERSE.match(key):
self.current_presentation.reverse_slide()
self.current_presentation.reverse_current_slide()
self.state = State.PLAYING
elif self.config.REWIND.match(key):
self.current_presentation.rewind_slide()
self.current_presentation.rewind_current_slide()
self.state = State.PLAYING
def quit(self):
"""Destroys all windows created by presentations and exits gracefully."""
cv2.destroyAllWindows()
sys.exit()
self.exit = True
@click.command()
@ -330,12 +430,18 @@ def list_scenes(folder):
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder):
def _list_scenes(folder) -> List[str]:
"""Lists available scenes in given directory."""
scenes = []
for file in os.listdir(folder):
if file.endswith(".json"):
scenes.append(os.path.basename(file)[:-5])
try:
filepath = os.path.join(folder, file)
_ = PresentationConfig.parse_file(filepath)
scenes.append(os.path.basename(file)[:-5])
except Exception as e: # Could not parse this file as a proper presentation config
pass
return scenes
@ -352,12 +458,12 @@ def _list_scenes(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",
"--skip-all",
is_flag=True,
help="Show the next animation first frame as last frame (hack).",
help="Skip all slides, useful the test if slides are working.",
)
@click.help_option("-h", "--help")
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
def present(scenes, config_path, folder, start_paused, fullscreen, skip_all):
"""Present the different scenes."""
if len(scenes) == 0:
@ -402,15 +508,25 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
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))
try:
config = PresentationConfig.parse_file(config_file)
presentations.append(Presentation(config))
except ValidationError as e:
raise click.UsageError(str(e))
if os.path.exists(config_path):
config = Config.parse_file(config_path)
try:
config = Config.parse_file(config_path)
except ValidationError as e:
raise click.UsageError(str(e))
else:
config = Config()
display = Display(
presentations, config=config, start_paused=start_paused, fullscreen=fullscreen
presentations,
config=config,
start_paused=start_paused,
fullscreen=fullscreen,
skip_all=skip_all,
)
display.run()

View File

@ -3,25 +3,27 @@ import os
import platform
import shutil
import subprocess
from typing import List
from tqdm import tqdm
from .config import PresentationConfig, SlideConfig, SlideType
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):
"""Reverses a video file, writting the result to `dst`."""
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate()
class Slide(Scene):
"""
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
"""
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
if MANIMGL:
if not os.path.isdir("videos"):
@ -44,7 +46,8 @@ class Slide(Scene):
self.pause_start_animation = 0
@property
def partial_movie_files(self):
def partial_movie_files(self) -> List[str]:
"""Returns a list of partial movie files, a.k.a animations."""
if MANIMGL:
from manimlib.utils.file_ops import get_sorted_integer_files
@ -59,7 +62,8 @@ class Slide(Scene):
return self.renderer.file_writer.partial_movie_files
@property
def show_progress_bar(self):
def show_progress_bar(self) -> bool:
"""Returns True if progress bar should be displayed."""
if MANIMGL:
return getattr(super(Scene, self), "show_progress_bar", True)
else:
@ -67,19 +71,22 @@ class Slide(Scene):
@property
def leave_progress_bar(self):
"""Returns True if progress bar should be left after completed."""
if MANIMGL:
return getattr(super(Scene, self), "leave_progress_bars", False)
else:
return config["progress_bar"] == "leave"
def play(self, *args, **kwargs):
"""Overloads `self.play` and increment animation count."""
super().play(*args, **kwargs)
self.current_animation += 1
def pause(self):
"""Creates a new slide with previous animations."""
self.slides.append(
dict(
type="slide",
SlideConfig(
type=SlideType.slide,
start_animation=self.pause_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
@ -89,16 +96,18 @@ class Slide(Scene):
self.pause_start_animation = self.current_animation
def start_loop(self):
"""Starts a loop."""
assert self.loop_start_animation is None, "You cannot nest loops"
self.loop_start_animation = self.current_animation
def end_loop(self):
"""Ends an existing loop."""
assert (
self.loop_start_animation is not None
), "You have to start a loop before ending it"
self.slides.append(
dict(
type="loop",
SlideConfig(
type=SlideType.loop,
start_animation=self.loop_start_animation,
end_animation=self.current_animation,
number=self.current_slide,
@ -109,7 +118,11 @@ class Slide(Scene):
self.pause_start_animation = self.current_animation
def save_slides(self, use_cache=True):
"""
Saves slides, optionally using cached files.
Note that cached files only work with Manim.
"""
if not os.path.exists(self.output_folder):
os.mkdir(self.output_folder)
@ -139,9 +152,7 @@ class Slide(Scene):
disable=not self.show_progress_bar,
):
filename = os.path.basename(src_file)
_hash, ext = os.path.splitext(filename)
rev_filename = f"{_hash}_reversed{ext}"
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
dst_file = os.path.join(scene_files_folder, filename)
# We only copy animation if it was not present
@ -165,9 +176,9 @@ class Slide(Scene):
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()
with open(slide_path, "w") as f:
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
logger.info(
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
)
@ -191,4 +202,10 @@ class Slide(Scene):
class ThreeDSlide(Slide, ThreeDScene):
"""
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
Note that ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
"""
pass

View File

@ -1,18 +1,25 @@
import importlib.util
import os
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+")
spec = importlib.util.spec_from_file_location(
"__version__", os.path.join("manim_slides", "__version__.py")
)
version = importlib.util.module_from_spec(spec)
spec.loader.exec_module(version)
with open("README.md", "r") as f:
long_description = f.read()
setuptools.setup(
name="manim-slides",
version=version,
version=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",
@ -32,7 +39,7 @@ setuptools.setup(
"numpy>=1.19.3",
"pydantic>=1.9.1",
"opencv-python>=4.6",
"tqdm",
"tqdm>=4.62.3",
],
classifiers=[
"Programming Language :: Python :: 3",

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

39
static/logo.py Normal file
View File

@ -0,0 +1,39 @@
from manim import *
class ManimSlidesLogo(Scene):
def construct(self):
tex_template = TexTemplate()
tex_template.add_to_preamble(r"\usepackage{graphicx}\usepackage{fontawesome5}")
self.camera.background_color = "#ffffff"
logo_green = "#87c2a5"
logo_blue = "#525893"
logo_red = "#e07a5f"
logo_black = "#343434"
ds_m = MathTex(r"\mathbb{M}", fill_color=logo_black).scale(7)
ds_m.shift(2.25 * LEFT + 1.5 * UP)
slides = MathTex(r"\mathbb{S}\text{lides}", fill_color=logo_black).scale(4)
slides.next_to(ds_m, DOWN)
slides.shift(DOWN)
play = Tex(
r"\faStepBackward\faStepForward",
fill_color=logo_black,
tex_template=tex_template,
).scale(4)
play.next_to(ds_m, LEFT)
play.shift(LEFT + 0.5 * DOWN)
comment = Tex(
r"\reflectbox{\faComment*[regular]}",
fill_color=logo_black,
tex_template=tex_template,
).scale(9)
comment.move_to(play)
comment.shift(0.4 * DOWN)
circle = Circle(color=logo_green, fill_opacity=1).shift(LEFT)
square = Square(color=logo_blue, fill_opacity=1).shift(UP)
triangle = Triangle(color=logo_red, fill_opacity=1).shift(RIGHT)
logo = VGroup(
triangle, square, circle, ds_m, slides, comment, play
) # order matters
logo.move_to(ORIGIN)
self.add(logo)