mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-19 03:26:17 +08:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
5461a20257 | |||
5e3603b40b | |||
d056d8d8b1 | |||
c95929dc7d | |||
e08edb6fe1 | |||
efc3017df8 | |||
788727ea22 | |||
0bb0285b18 | |||
ce878bece2 | |||
1275e9119f | |||
5555ac2e54 | |||
838de83c75 | |||
f1f7146c7e | |||
fb02764bb7 | |||
f6f2e4090f | |||
146a2f7839 | |||
d282766f2d | |||
dec2f5e724 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 4.15.0
|
||||
current_version = 4.16.1
|
||||
commit = True
|
||||
message = chore(version): bump {current_version} to {new_version}
|
||||
|
||||
@ -10,3 +10,7 @@ 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}
|
||||
|
4
.github/workflows/pages.yml
vendored
4
.github/workflows/pages.yml
vendored
@ -46,8 +46,12 @@ jobs:
|
||||
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
|
||||
|
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
@ -29,6 +29,9 @@ jobs:
|
||||
python-version: ${{ matrix.pyversion }}
|
||||
cache: poetry
|
||||
|
||||
- name: Run apt-get update on Ubuntu
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: Install manim dependencies on Ubuntu
|
||||
run: |
|
||||
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
|
||||
@ -43,7 +46,7 @@ jobs:
|
||||
poetry install --with test
|
||||
|
||||
- name: Run pytest
|
||||
run: poetry run pytest
|
||||
run: poetry run pytest -x
|
||||
|
||||
build-examples:
|
||||
strategy:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -26,6 +26,8 @@ docs/build/
|
||||
|
||||
slides.html
|
||||
|
||||
docs/source/reference/.ipynb_checkpoints/
|
||||
|
||||
docs/source/_static/basic_example_assets/
|
||||
|
||||
docs/source/_static/basic_example.html
|
||||
@ -34,6 +36,7 @@ docs/source/_static/three_d_example.html
|
||||
|
||||
docs/source/_static/three_d_example_assets/
|
||||
|
||||
docs/source/reference/media/
|
||||
|
||||
# JOSE Paper
|
||||
paper/paper.pdf
|
||||
|
@ -24,7 +24,7 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.281
|
||||
rev: v0.0.282
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
|
32
CITATION.cff
Normal file
32
CITATION.cff
Normal 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: v4.16.1
|
@ -9,6 +9,7 @@
|
||||
[![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
|
||||
@ -261,7 +262,10 @@ you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
|
||||
[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.7971361.svg
|
||||
[doi-url]: https://doi.org/10.5281/zenodo.7971361
|
||||
[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
|
||||
|
@ -20,6 +20,7 @@ extensions = [
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.viewcode",
|
||||
# Additional
|
||||
"nbsphinx",
|
||||
"myst_parser",
|
||||
"sphinxext.opengraph",
|
||||
"sphinx_click",
|
||||
|
@ -10,6 +10,7 @@ cli
|
||||
examples
|
||||
gui
|
||||
html
|
||||
IPython magic <ipython_magic>
|
||||
sharing
|
||||
Sphinx Extension <sphinx_extension>
|
||||
```
|
||||
@ -26,6 +27,11 @@ Slides' executable.
|
||||
|
||||
[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.
|
||||
|
6
docs/source/reference/ipython_magic.md
Normal file
6
docs/source/reference/ipython_magic.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Manim Slides' IPython magic
|
||||
|
||||
```{eval-rst}
|
||||
.. automodule:: manim_slides.ipython.ipython_magic
|
||||
:members: ManimSlidesMagic
|
||||
```
|
100
docs/source/reference/magic_example.ipynb
Normal file
100
docs/source/reference/magic_example.ipynb
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"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.)\n",
|
||||
" \n",
|
||||
" self.play(Write(text))\n",
|
||||
" self.next_slide()\n",
|
||||
" self.start_loop()\n",
|
||||
" self.play(Indicate(text[-1], scale_factor=2., run_time=.5))\n",
|
||||
" self.end_loop()\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
|
||||
}
|
19
example.py
19
example.py
@ -72,10 +72,9 @@ class TestFileTooLong(Slide):
|
||||
class ConvertExample(Slide):
|
||||
"""WARNING: this example does not seem to work with ManimGL."""
|
||||
|
||||
def tinywait(self):
|
||||
self.wait(0.1)
|
||||
|
||||
def construct(self):
|
||||
self.wait_time_between_slides = 0.1
|
||||
|
||||
title = VGroup(
|
||||
Text("From Manim animations", t2c={"From": BLUE}),
|
||||
Text("to slides presentation", t2c={"to": BLUE}),
|
||||
@ -210,41 +209,32 @@ class Example(Slide):
|
||||
language="console",
|
||||
).shift(DOWN)
|
||||
|
||||
self.clear()
|
||||
|
||||
self.play(FadeIn(code))
|
||||
self.tinywait()
|
||||
self.play(self.wipe(title, code))
|
||||
self.next_slide()
|
||||
|
||||
self.play(FadeIn(step, shift=RIGHT))
|
||||
self.play(Transform(code, code_step_1))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_2))
|
||||
self.play(Transform(code, code_step_2))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_3))
|
||||
self.play(Transform(code, code_step_3))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_4))
|
||||
self.play(Transform(code, code_step_4))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_5))
|
||||
self.play(Transform(code, code_step_5))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
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))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
watch_text = Text("Watch result on next slides!").shift(2 * DOWN).scale(0.5)
|
||||
@ -264,10 +254,8 @@ class Example(Slide):
|
||||
self.play(Transform(dot, square))
|
||||
self.remove(dot)
|
||||
self.add(square)
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
self.play(Rotate(square, angle=PI / 4))
|
||||
self.tinywait()
|
||||
self.next_slide()
|
||||
|
||||
learn_more_text = (
|
||||
@ -280,7 +268,6 @@ class Example(Slide):
|
||||
)
|
||||
|
||||
self.play(Transform(square, learn_more_text))
|
||||
self.tinywait()
|
||||
|
||||
|
||||
# For ThreeDExample, things are different
|
||||
|
@ -13,6 +13,20 @@ class module(ModuleType):
|
||||
"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)
|
||||
|
||||
@ -43,6 +57,6 @@ new_module.__dict__.update(
|
||||
"__path__": __path__,
|
||||
"__doc__": __doc__,
|
||||
"__version__": __version__,
|
||||
"__all__": ("__version__", "Slides", "ThreeDSlide"),
|
||||
"__all__": ("__version__", "ManimSlidesMagic", "Slide", "ThreeDSlide"),
|
||||
}
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "4.15.0"
|
||||
__version__ = "4.16.1"
|
||||
|
@ -44,7 +44,7 @@ def config_options(function: F) -> F:
|
||||
def verbosity_option(function: F) -> F:
|
||||
"""Wraps a function to add verbosity option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
def callback(ctx: Context, param: Parameter, value: str) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
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
|
||||
@ -33,6 +35,29 @@ from .config import PresentationConfig
|
||||
from .logger import logger
|
||||
from .present import get_scenes_presentation_config
|
||||
|
||||
DATA_URI_FIX = r"""
|
||||
// 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 );
|
||||
"""
|
||||
|
||||
|
||||
def open_with_default(file: Path) -> None:
|
||||
system = platform.system()
|
||||
@ -61,6 +86,16 @@ def validate_config_option(
|
||||
return config
|
||||
|
||||
|
||||
def data_uri(file: Path) -> str:
|
||||
"""
|
||||
Reads a video and returns 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}"
|
||||
|
||||
|
||||
class Converter(BaseModel): # type: ignore
|
||||
presentation_configs: List[PresentationConfig] = []
|
||||
assets_dir: str = "{basename}_assets"
|
||||
@ -105,43 +140,48 @@ class Str(str):
|
||||
def __str__(self) -> str:
|
||||
"""Ensures that the string is correctly quoted."""
|
||||
if self in ["true", "false", "null"]:
|
||||
return super().__str__()
|
||||
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, Enum):
|
||||
class JsTrue(str, StrEnum):
|
||||
true = "true"
|
||||
|
||||
|
||||
class JsFalse(str, Enum):
|
||||
class JsFalse(str, StrEnum):
|
||||
false = "false"
|
||||
|
||||
|
||||
class JsBool(Str, Enum): # type: ignore
|
||||
class JsBool(Str, StrEnum): # type: ignore
|
||||
true = "true"
|
||||
false = "false"
|
||||
|
||||
|
||||
class JsNull(Str, Enum): # type: ignore
|
||||
class JsNull(Str, StrEnum): # type: ignore
|
||||
null = "null"
|
||||
|
||||
|
||||
class ControlsLayout(Str, Enum): # type: ignore
|
||||
class ControlsLayout(Str, StrEnum): # type: ignore
|
||||
edges = "edges"
|
||||
bottom_right = "bottom-right"
|
||||
|
||||
|
||||
class ControlsBackArrows(Str, Enum): # type: ignore
|
||||
class ControlsBackArrows(Str, StrEnum): # type: ignore
|
||||
faded = "faded"
|
||||
hidden = "hidden"
|
||||
visibly = "visibly"
|
||||
|
||||
|
||||
class SlideNumber(Str, Enum): # type: ignore
|
||||
class SlideNumber(Str, StrEnum): # type: ignore
|
||||
true = "true"
|
||||
false = "false"
|
||||
hdotv = "h.v"
|
||||
@ -150,24 +190,24 @@ class SlideNumber(Str, Enum): # type: ignore
|
||||
candt = "c/t"
|
||||
|
||||
|
||||
class ShowSlideNumber(Str, Enum): # type: ignore
|
||||
class ShowSlideNumber(Str, StrEnum): # type: ignore
|
||||
all = "all"
|
||||
print = "print"
|
||||
speaker = "speaker"
|
||||
|
||||
|
||||
class KeyboardCondition(Str, Enum): # type: ignore
|
||||
class KeyboardCondition(Str, StrEnum): # type: ignore
|
||||
null = "null"
|
||||
focused = "focused"
|
||||
|
||||
|
||||
class NavigationMode(Str, Enum): # type: ignore
|
||||
class NavigationMode(Str, StrEnum): # type: ignore
|
||||
default = "default"
|
||||
linear = "linear"
|
||||
grid = "grid"
|
||||
|
||||
|
||||
class AutoPlayMedia(Str, Enum): # type: ignore
|
||||
class AutoPlayMedia(Str, StrEnum): # type: ignore
|
||||
null = "null"
|
||||
true = "true"
|
||||
false = "false"
|
||||
@ -176,25 +216,25 @@ class AutoPlayMedia(Str, Enum): # type: ignore
|
||||
PreloadIframes = AutoPlayMedia
|
||||
|
||||
|
||||
class AutoAnimateMatcher(Str, Enum): # type: ignore
|
||||
class AutoAnimateMatcher(Str, StrEnum): # type: ignore
|
||||
null = "null"
|
||||
|
||||
|
||||
class AutoAnimateEasing(Str, Enum): # type: ignore
|
||||
class AutoAnimateEasing(Str, StrEnum): # type: ignore
|
||||
ease = "ease"
|
||||
|
||||
|
||||
AutoSlide = Union[PositiveInt, JsFalse]
|
||||
|
||||
|
||||
class AutoSlideMethod(Str, Enum): # type: ignore
|
||||
class AutoSlideMethod(Str, StrEnum): # type: ignore
|
||||
null = "null"
|
||||
|
||||
|
||||
MouseWheel = Union[JsNull, float]
|
||||
|
||||
|
||||
class Transition(Str, Enum): # type: ignore
|
||||
class Transition(Str, StrEnum): # type: ignore
|
||||
none = "none"
|
||||
fade = "fade"
|
||||
slide = "slide"
|
||||
@ -203,13 +243,13 @@ class Transition(Str, Enum): # type: ignore
|
||||
zoom = "zoom"
|
||||
|
||||
|
||||
class TransitionSpeed(Str, Enum): # type: ignore
|
||||
class TransitionSpeed(Str, StrEnum): # type: ignore
|
||||
default = "default"
|
||||
fast = "fast"
|
||||
slow = "slow"
|
||||
|
||||
|
||||
class BackgroundSize(Str, Enum): # type: ignore
|
||||
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"
|
||||
@ -219,11 +259,11 @@ class BackgroundSize(Str, Enum): # type: ignore
|
||||
BackgroundTransition = Transition
|
||||
|
||||
|
||||
class Display(Str, Enum): # type: ignore
|
||||
class Display(Str, StrEnum): # type: ignore
|
||||
block = "block"
|
||||
|
||||
|
||||
class RevealTheme(str, Enum):
|
||||
class RevealTheme(str, StrEnum):
|
||||
black = "black"
|
||||
white = "white"
|
||||
league = "league"
|
||||
@ -238,6 +278,8 @@ class RevealTheme(str, Enum):
|
||||
|
||||
|
||||
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%")
|
||||
@ -326,10 +368,14 @@ class RevealJS(Converter):
|
||||
for presentation_config in self.presentation_configs:
|
||||
for slide_config in presentation_config.slides:
|
||||
file = presentation_config.files[slide_config.start_animation]
|
||||
file = assets_dir / file.name
|
||||
|
||||
logger.debug(f"Writing video section with file {file}")
|
||||
|
||||
if self.data_uri:
|
||||
file = data_uri(file)
|
||||
else:
|
||||
file = assets_dir / file.name
|
||||
|
||||
# TODO: document this
|
||||
# Videos are muted because, otherwise, the first slide never plays correctly.
|
||||
# This is due to a restriction in playing audio without the user doing anything.
|
||||
@ -356,27 +402,41 @@ class RevealJS(Converter):
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Converts this configuration into a RevealJS HTML presentation, saved to DEST."""
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
if self.data_uri:
|
||||
assets_dir = Path("") # Actually we won't care.
|
||||
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
for presentation_config in self.presentation_configs:
|
||||
presentation_config.concat_animations()
|
||||
else:
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
|
||||
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
|
||||
os.makedirs(full_assets_dir, exist_ok=True)
|
||||
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
||||
|
||||
for presentation_config in self.presentation_configs:
|
||||
presentation_config.concat_animations().copy_to(full_assets_dir)
|
||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for presentation_config in self.presentation_configs:
|
||||
presentation_config.concat_animations().copy_to(full_assets_dir)
|
||||
|
||||
with open(dest, "w") as f:
|
||||
sections = "".join(self.get_sections_iter(assets_dir))
|
||||
|
||||
revealjs_template = self.load_template()
|
||||
content = revealjs_template.format(sections=sections, **self.dict())
|
||||
|
||||
if self.data_uri:
|
||||
data_uri_fix = DATA_URI_FIX
|
||||
else:
|
||||
data_uri_fix = ""
|
||||
|
||||
content = revealjs_template.format(
|
||||
sections=sections, data_uri_fix=data_uri_fix, **self.dict()
|
||||
)
|
||||
|
||||
f.write(content)
|
||||
|
||||
@ -496,6 +556,8 @@ class PowerPoint(Converter):
|
||||
):
|
||||
file = presentation_config.files[slide_config.start_animation]
|
||||
|
||||
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:
|
||||
@ -509,7 +571,7 @@ class PowerPoint(Converter):
|
||||
self.width * 9525,
|
||||
self.height * 9525,
|
||||
poster_frame_image=poster_frame_image,
|
||||
mime_type="video/mp4",
|
||||
mime_type=mime_type,
|
||||
)
|
||||
if self.auto_play_media:
|
||||
auto_play_media(movie, loop=slide_config.is_loop())
|
||||
|
@ -30,261 +30,260 @@
|
||||
|
||||
<!-- <script src="index.js"></script> -->
|
||||
<script>
|
||||
Reveal.initialize({{
|
||||
Reveal.initialize({{
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// Bounds for smallest/largest possible scale to apply to content
|
||||
minScale: {min_scale},
|
||||
maxScale: {max_scale},
|
||||
// Display presentation control arrows
|
||||
controls: {controls},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
||||
// or "visible"
|
||||
controlsBackArrows: {controls_back_arrows},
|
||||
// Display a presentation progress bar
|
||||
progress: {progress},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// Push each slide change to the browser history. Implies `hash: true`
|
||||
history: {history},
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: {keyboard},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// Enable the slide overview mode
|
||||
overview: {overview},
|
||||
// Vertical centering of slides
|
||||
center: {center},
|
||||
|
||||
// Vertical centering of slides
|
||||
center: {center},
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: {touch},
|
||||
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: {touch},
|
||||
// Loop the presentation
|
||||
loop: {loop},
|
||||
|
||||
// Loop the presentation
|
||||
loop: {loop},
|
||||
// Change the presentation direction to be RTL
|
||||
rtl: {rtl},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// Randomizes the order of slides each time the presentation loads
|
||||
shuffle: {shuffle},
|
||||
// Turns fragments on and off globally
|
||||
fragments: {fragments},
|
||||
|
||||
// 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 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 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 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 it should be possible to pause the presentation (blackout)
|
||||
pause: {pause},
|
||||
// Flags if speaker notes should be visible to all viewers
|
||||
showNotes: {show_notes},
|
||||
|
||||
// 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 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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 style
|
||||
transition: {transition}, // none/fade/slide/convex/concave/zoom
|
||||
// Transition speed
|
||||
transitionSpeed: {transition_speed}, // default/fast/slow
|
||||
|
||||
// Transition speed
|
||||
transitionSpeed: {transition_speed}, // default/fast/slow
|
||||
// Transition style for full page slide backgrounds
|
||||
backgroundTransition: {background_transition}, // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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},
|
||||
|
||||
// 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
|
||||
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},
|
||||
|
||||
// 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},
|
||||
|
||||
// The display mode that will be used to show slides
|
||||
display: {display},
|
||||
// Hide cursor if inactive
|
||||
hideInactiveCursor: {hide_inactive_cursor},
|
||||
|
||||
// Hide cursor if inactive
|
||||
hideInactiveCursor: {hide_inactive_cursor},
|
||||
// Time before the cursor is hidden (in ms)
|
||||
hideCursorTime: {hide_cursor_time}
|
||||
}});
|
||||
|
||||
// Time before the cursor is hidden (in ms)
|
||||
hideCursorTime: {hide_cursor_time}
|
||||
|
||||
|
||||
}});
|
||||
{data_uri_fix}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
@ -16,6 +16,17 @@ A directive for including Manim slides in a Sphinx document
|
||||
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
|
||||
-----
|
||||
|
||||
|
265
manim_slides/ipython/ipython_magic.py
Normal file
265
manim_slides/ipython/ipython_magic.py
Normal 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>`_.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
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(
|
||||
self,
|
||||
line: str,
|
||||
cell: Optional[str] = None,
|
||||
local_ns: Dict[str, Any] = {},
|
||||
) -> 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 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:
|
||||
SceneClass = local_ns[config["scene_names"][0]]
|
||||
scene = SceneClass(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
|
@ -40,6 +40,7 @@ def make_logger() -> logging.Logger:
|
||||
)
|
||||
logging.addLevelName(5, "PERF")
|
||||
logger = logging.getLogger("manim-slides")
|
||||
logger.setLevel(logging.getLogger("manim").level)
|
||||
logger.addHandler(rich_handler)
|
||||
|
||||
return logger
|
||||
|
@ -1,8 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from importlib.util import find_spec
|
||||
from typing import Iterator
|
||||
|
||||
__all__ = [
|
||||
# Constants
|
||||
@ -29,17 +26,6 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def suppress_stdout() -> Iterator[None]:
|
||||
with open(os.devnull, "w") as devnull:
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = devnull
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
|
||||
MANIM_PACKAGE_NAME = "manim"
|
||||
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
|
||||
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
|
||||
@ -89,20 +75,19 @@ if MANIMGL:
|
||||
from manimlib.logger import log as logger
|
||||
|
||||
else:
|
||||
with suppress_stdout(): # Avoids printing "Manim Community v..."
|
||||
from manim import (
|
||||
LEFT,
|
||||
AnimationGroup,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
Mobject,
|
||||
Scene,
|
||||
ThreeDScene,
|
||||
config,
|
||||
logger,
|
||||
)
|
||||
from manim import (
|
||||
LEFT,
|
||||
AnimationGroup,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
Mobject,
|
||||
Scene,
|
||||
ThreeDScene,
|
||||
config,
|
||||
logger,
|
||||
)
|
||||
|
||||
try: # For manim<v0.16.0.post0
|
||||
from manim.constants import FFMPEG_BIN
|
||||
except ImportError:
|
||||
FFMPEG_BIN = config.ffmpeg_executable
|
||||
try: # For manim<v0.16.0.post0
|
||||
from manim.constants import FFMPEG_BIN
|
||||
except ImportError:
|
||||
FFMPEG_BIN = config.ffmpeg_executable
|
||||
|
@ -137,7 +137,7 @@ page.
|
||||
|
||||
## Comparison with manim-presentation
|
||||
|
||||
Starting from [@manim-presentation]'s original work, Manim Slides now provides
|
||||
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:
|
||||
|
||||
@ -162,7 +162,7 @@ with the appropriate template.
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
We acknowledge the work of [@manim-presentation] that paved the initial structure
|
||||
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
|
||||
|
4868
poetry.lock
generated
4868
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -43,14 +43,18 @@ packages = [
|
||||
]
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/jeertmans/manim-slides"
|
||||
version = "4.15.0"
|
||||
version = "4.16.1"
|
||||
|
||||
[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 = {version = "^3.1.2", optional = true}
|
||||
lxml = "^4.9.2"
|
||||
manim = {version = "^0.17.3", optional = true}
|
||||
manimgl = {version = "^1.6.1", optional = true}
|
||||
notebook = {version = "^7.0.2", optional = true}
|
||||
numpy = "^1.19"
|
||||
opencv-python = "^4.6.0.66"
|
||||
pillow = "^9.5.0"
|
||||
@ -65,8 +69,13 @@ rtoml = "^0.9.0"
|
||||
tqdm = "^4.64.1"
|
||||
|
||||
[tool.poetry.extras]
|
||||
magic = ["manim", "ipython"]
|
||||
manim = ["manim"]
|
||||
manimgl = ["manimgl"]
|
||||
sphinx-directive = ["docutils", "jinja2", "manim"]
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^22.10.0"
|
||||
@ -76,20 +85,31 @@ mypy = "^0.991"
|
||||
pre-commit = "^3.0.2"
|
||||
ruff = "^0.0.219"
|
||||
|
||||
[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-xdist = "^3.3.1"
|
||||
|
||||
[tool.poetry.plugins]
|
||||
|
||||
@ -97,6 +117,9 @@ pytest-cov = "^4.1.0"
|
||||
manim-slides = "manim_slides.__main__:cli"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
env = [
|
||||
"QT_QPA_PLATFORM=offscreen"
|
||||
]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning"
|
||||
|
@ -1,6 +1,126 @@
|
||||
from enum import EnumMeta
|
||||
|
||||
import pytest
|
||||
|
||||
from manim_slides.convert import PDF, Converter, PowerPoint, RevealJS
|
||||
from manim_slides.convert import (
|
||||
PDF,
|
||||
AutoAnimateEasing,
|
||||
AutoAnimateMatcher,
|
||||
AutoPlayMedia,
|
||||
AutoSlideMethod,
|
||||
BackgroundSize,
|
||||
BackgroundTransition,
|
||||
ControlsBackArrows,
|
||||
ControlsLayout,
|
||||
Converter,
|
||||
Display,
|
||||
JsBool,
|
||||
JsFalse,
|
||||
JsNull,
|
||||
JsTrue,
|
||||
KeyboardCondition,
|
||||
NavigationMode,
|
||||
PowerPoint,
|
||||
PreloadIframes,
|
||||
RevealJS,
|
||||
RevealTheme,
|
||||
ShowSlideNumber,
|
||||
SlideNumber,
|
||||
Transition,
|
||||
TransitionSpeed,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("enum_type",),
|
||||
[
|
||||
(JsTrue,),
|
||||
(JsFalse,),
|
||||
(JsBool,),
|
||||
(JsNull,),
|
||||
(ControlsLayout,),
|
||||
(ControlsBackArrows,),
|
||||
(SlideNumber,),
|
||||
(ShowSlideNumber,),
|
||||
(KeyboardCondition,),
|
||||
(NavigationMode,),
|
||||
(AutoPlayMedia,),
|
||||
(PreloadIframes,),
|
||||
(AutoAnimateMatcher,),
|
||||
(AutoAnimateEasing,),
|
||||
(AutoSlideMethod,),
|
||||
(Transition,),
|
||||
(TransitionSpeed,),
|
||||
(BackgroundSize,),
|
||||
(BackgroundTransition,),
|
||||
(Display,),
|
||||
(RevealTheme,),
|
||||
],
|
||||
)
|
||||
def test_format_enum(enum_type: EnumMeta) -> None:
|
||||
for enum in enum_type: # type: ignore[var-annotated]
|
||||
expected = str(enum)
|
||||
got = f"{enum}"
|
||||
|
||||
assert expected == got
|
||||
|
||||
got = "{enum}".format(enum=enum)
|
||||
|
||||
assert expected == got
|
||||
|
||||
got = format(enum, "")
|
||||
|
||||
assert expected == got
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("enum_type",),
|
||||
[
|
||||
(ControlsLayout,),
|
||||
(ControlsBackArrows,),
|
||||
(SlideNumber,),
|
||||
(ShowSlideNumber,),
|
||||
(KeyboardCondition,),
|
||||
(NavigationMode,),
|
||||
(AutoPlayMedia,),
|
||||
(PreloadIframes,),
|
||||
(AutoAnimateMatcher,),
|
||||
(AutoAnimateEasing,),
|
||||
(AutoSlideMethod,),
|
||||
(Transition,),
|
||||
(TransitionSpeed,),
|
||||
(BackgroundSize,),
|
||||
(BackgroundTransition,),
|
||||
(Display,),
|
||||
],
|
||||
)
|
||||
def test_quoted_enum(enum_type: EnumMeta) -> None:
|
||||
for enum in enum_type: # type: ignore[var-annotated]
|
||||
if enum in ["true", "false", "null"]:
|
||||
continue
|
||||
|
||||
expected = "'" + enum.value + "'"
|
||||
got = str(enum)
|
||||
|
||||
assert expected == got
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("enum_type",),
|
||||
[
|
||||
(JsTrue,),
|
||||
(JsFalse,),
|
||||
(JsBool,),
|
||||
(JsNull,),
|
||||
(RevealTheme,),
|
||||
],
|
||||
)
|
||||
def test_unquoted_enum(enum_type: EnumMeta) -> None:
|
||||
for enum in enum_type: # type: ignore[var-annotated]
|
||||
expected = enum.value
|
||||
got = str(enum)
|
||||
|
||||
assert expected == got
|
||||
|
||||
|
||||
class TestConverter:
|
||||
|
Reference in New Issue
Block a user