feat(lib): add support for presenter notes (#322)

* feat(lib): add support for presenter notes

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

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

* fix(test): typo

* Update test_slide.py

* Update convert.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jérome Eertmans
2023-11-23 18:20:10 +01:00
committed by GitHub
parent b09a000c17
commit f898dd3054
7 changed files with 94 additions and 9 deletions

View File

@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the `playback-rate` and `reversed-playback-rate` options - Added the `playback-rate` and `reversed-playback-rate` options
to slide config. to slide config.
[#320](https://github.com/jeertmans/manim-slides/pull/320) [#320](https://github.com/jeertmans/manim-slides/pull/320)
- Added the speaker notes option.
[#322](https://github.com/jeertmans/manim-slides/pull/322)
(v5.1-modified)= (v5.1-modified)=
### Modified ### Modified

View File

@ -144,6 +144,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
auto_next: bool = False auto_next: bool = False
playback_rate: float = 1.0 playback_rate: float = 1.0
reversed_playback_rate: float = 1.0 reversed_playback_rate: float = 1.0
notes: str = ""
@classmethod @classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]: def wrapper(cls, arg_name: str) -> Callable[..., Any]:

View File

@ -406,9 +406,16 @@ class RevealJS(Converter):
options = self.dict() options = self.dict()
options["assets_dir"] = assets_dir options["assets_dir"] = assets_dir
has_notes = any(
slide_config.notes != ""
for presentation_config in self.presentation_configs
for slide_config in presentation_config.slides
)
content = revealjs_template.render( content = revealjs_template.render(
file_to_data_uri=file_to_data_uri, file_to_data_uri=file_to_data_uri,
get_duration_ms=get_duration_ms, get_duration_ms=get_duration_ms,
has_notes=has_notes,
**options, **options,
) )

View File

@ -5,7 +5,7 @@ from PySide6.QtCore import Qt, QUrl, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from PySide6.QtMultimedia import QMediaPlayer from PySide6.QtMultimedia import QMediaPlayer
from PySide6.QtMultimediaWidgets import QVideoWidget from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtWidgets import QDialog, QGridLayout, QLabel, QMainWindow from PySide6.QtWidgets import QDialog, QGridLayout, QLabel, QMainWindow, QVBoxLayout
from ..config import Config, PresentationConfig, SlideConfig from ..config import Config, PresentationConfig, SlideConfig
from ..logger import logger from ..logger import logger
@ -18,17 +18,25 @@ class Info(QDialog): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
layout = QGridLayout() main_layout = QVBoxLayout()
labels_layout = QGridLayout()
notes_layout = QVBoxLayout()
self.scene_label = QLabel() self.scene_label = QLabel()
self.slide_label = QLabel() self.slide_label = QLabel()
self.slide_notes = QLabel("")
self.slide_notes.setWordWrap(True)
layout.addWidget(QLabel("Scene:"), 1, 1) labels_layout.addWidget(QLabel("Scene:"), 1, 1)
layout.addWidget(QLabel("Slide:"), 2, 1) labels_layout.addWidget(QLabel("Slide:"), 2, 1)
layout.addWidget(self.scene_label, 1, 2) labels_layout.addWidget(self.scene_label, 1, 2)
layout.addWidget(self.slide_label, 2, 2) labels_layout.addWidget(self.slide_label, 2, 2)
self.setLayout(layout)
self.setFixedWidth(150) notes_layout.addWidget(self.slide_notes)
self.setFixedHeight(80)
main_layout.addLayout(labels_layout)
main_layout.addLayout(notes_layout)
self.setLayout(main_layout)
if parent := self.parent(): if parent := self.parent():
self.closeEvent = parent.closeEvent self.closeEvent = parent.closeEvent
@ -312,6 +320,7 @@ class Player(QMainWindow): # type: ignore[misc]
index = self.current_slide_index index = self.current_slide_index
count = self.current_slides_count count = self.current_slides_count
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}") self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.slide_notes.setText(self.current_slide_config.notes)
def show(self) -> None: def show(self) -> None:
super().show() super().show()

View File

@ -288,6 +288,11 @@ class BaseSlide:
Playback rate at which the reversed video is played. Playback rate at which the reversed video is played.
Note that this is only supported by ``manim-slides present``. Note that this is only supported by ``manim-slides present``.
:param notes:
Presenter notes, in HTML format.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
:param kwargs: :param kwargs:
Keyword arguments to be passed to Keyword arguments to be passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`, :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
@ -372,6 +377,29 @@ class BaseSlide:
self.next_slide() self.next_slide()
self.wipe(square) self.wipe(square)
The following contains speaker notes. On the webbrowser,
the speaker view can be triggered by pressing :kbd:`S`.
.. manim-slides:: SpeakerNotesExample
from manim import *
from manim_slides import Slide
class SpeakerNotesExample(Slide):
def construct(self):
self.next_slide(notes="Some introduction")
square = Square(color=GREEN, side_length=2)
self.play(GrowFromCenter(square))
self.next_slide(notes="We now rotate the slide")
self.play(Rotate(square, PI / 2))
self.next_slide(notes="Bye bye")
self.zoom(square)
""" """
if self._current_animation > self._start_animation: if self._current_animation > self._start_animation:
if self.wait_time_between_slides > 0.0: if self.wait_time_between_slides > 0.0:

View File

@ -40,6 +40,9 @@
{% if slide_config.auto_next -%} {% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}" data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}> {%- endif -%}>
{% if slide_config.notes != "" -%}
<aside class="notes">{{ slide_config.notes }}</aside>
{%- endif %}
</section> </section>
{%- endfor -%} {%- endfor -%}
{%- endfor -%} {%- endfor -%}
@ -50,9 +53,16 @@
<!-- To include plugins, see: https://revealjs.com/plugins/ --> <!-- To include plugins, see: https://revealjs.com/plugins/ -->
{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}
<!-- <script src="index.js"></script> --> <!-- <script src="index.js"></script> -->
<script> <script>
Reveal.initialize({ Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealNotes ],
{%- endif %}
// The "normal" size of the presentation, aspect ratio will // The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different // be preserved when the presentation is scaled to fit different
// resolutions. Can be specified using percentage units. // resolutions. Can be specified using percentage units.

View File

@ -196,6 +196,34 @@ class TestSlide:
assert self._base_slide_config.playback_rate == 2.0 assert self._base_slide_config.playback_rate == 2.0
@assert_constructs
class TestReversedPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert self._base_slide_config.reversed_playback_rate == 1.0
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
assert self._base_slide_config.reversed_playback_rate == 2.0
@assert_constructs
class TestNotes(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert self._base_slide_config.notes == ""
self.next_slide(notes="test")
self.play(text.animate.scale(2))
assert self._base_slide_config.notes == "test"
@assert_constructs @assert_constructs
class TestWipe(Slide): class TestWipe(Slide):
def construct(self) -> None: def construct(self) -> None: