mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-18 11:05:54 +08:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
d6ec0d3da9 | |||
546451e019 | |||
2457ca8a05 | |||
9900b3123e | |||
ee92e0aa88 | |||
cbee6320f5 | |||
382084f9ef | |||
068484b828 | |||
91f8d97acf | |||
49cdedc6fe | |||
fe1fa059f6 | |||
3f6d2e5e57 | |||
99ad798155 | |||
84c25f1ed5 | |||
7fb3fa01dd | |||
2d2a225afe | |||
b9d2cd92b5 |
50
.github/workflows/pages.yml
vendored
Normal file
50
.github/workflows/pages.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
- name: Install Linux Dependencies
|
||||
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg
|
||||
- name: Install Python dependencies
|
||||
run: pip install manim sphinx sphinx_click furo
|
||||
- name: Install local Python package
|
||||
run: pip install -e .
|
||||
- name: Build docs
|
||||
run: cd docs && make html
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload docs/build/html dir
|
||||
path: 'docs/build/html/'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -17,3 +17,5 @@ slides/
|
||||
videos/
|
||||
|
||||
images/
|
||||
|
||||
docs/build/
|
||||
|
@ -6,7 +6,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 22.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@ -19,3 +19,7 @@ repos:
|
||||
rev: 22.8.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/jendrikseipp/vulture
|
||||
rev: v2.3
|
||||
hooks:
|
||||
- id: vulture
|
||||
|
16
README.md
16
README.md
@ -9,7 +9,7 @@ Tool for live presentations using either [Manim (community edition)](https://www
|
||||
|
||||
> **_NOTE:_** This project extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
|
||||
|
||||
- [Install](#install)
|
||||
- [Installation](#installation)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Pip install](#pip-install)
|
||||
* [Install From Repository](#install-from-repository)
|
||||
@ -18,6 +18,8 @@ Tool for live presentations using either [Manim (community edition)](https://www
|
||||
* [Key Bindings](#key-bindings)
|
||||
* [Other Examples](#other-examples)
|
||||
- [Features and Comparison with Original manim-presentation](#features-and-comparison-with-original-manim-presentation)
|
||||
- [F.A.Q](#faq)
|
||||
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Installation
|
||||
@ -57,6 +59,8 @@ Using Manim Slides is a two-step process:
|
||||
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.pause()` everytime you want to create a new slide.
|
||||
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
|
||||
|
||||
The command-line documentation is available [online](https://eertmans.be/manim-slides/).
|
||||
|
||||
### Basic Example
|
||||
|
||||
|
||||
@ -168,6 +172,16 @@ Below is a non-exhaustive list of features:
|
||||
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
|
||||
## 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%:
|
||||
|
||||

|
||||
|
||||
in *Settings*->*Display*.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
BIN
docs/source/_static/logo.png
Normal file
BIN
docs/source/_static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
26
docs/source/conf.py
Normal file
26
docs/source/conf.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "Manim Slides"
|
||||
copyright = "2022, Jérome Eertmans"
|
||||
author = "Jérome Eertmans"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx_click"]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = []
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = "furo"
|
||||
html_static_path = ["_static"]
|
20
docs/source/index.rst
Normal file
20
docs/source/index.rst
Normal file
@ -0,0 +1,20 @@
|
||||
.. manim-slides documentation master file, created by
|
||||
sphinx-quickstart on Wed Sep 21 15:07:28 2022.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
.. image:: _static/logo.png
|
||||
:width: 600
|
||||
:align: center
|
||||
:alt: Manim Slide logo
|
||||
|
||||
Welcome to Manim Slide's CLI documentation!
|
||||
===========================================
|
||||
|
||||
This page contains an exhaustive list of all the commands available with `manim-slides`.
|
||||
|
||||
If you need help installing or using Manim Slide, please refer to the `GitHub README <https://github.com/jeertmans/manim-slides>`_.
|
||||
|
||||
.. click:: manim_slides.main:cli
|
||||
:prog: manim-slides
|
||||
:nested: full
|
@ -1 +1 @@
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "4.1.1"
|
||||
|
@ -1,9 +1,12 @@
|
||||
from typing import Callable
|
||||
|
||||
import click
|
||||
|
||||
from .defaults import CONFIG_PATH
|
||||
|
||||
|
||||
def config_path_option(function):
|
||||
def config_path_option(function) -> Callable:
|
||||
"""Wraps a function to add configuration path option."""
|
||||
return click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
@ -11,10 +14,12 @@ def config_path_option(function):
|
||||
default=CONFIG_PATH,
|
||||
type=click.Path(dir_okay=False),
|
||||
help="Set path to configuration file.",
|
||||
show_default=True,
|
||||
)(function)
|
||||
|
||||
|
||||
def config_options(function):
|
||||
def config_options(function) -> Callable:
|
||||
"""Wraps 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."
|
||||
|
@ -2,7 +2,7 @@ import os
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from pydantic import BaseModel, FilePath, root_validator, validator
|
||||
from pydantic import BaseModel, root_validator, validator
|
||||
|
||||
from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE
|
||||
|
||||
@ -84,6 +84,11 @@ class SlideConfig(BaseModel):
|
||||
def start_animation_is_before_end(cls, values):
|
||||
if values["start_animation"] >= values["end_animation"]:
|
||||
|
||||
if values["start_animation"] == values["end_animation"] == 0:
|
||||
raise ValueError(
|
||||
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting."
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
"Start animation index must be strictly lower than end animation index"
|
||||
)
|
||||
|
@ -3,7 +3,6 @@ import platform
|
||||
import cv2
|
||||
|
||||
FONT_ARGS = (cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 1, cv2.LINE_AA)
|
||||
PIXELS_PER_CHARACTER = 20
|
||||
FOLDER_PATH: str = "./slides"
|
||||
CONFIG_PATH: str = ".manim-slides.json"
|
||||
|
||||
|
@ -10,6 +10,11 @@ from .wizard import init, wizard
|
||||
@click.version_option(__version__, "-v", "--version")
|
||||
@click.help_option("-h", "--help")
|
||||
def cli():
|
||||
"""
|
||||
Manim Slides command-line utilities.
|
||||
|
||||
If not command is specified, defaults to `present`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -7,20 +7,29 @@ import time
|
||||
from enum import IntEnum, auto, unique
|
||||
from typing import List, Tuple
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import ctypes
|
||||
|
||||
import click
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pydantic import ValidationError
|
||||
from tqdm import tqdm
|
||||
|
||||
from .commons import config_path_option
|
||||
from .config import Config, PresentationConfig, SlideConfig, SlideType
|
||||
from .defaults import CONFIG_PATH, FOLDER_PATH, FONT_ARGS
|
||||
|
||||
INTERPOLATION_FLAGS = {
|
||||
"nearest": cv2.INTER_NEAREST,
|
||||
"linear": cv2.INTER_LINEAR,
|
||||
"cubic": cv2.INTER_CUBIC,
|
||||
"area": cv2.INTER_AREA,
|
||||
"lanczos4": cv2.INTER_LANCZOS4,
|
||||
"linear-exact": cv2.INTER_LINEAR_EXACT,
|
||||
"nearest-exact": cv2.INTER_NEAREST_EXACT,
|
||||
}
|
||||
|
||||
WINDOW_NAME = "Manim Slides"
|
||||
WINDOW_INFO_NAME = f"{WINDOW_NAME}: Info"
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
|
||||
|
||||
@unique
|
||||
@ -37,7 +46,7 @@ class State(IntEnum):
|
||||
|
||||
|
||||
def now() -> int:
|
||||
"""Returns time.time() in seconds."""
|
||||
"""Returns time.time() in milliseconds."""
|
||||
return round(time.time() * 1000)
|
||||
|
||||
|
||||
@ -55,6 +64,7 @@ class Presentation:
|
||||
|
||||
self.current_slide_index = 0
|
||||
self.current_animation = self.current_slide.start_animation
|
||||
self.current_file = None
|
||||
|
||||
self.loaded_animation_cap = -1
|
||||
self.cap = None # cap = cv2.VideoCapture
|
||||
@ -104,6 +114,8 @@ class Presentation:
|
||||
file = "{}_reversed{}".format(*os.path.splitext(file))
|
||||
self.reversed_animation = animation
|
||||
|
||||
self.current_file = file
|
||||
|
||||
self.cap = cv2.VideoCapture(file)
|
||||
self.loaded_animation_cap = animation
|
||||
|
||||
@ -122,6 +134,13 @@ class Presentation:
|
||||
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
def cancel_reverse(self):
|
||||
"""Cancels any effet produced by a reversed slide."""
|
||||
if self.reverse:
|
||||
self.reverse = False
|
||||
self.reversed_animation = -1
|
||||
self.release_cap()
|
||||
|
||||
def reverse_current_slide(self):
|
||||
"""Reverses current slide."""
|
||||
self.reverse = True
|
||||
@ -130,9 +149,7 @@ class Presentation:
|
||||
def load_next_slide(self):
|
||||
"""Loads next slide."""
|
||||
if self.reverse:
|
||||
self.reverse = False
|
||||
self.reversed_animation = -1
|
||||
self.release_cap()
|
||||
self.cancel_reverse()
|
||||
self.rewind_current_slide()
|
||||
elif self.current_slide.is_last():
|
||||
self.current_slide.terminated = True
|
||||
@ -144,6 +161,7 @@ class Presentation:
|
||||
|
||||
def load_previous_slide(self):
|
||||
"""Loads previous slide."""
|
||||
self.cancel_reverse()
|
||||
self.current_slide_index = max(0, self.current_slide_index - 1)
|
||||
self.rewind_current_slide()
|
||||
|
||||
@ -196,6 +214,11 @@ class Presentation:
|
||||
else:
|
||||
return self.next_animation == self.current_slide.end_animation
|
||||
|
||||
@property
|
||||
def current_frame_number(self) -> int:
|
||||
"""Returns current frame number."""
|
||||
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||
|
||||
def update_state(self, state) -> Tuple[np.ndarray, State]:
|
||||
"""
|
||||
Updates the current state given the previous one.
|
||||
@ -252,14 +275,22 @@ class Display:
|
||||
start_paused=False,
|
||||
fullscreen=False,
|
||||
skip_all=False,
|
||||
resolution=(1280, 720),
|
||||
resolution=(1980, 1080),
|
||||
interpolation_flag=cv2.INTER_LINEAR,
|
||||
record_to=None,
|
||||
):
|
||||
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.resolution = resolution
|
||||
self.interpolation_flag = interpolation_flag
|
||||
self.record_to = record_to
|
||||
self.recordings = []
|
||||
self.window_flags = (
|
||||
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_NORMAL
|
||||
)
|
||||
|
||||
self.state = State.PLAYING
|
||||
self.lastframe = None
|
||||
@ -274,43 +305,20 @@ class Display:
|
||||
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_AUTOSIZE,
|
||||
)
|
||||
|
||||
if self.is_windows:
|
||||
user32 = ctypes.windll.user32
|
||||
self.screen_width, self.screen_height = user32.GetSystemMetrics(
|
||||
0
|
||||
), user32.GetSystemMetrics(1)
|
||||
|
||||
if self.fullscreen:
|
||||
cv2.namedWindow(WINDOW_NAME, cv2.WND_PROP_FULLSCREEN)
|
||||
cv2.namedWindow(
|
||||
WINDOW_NAME, cv2.WINDOW_GUI_NORMAL | cv2.WND_PROP_FULLSCREEN
|
||||
)
|
||||
cv2.setWindowProperty(
|
||||
WINDOW_NAME, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
|
||||
)
|
||||
else:
|
||||
cv2.namedWindow(
|
||||
WINDOW_NAME,
|
||||
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_NORMAL,
|
||||
)
|
||||
cv2.resizeWindow(WINDOW_NAME, *resolution)
|
||||
|
||||
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
|
||||
scale_width = self.screen_width / frame_width
|
||||
|
||||
scale = min(scale_height, scale_width)
|
||||
|
||||
return cv2.resize(frame, (int(scale * frame_height), int(scale * frame_width)))
|
||||
cv2.namedWindow(WINDOW_NAME, self.window_flags)
|
||||
cv2.resizeWindow(WINDOW_NAME, *self.resolution)
|
||||
|
||||
@property
|
||||
def current_presentation(self) -> Presentation:
|
||||
"""Returns the current presentation"""
|
||||
"""Returns the current presentation."""
|
||||
return self.presentations[self.current_presentation_index]
|
||||
|
||||
def run(self):
|
||||
@ -341,10 +349,25 @@ class Display:
|
||||
self.lag = now() - self.last_time
|
||||
self.last_time = now()
|
||||
|
||||
if not self.record_to is None:
|
||||
pres = self.current_presentation
|
||||
self.recordings.append(
|
||||
(pres.current_file, pres.current_frame_number, pres.fps)
|
||||
)
|
||||
|
||||
frame = self.lastframe
|
||||
|
||||
if self.is_windows and self.fullscreen:
|
||||
frame = self.resize_frame_to_screen(frame)
|
||||
# If Window was manually closed (impossible in fullscreen),
|
||||
# we reopen it
|
||||
if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
|
||||
cv2.namedWindow(WINDOW_NAME, self.window_flags)
|
||||
cv2.resizeWindow(WINDOW_NAME, *self.resolution)
|
||||
|
||||
if WINDOWS: # Only resize on Windows
|
||||
_, _, w, h = cv2.getWindowImageRect(WINDOW_NAME)
|
||||
|
||||
if (h, w) != frame.shape[:2]: # Only if shape is different
|
||||
frame = cv2.resize(frame, (w, h), self.interpolation_flag)
|
||||
|
||||
cv2.imshow(WINDOW_NAME, frame)
|
||||
|
||||
@ -408,7 +431,7 @@ class Display:
|
||||
elif self.config.BACK.match(key):
|
||||
if self.current_presentation.current_slide_index == 0:
|
||||
if self.current_presentation_index == 0:
|
||||
self.current_presentation.rewind_current_slide()
|
||||
self.current_presentation.load_previous_slide()
|
||||
else:
|
||||
self.current_presentation_index -= 1
|
||||
self.current_presentation.load_last_slide()
|
||||
@ -420,12 +443,42 @@ class Display:
|
||||
self.current_presentation.reverse_current_slide()
|
||||
self.state = State.PLAYING
|
||||
elif self.config.REWIND.match(key):
|
||||
self.current_presentation.cancel_reverse()
|
||||
self.current_presentation.rewind_current_slide()
|
||||
self.state = State.PLAYING
|
||||
|
||||
def quit(self):
|
||||
"""Destroys all windows created by presentations and exits gracefully."""
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if not self.record_to is None and len(self.recordings) > 0:
|
||||
file, frame_number, fps = self.recordings[0]
|
||||
|
||||
cap = cv2.VideoCapture(file)
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
|
||||
_, frame = cap.read()
|
||||
|
||||
w, h = frame.shape[:2]
|
||||
fourcc = cv2.VideoWriter_fourcc(*"XVID")
|
||||
out = cv2.VideoWriter(self.record_to, fourcc, fps, (h, w))
|
||||
|
||||
out.write(frame)
|
||||
|
||||
for _file, frame_number, _ in tqdm(
|
||||
self.recordings[1:], desc="Creating recording file", leave=False
|
||||
):
|
||||
if file != _file:
|
||||
cap.release()
|
||||
file = _file
|
||||
cap = cv2.VideoCapture(_file)
|
||||
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
|
||||
_, frame = cap.read()
|
||||
out.write(frame)
|
||||
|
||||
cap.release()
|
||||
out.release()
|
||||
|
||||
self.exit = True
|
||||
|
||||
|
||||
@ -435,6 +488,7 @@ class Display:
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
def list_scenes(folder):
|
||||
@ -454,7 +508,7 @@ def _list_scenes(folder) -> List[str]:
|
||||
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
|
||||
except Exception: # Could not parse this file as a proper presentation config
|
||||
pass
|
||||
|
||||
return scenes
|
||||
@ -468,6 +522,7 @@ def _list_scenes(folder) -> List[str]:
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
|
||||
@ -477,17 +532,48 @@ def _list_scenes(folder) -> List[str]:
|
||||
help="Skip all slides, useful the test if slides are working.",
|
||||
)
|
||||
@click.option(
|
||||
"-r",
|
||||
"--resolution",
|
||||
type=(int, int),
|
||||
default=(1280, 720),
|
||||
help="Window resolution used if fullscreen is not set. You may manually resize the window afterward.",
|
||||
default=(1920, 1080),
|
||||
help="Window resolution WIDTH HEIGHT used if fullscreen is not set. You may manually resize the window afterward.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--interpolation-flag",
|
||||
type=click.Choice(INTERPOLATION_FLAGS.keys(), case_sensitive=False),
|
||||
default="linear",
|
||||
help="Set the interpolation flag to be used when resizing image. See OpenCV cv::InterpolationFlags.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--record-to",
|
||||
type=click.Path(dir_okay=False),
|
||||
default=None,
|
||||
help="If set, the presentation will be recorded into a AVI video file with given name.",
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
def present(
|
||||
scenes, config_path, folder, start_paused, fullscreen, skip_all, resolution
|
||||
scenes,
|
||||
config_path,
|
||||
folder,
|
||||
start_paused,
|
||||
fullscreen,
|
||||
skip_all,
|
||||
resolution,
|
||||
interpolation_flag,
|
||||
record_to,
|
||||
):
|
||||
"""Present the different scenes."""
|
||||
"""
|
||||
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 len(scenes) == 0:
|
||||
scene_choices = _list_scenes(folder)
|
||||
@ -545,6 +631,13 @@ def present(
|
||||
else:
|
||||
config = Config()
|
||||
|
||||
if not record_to is None:
|
||||
_, ext = os.path.splitext(record_to)
|
||||
if ext.lower() != ".avi":
|
||||
raise click.UsageError(
|
||||
f"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
|
||||
)
|
||||
|
||||
display = Display(
|
||||
presentations,
|
||||
config=config,
|
||||
@ -552,5 +645,7 @@ def present(
|
||||
fullscreen=fullscreen,
|
||||
skip_all=skip_all,
|
||||
resolution=resolution,
|
||||
interpolation_flag=INTERPOLATION_FLAGS[interpolation_flag],
|
||||
record_to=record_to,
|
||||
)
|
||||
display.run()
|
||||
|
@ -7,7 +7,7 @@ import numpy as np
|
||||
|
||||
from .commons import config_options
|
||||
from .config import Config
|
||||
from .defaults import CONFIG_PATH, FONT_ARGS, PIXELS_PER_CHARACTER
|
||||
from .defaults import CONFIG_PATH, FONT_ARGS
|
||||
|
||||
WINDOW_NAME = "Manim Slides Configuration Wizard"
|
||||
WINDOW_SIZE = (120, 620)
|
||||
@ -38,6 +38,7 @@ def prompt(question: str) -> int:
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
def wizard(config_path, force, merge):
|
||||
"""Launch configuration wizard."""
|
||||
return _init(config_path, force, merge, skip_interactive=False)
|
||||
@ -45,12 +46,14 @@ def wizard(config_path, force, merge):
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
def init(config_path, force, merge, skip_interactive=False):
|
||||
"""Initialize a new default configuration file."""
|
||||
return _init(config_path, force, merge, skip_interactive=True)
|
||||
|
||||
|
||||
def _init(config_path, force, merge, skip_interactive=False):
|
||||
"""Actual initialization code for configuration file, with optional interactive mode."""
|
||||
|
||||
if os.path.exists(config_path):
|
||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||
|
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[tool.vulture]
|
||||
paths = ["manim_slides"]
|
BIN
static/windows_quality_fix.png
Normal file
BIN
static/windows_quality_fix.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Reference in New Issue
Block a user