Skip to content
Draft
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
384c44e
Add subsection support across exporters
krystophny Nov 7, 2025
d635e19
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2025
64a8e81
Remove PLAN.md per request
krystophny Nov 7, 2025
1dad0ed
fix: resolve pre-commit lint and type check failures
krystophny Nov 7, 2025
c88e79b
docs: add example to next_subsection docstring
krystophny Nov 7, 2025
e16a7aa
chore: trigger CI checks
krystophny Nov 7, 2025
bf587f4
fix: address Codacy security and complexity warnings
krystophny Nov 7, 2025
6d195f1
fix: add security annotations for Codacy static analyzer
krystophny Nov 7, 2025
3b5b7b5
fix: properly secure subprocess usage with comprehensive validation
krystophny Nov 7, 2025
3bdff00
fix: add Bandit security markers for subprocess usage
krystophny Nov 7, 2025
e9f8f05
fix: restore Click decorator chain for convert command
krystophny Nov 7, 2025
a142b81
feat: make subsections work by default and add SubsectionExample
krystophny Nov 9, 2025
29308c3
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
57aca8b
fix: capture final subsection state in PDF when subsection_mode is none
krystophny Nov 9, 2025
49b59a2
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
ff50ca9
refactor: unify subsection modes to none/all and set all as default
krystophny Nov 9, 2025
a87111f
refactor: unify PDF and PowerPoint subsection flags into single --sub…
krystophny Nov 9, 2025
dd7dc86
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
d941ae3
feat: unify subsection handling across all formats with single --subs…
krystophny Nov 9, 2025
d176105
docs: clarify SubsectionExample to better demonstrate accumulative be…
krystophny Nov 9, 2025
2d420e4
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
f532851
docs: correct SubsectionExample slide count to two slides
krystophny Nov 9, 2025
8599c3b
docs: clarify that subtitle appears before subsections in SubsectionE…
krystophny Nov 9, 2025
5ee7f70
docs: add comprehensive subsection usage documentation across all modes
krystophny Nov 9, 2025
672073b
refactor: simplify RevealJS subsection mode to match PDF/PowerPoint p…
krystophny Nov 9, 2025
daa69f9
refactor: simplify Qt presenter subsection modes to match PDF/PowerPoint
krystophny Nov 9, 2025
323f25e
refactor: RADICAL SIMPLIFICATION - HTML now creates separate sections…
krystophny Nov 9, 2025
58a0c76
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
24488ea
fix: CRITICAL - make subsections accumulate content correctly
krystophny Nov 9, 2025
33fc02a
fix: correct subsection marker placement in SubsectionExample
krystophny Nov 9, 2025
0ccf4df
fix: add separate subsection for subtitle and differentiate title fro…
krystophny Nov 9, 2025
7d3a008
fix: use self.clear() to properly clear screen between slides
krystophny Nov 9, 2025
0c40493
fix: add clear() after first slide and update title on second slide
krystophny Nov 9, 2025
2a6cb2d
fix: capture final video frame in PDF none mode instead of last subse…
krystophny Nov 9, 2025
4f6dd7a
fix: remove duplicate final frame from PDF subsections all mode
krystophny Nov 9, 2025
38cd73b
fix: add final frame after subsections in PDF all mode
krystophny Nov 9, 2025
665c39d
feat: add third slide with subsections to SubsectionExample for bette…
krystophny Nov 9, 2025
338418c
fix: PowerPoint tail fragment should show accumulated content from start
krystophny Nov 9, 2025
6e4c789
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
2c0a27d
style: improve slide titles with better font size and Write animation
krystophny Nov 9, 2025
3e816a6
fix: move dot and triangle closer to center on third slide
krystophny Nov 9, 2025
aa80df2
fix: PowerPoint subsections show final frame only, not full animation
krystophny Nov 9, 2025
e9200b9
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
fe9ca7f
fix: PowerPoint extracts full videos from 0 to end_time for each subs…
krystophny Nov 9, 2025
20c2fd0
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
471e82d
fix: use frame-accurate video cuts for PowerPoint subsections
krystophny Nov 9, 2025
8c99690
fix: PowerPoint shows single frame at each subsection end_time
krystophny Nov 9, 2025
fbfd4d1
fix: use last frame for PowerPoint poster image instead of first
krystophny Nov 9, 2025
6cf7ca7
fix: add cut transition to PowerPoint slides to eliminate black flashes
krystophny Nov 9, 2025
d11ae3a
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
743c93f
fix: extract 0.5s segments for PowerPoint to avoid black flashes
krystophny Nov 9, 2025
c5982cf
fix: extract full videos from 0 to end_time for PowerPoint subsections
krystophny Nov 9, 2025
c46e173
fix: extract subsections from start_time to end_time for PowerPoint
krystophny Nov 9, 2025
f6cd61a
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2025
c2edd67
Revert "fix: use last frame for PowerPoint poster image instead of fi…
krystophny Nov 9, 2025
2d354de
fix: add one frame duration to end_time to include final frame
krystophny Nov 9, 2025
dce2a47
fix: remove extra frame, use exact start_time to end_time
krystophny Nov 9, 2025
7bc5e99
fix: add epsilon to video extraction duration for inclusive end frame
krystophny Nov 9, 2025
9e80001
feat: place subsections as sequential videos on single PowerPoint slide
krystophny Nov 10, 2025
fd3244b
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
e1a457f
fix: remove video timing nodes for click-to-play subsections
krystophny Nov 10, 2025
244ca7b
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
594df2f
fix: use subsection start_time and add tail section in HTML converter
krystophny Nov 10, 2025
f6b592a
fix: prevent black screen when loading slides with subsections in Qt …
krystophny Nov 10, 2025
286e737
fix: extract video segments for HTML subsections with accurate timing
krystophny Nov 10, 2025
3ed6732
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
779426a
feat: use Manim's next_section for subsections
krystophny Nov 10, 2025
e6a48e7
refactor: use partial animation files directly for subsections
krystophny Nov 10, 2025
7407b26
fix: use subsection files in PDF converter
krystophny Nov 10, 2025
0eab92a
fix: work around Manim incomplete animation bug in PDF
krystophny Nov 10, 2025
4787b4e
fix: add wait after subsections to show completed animation
krystophny Nov 10, 2025
e859ec3
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
6487fe3
polish: improve subsection implementation comments and tests
krystophny Nov 10, 2025
5460b22
Handle PPTX subsections and prevent Qt fade
krystophny Nov 10, 2025
512cbef
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
5eb6093
fix: create vertical slides for subsections in HTML export
krystophny Nov 10, 2025
3000d23
fix: remove unrelated Qt presenter item from CHANGELOG
krystophny Nov 10, 2025
e00ae66
fix: only call wait() in next_subsection when animations exist
krystophny Nov 10, 2025
6057569
fix: Qt presenter subsection handling issues
krystophny Nov 10, 2025
0efcd0b
fix: extract tail video segment for HTML export
krystophny Nov 10, 2025
d67c5f9
chore(fmt): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
8ef2b83
fix: remove frame freeze from load_next_slide to prevent black frames
krystophny Nov 10, 2025
7c0b9a3
fix: add wait_time_between_slides to SubsectionExample for complete f…
krystophny Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it AI generated? I am asking because the "changed" section does not make much sense: we did not change the defaults, because most of them did not exist previously.

Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.5.2...HEAD)

(unreleased-added)=
### Added

- Introduced `Slide.next_subsection()` to record intra-slide checkpoints. New
presenter/convert flags (`--subsections`, `--html-subsections`,
`--pdf-subsections`, and `--pptx-subsections`) allow the Qt/HTML presenters,
PDF exporter, and PowerPoint converter to pause at those subsections or split
slides when explicitly requested.

(unreleased-changed)=
### Changed

- Changed defaults for subsection handling to make subsections work automatically:
- Qt presenter: `--subsections` changed from `off` to `pause`
- HTML/RevealJS: `--html-subsections` changed from `disabled` to `pause`
- PDF/PowerPoint: unified into single `--subsections` flag (default: `all`)
- Unified subsection handling across formats:
- Removed separate `--pdf-subsections` and `--pptx-subsections` flags
- Single `--subsections` flag now controls both PDF and PowerPoint exports
- Simplified modes to `none` and `all` (removed `final` and `split` as redundant)
- Consistent terminology across all export formats
- PowerPoint exports temporarily force `--subsections=none` and emit a warning when
`--subsections=all` is requested, since per-subsection slide splitting is not yet
implemented for PPTX.
- Qt presenter now freezes the last rendered frame when slides or subsections end
to avoid flashing to black between steps.
- Documented the RevealJS limitation where rewinding subsections replays their
clip instead of showing the final accumulated state immediately.
- Sort the scenes alphabetically when listing scenes
(e.g., when prompting for scenes with `manim-slides present`).
[@msaadsbr](https://github.com/msaadsbr) [#573](https://github.com/jeertmans/manim-slides/pull/573)
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ The documentation is available [online](https://eertmans.be/manim-slides/).

Call `self.next_slide()` every time you want to create a pause between
animations, and `self.next_slide(loop=True)` if you want the next slide to loop
over animations until the user presses continue:
over animations until the user presses continue.
Use `self.next_subsection()` when you need multiple presenter-controlled pause
points inside a single slide (similar to Beamer overlays). Subsections
automatically work in the Qt/HTML presenters and can be exported to PDF or
PowerPoint with additional flags:

```python
from manim import * # or: from manimlib import *
Expand Down
1 change: 1 addition & 0 deletions docs/source/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use, not the methods used internally when rendering.
canvas,
canvas_mobjects,
mobjects_without_canvas,
next_subsection,
next_section,
next_slide,
remove_from_canvas,
Expand Down
35 changes: 35 additions & 0 deletions docs/source/reference/examples.md
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another comment (I know it's still a draft): I don't really understand how this example motivates the use of subsection? In HTML, it appears that they are exactly the same. The "cleared" slide is not a feature from the section, but rather from using self.clear() before calling self.next_section. One thing I would expect from subsection is to create vertical slides when exported to HTML.

Another point to consider is that self.next_section inherits its names from Manim's sections, but it is a "smart" alias to self.next_slide (i.e., it also calls super().next_section()). We should make sure that user self.next_subsection does not interfere with Manim's sections.

Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,41 @@ directly write the `construct` method in the body of `MovingCameraSlide`.
self.wait()
```

## Subsection Example

Example demonstrating the use of subsections within a single slide.

**Key concept**: `next_subsection()` keeps building on existing content (accumulates),
while `next_slide()` starts fresh (clears the screen). This example shows a diagram
being built step by step, with each subsection adding more elements while keeping
everything from previous subsections visible.

```{eval-rst}
.. manim-slides:: ../../../example.py:SubsectionExample
:hide_source:
:quality: high

.. literalinclude:: ../../../example.py
:language: python
:linenos:
:pyobject: SubsectionExample
```

This example creates **two slides**:
- **Slide 1**: Title slide showing "Subsections Demo"
- **Slide 2**: Starts with title and subtitle, then has **four subsections** that build the diagram:
1. Subsection "Add circle": adds a circle
2. Subsection "Add square": adds a square (circle still visible)
3. Subsection "Add labels": adds labels (circle and square still visible)
4. Subsection "Add arrows": adds an arrow connecting them (everything still visible)

The key point: the subtitle appears **before** the first subsection, so it's part of the
base slide content. Each subsection then adds more elements while keeping all previous
content visible.

Note that `auto_next=True` on the third subsection will automatically advance
to the fourth subsection after animations complete.

## Advanced Example

A more advanced example is `ConvertExample`, which is used as demo slide and tutorial.
Expand Down
62 changes: 58 additions & 4 deletions docs/source/reference/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,40 @@ In the next sections, we will assume your animations are described
in `example.py`, and you have one presentation called `BasicExample`.
:::

## Understanding Subsections

Before sharing slides, it's important to understand **subsections**. Subsections
allow multiple pause points within a single slide, useful for step-by-step reveals:

- `next_subsection()`: **Accumulates** content (keeps building on what's there)
- `next_slide()`: **Clears** content (starts fresh)

All presentation modes support subsections with the `--subsections` flag:

- `--subsections=all` (default): Enable subsections (pause at each subsection/create separate pages)
- `--subsections=none`: Ignore subsections (play through/show final state only)

## With Manim Slides installed on the target machine

If Manim Slides, Manim (or ManimGL), and their dependencies are installed, then
using `manim-slides present` allows for the best presentations, with the most
options available.

### Controlling Subsection Behavior

When presenting with the Qt GUI, you can control how subsections behave:

```bash
# Default: pause at each subsection, press key to advance
manim-slides present BasicExample

# Explicit (same as default)
manim-slides present BasicExample --subsections=all

# Play through entire slide without pausing at subsections
manim-slides present BasicExample --subsections=none
```

### Sharing your Python file(s)

The lightest way to share your presentation is with the Python files that
Expand Down Expand Up @@ -85,9 +113,18 @@ First, you need to create the HTML file and its assets directory.
Example:

```bash
# Default: subsections create pause points (press arrow keys to advance)
manim-slides convert BasicExample basic_example.html

# Without subsections: play through entire slide
manim-slides convert BasicExample basic_example.html --subsections=none
```

**Limitation:** When you skim backwards through subsections in HTML/RevealJS,
each subsection clip restarts from its beginning. Unlike the Qt presenter, the
final accumulated state is not frozen when rewinding, so expect a short replay
when stepping back.

Then, you need to copy the HTML files and its assets directory to target location,
while keeping the relative path between the HTML and the assets the same. The
easiest solution is to compress both the file and the directory into one ZIP,
Expand Down Expand Up @@ -172,17 +209,26 @@ A convenient conversion feature is to the PowerPoint format, thanks to the
it is still considered in an *EXPERIMENTAL* status because we do not
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.

Basically, you can create a PowerPoint in a single command:
You can create a PowerPoint in a single command:

```bash
# PowerPoint currently exports one slide per manim slide (subsections disabled)
manim-slides convert --to=pptx BasicExample basic_example.pptx

# Explicitly silence the warning by disabling subsections yourself
manim-slides convert --to=pptx BasicExample basic_example.pptx --subsections=none
```

All the videos and necessary files will be contained inside the `.pptx` file, so
you can safely share it with anyone. By default, the `poster_frame_image`, i.e.,
what is displayed by PowerPoint when the video is not playing, is the first
frame of each slide. This allows for smooth transitions.

**Subsection handling:** PowerPoint export currently forces `--subsections=none`.
Passing `--subsections=all` logs a warning and behaves the same. The HTML/RevealJS
and PDF exporters still honor `--subsections=all`, so use those formats when you
need to step through subsections.

In the future, we hope to provide more features to this format,
so feel free to suggest new features too!

Expand All @@ -192,10 +238,18 @@ If you ever need backup slides, that are only made of PDF pages
with static images, you can generate such a PDF with the following command:

```bash
# Default: subsections become separate PDF pages
manim-slides convert --to=pdf BasicExample basic_example.pdf

# Without subsections: one page per manim slide, final state only
manim-slides convert --to=pdf BasicExample basic_example.pdf --subsections=none
```

Note that you will lose all the benefits from animated slides. Therefore,
this is only recommended to be used as a backup plan. By default, the last frame
of each slide will be printed. This can be changed to be the first one with
`-cframe_index=first`.
this is only recommended to be used as a backup plan.

**Subsection handling:** By default (`--subsections=all`), each subsection becomes
a separate PDF page, showing the progressive build-up. Use `--subsections=none`
to create one page per manim slide showing only the final state. The frame index
option (`-cframe_index=first` or `last`) controls which frame is captured when
subsections are not used.
46 changes: 46 additions & 0 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,52 @@ def construct(self):
self.play(Transform(square, learn_more_text))


class SubsectionExample(Slide):
def construct(self):
title = Text("Subsections Demo", color=YELLOW)
self.play(Write(title))
self.next_slide()

self.clear()
title = Text("Building a Diagram", font_size=36, color=YELLOW).to_edge(UP)
self.play(Write(title))
self.next_subsection(name="Show title")

circle = Circle(radius=1, color=BLUE).shift(LEFT * 2)
self.play(Create(circle))
self.next_subsection(name="Add circle")

square = Square(side_length=2, color=RED).shift(RIGHT * 2)
self.play(Create(square))
self.next_subsection(name="Add square")

circle_label = Text("Circle", font_size=20).next_to(circle, DOWN)
square_label = Text("Square", font_size=20).next_to(square, DOWN)
self.play(Write(circle_label), Write(square_label))
self.next_subsection(name="Add labels", auto_next=True)

arrow = Arrow(circle.get_right(), square.get_left(), buff=0.1, color=GREEN)
arrow_label = Text("Connection", font_size=16).next_to(arrow, UP)
self.play(Create(arrow), Write(arrow_label))
self.next_slide()

self.clear()
title = Text("Transformations", font_size=36, color=YELLOW).to_edge(UP)
self.play(Write(title))
self.next_subsection(name="Show title")

dot = Dot(color=ORANGE).shift(DOWN)
self.play(FadeIn(dot))
self.next_subsection(name="Add dot")

triangle = Triangle(color=PURPLE).shift(UP)
self.play(Create(triangle))
self.next_subsection(name="Add triangle")

self.play(dot.animate.move_to(triangle.get_center()))
self.next_slide()


# For ThreeDExample, things are different

if not MANIMGL:
Expand Down
55 changes: 53 additions & 2 deletions manim_slides/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import shutil
from collections.abc import Sequence
from functools import wraps
from inspect import Parameter, signature
from pathlib import Path
Expand All @@ -11,6 +12,7 @@
BaseModel,
Field,
FilePath,
NonNegativeInt,
PositiveInt,
PrivateAttr,
conset,
Expand Down Expand Up @@ -151,6 +153,40 @@ def merge_with(self, other: "Config") -> "Config":
return self


class SubsectionMarker(BaseModel): # type: ignore[misc]
"""User-authored subsection boundary recorded before rendering."""

animation_index: NonNegativeInt
name: str = ""
auto_next: bool = False


class SubsectionConfig(BaseModel): # type: ignore[misc]
"""Fully resolved subsection configuration stored after rendering."""

name: str = ""
auto_next: bool = False
start_animation: NonNegativeInt
end_animation: NonNegativeInt
start_time: float = Field(0.0, ge=0.0)
end_time: float = Field(0.0, ge=0.0)
file: Optional[FilePath] = Field(
None,
description="Path to the partial animation file for this subsection. "
"Only set when subsection contains exactly one animation.",
)

@model_validator(mode="after")
def animations_are_monotone(self) -> "SubsectionConfig":
if self.end_animation < self.start_animation:
raise ValueError(
"end_animation must be greater or equal to start_animation"
)
if self.end_time < self.start_time:
raise ValueError("end_time must be greater or equal to start_time")
return self


class BaseSlideConfig(BaseModel): # type: ignore
"""Base class for slide config."""

Expand Down Expand Up @@ -220,17 +256,21 @@ class PreSlideConfig(BaseSlideConfig):

start_animation: int
end_animation: int
subsection_markers: tuple[SubsectionMarker, ...] = Field(default_factory=tuple)

@classmethod
def from_base_slide_config_and_animation_indices(
cls,
base_slide_config: BaseSlideConfig,
start_animation: int,
end_animation: int,
*,
subsection_markers: Sequence[SubsectionMarker] = (),
) -> "PreSlideConfig":
return cls(
start_animation=start_animation,
end_animation=end_animation,
subsection_markers=tuple(subsection_markers),
**base_slide_config.model_dump(),
)

Expand Down Expand Up @@ -280,12 +320,23 @@ class SlideConfig(BaseSlideConfig):

file: FilePath
rev_file: FilePath
subsections: tuple[SubsectionConfig, ...] = Field(default_factory=tuple)

@classmethod
def from_pre_slide_config_and_files(
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
cls,
pre_slide_config: PreSlideConfig,
file: Path,
rev_file: Path,
*,
subsections: Sequence[SubsectionConfig] = (),
) -> "SlideConfig":
return cls(file=file, rev_file=rev_file, **pre_slide_config.model_dump())
return cls(
file=file,
rev_file=rev_file,
subsections=tuple(subsections),
**pre_slide_config.model_dump(exclude={"subsection_markers"}),
)


class PresentationConfig(BaseModel): # type: ignore[misc]
Expand Down
Loading
Loading