diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index dea4ca5..a3436d3 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -15,16 +15,8 @@ PDFCube selects the matching shader dialect after the context is current. | Windows | GTK UCRT64 | Build, CLI, MSI assembly and extraction smoke test | Manual installed-MSI rendering test | | macOS | GTK/Homebrew | Build, CLI, DMG assembly and signature verification | Manual installed-DMG rendering test | -KDE, GNOME, and other desktop environments are not separate renderer targets; -they are covered through GTK's X11 and Wayland backends. GPU and driver -combinations are only considered verified after they appear in this matrix. +KDE, GNOME, and other desktop environments use GTK's X11 and Wayland backends. -## Out of scope for the current release - -- OpenGL 2.x and OpenGL ES 2.x -- Vulkan, Metal, and Direct3D renderers -- Vendor-trusted Windows Authenticode signatures and Apple notarization - -Those trust integrations require project-owned signing identities. Automated -CI packages are still checked for complete assembly and basic execution, but -that does not replace rendering tests on installed native packages. +Native packages use GTK's platform integration and bundle the supporting runtime +libraries. Automated package checks cover assembly and basic execution; installed +packages receive the rendering smoke tests listed above. diff --git a/INSTALL b/INSTALL index b5c81f3..8063430 100644 --- a/INSTALL +++ b/INSTALL @@ -7,8 +7,10 @@ Requirements PDFCube requires a C++20 compiler, Meson 1.3 or newer, Ninja, GTK 4.12 or newer, Poppler GLib 22.0 or newer, Cairo, libepoxy, and Boost Program Options. The renderer supports desktop OpenGL and OpenGL ES 3 contexts. -Python 3, Xvfb, Xauth, and a software OpenGL driver such as Mesa DRI are -needed to run the complete rendering test suite on Linux. Windows builds use +Linux tests use Python 3, Pillow, EGL development headers, DejaVu fonts, +dbus-run-session, and a software OpenGL driver such as Mesa DRI. Transition +frames use surfaceless EGL; the GTK integration smoke test uses Xvfb and Xauth. +See tests/README.md for the test contract. Windows builds use the MSYS2 UCRT64 GTK, Poppler, libepoxy, and Boost packages. macOS builds use their Homebrew equivalents. diff --git a/README.md b/README.md index afb0849..bf335cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PDFCube -The [transition roadmap and review gallery](docs/transitions/README.md) describes -new effects, their validation gates, and how to generate videos and contact sheets. +The [transition design and gallery](docs/transitions/README.md) describes +the effects and how to generate videos and contact sheets. Ready-to-open [example PDFs](examples/transitions/README.md) are included. A self-contained [LaTeX/Beamer showcase](examples/showcase/README.md) covers all standard PDF transitions with vector artwork and next-effect labels. @@ -14,8 +14,8 @@ compressing slide blocks in all four PDF directions. Dissolve fractures the outgoing slide into deterministic glass shards that clear to reveal the next page. Glitter releases directional fragments with brief warm sparks. Wipe sweeps a 3D wiper with a bowed spine, pressure brackets and subtle flex across the slide in all four cardinal directions, revealing -the next page without stretching either image. Fade remains the next planned -effect; Replace is instant. +the next page with each image fitted to its own aspect ratio. Fade exchanges +light between stationary slides with gently eased timing; Replace is instant. [![CI](https://github.com/baol/pdfcube/actions/workflows/ci.yml/badge.svg)](https://github.com/baol/pdfcube/actions/workflows/ci.yml) @@ -24,10 +24,8 @@ transitions, programmed slide timing, and keyboard-controlled zoom regions. It is especially useful for slide decks produced by LaTeX Beamer and office suites. -The current PDFCube release is a complete modernization of the original -application. It uses C++20, GTK 4 `GtkGLArea`, shader-based OpenGL/OpenGL ES 3, -libepoxy, Poppler GLib, Cairo, Boost Program Options, and Meson. The obsolete -GtkGLExt, fixed-function OpenGL, and Autotools implementation has been removed. +PDFCube uses C++20, GTK 4 `GtkGLArea`, shader-based OpenGL/OpenGL ES 3, +libepoxy, Poppler GLib, Cairo, Boost Program Options, and Meson. @@ -57,8 +55,8 @@ sudo apt install python3 python3-pil dbus-daemon libegl-dev libgl1-mesa-dri font ``` Frame tests render explicit progress values in one headless context per case; -they do not wait through animation durations. Every test has a 500 ms -timing SLO, checked before pushing, including cold graphics setup. Timing misses are +synthetic progress makes each capture independent of playback duration. +Every test has a 500 ms timing SLO, checked before pushing, including cold graphics setup. Timing misses are reported separately from functional failures. See [the test contract](tests/README.md). The optional review gallery additionally requires Xvfb, Xauth, and ffmpeg. @@ -78,8 +76,7 @@ pacman -S --needed mingw-w64-ucrt-x86_64-{toolchain,meson,ninja,python,gtk4,libe The renderer selects GLSL 1.30 or GLSL ES 3.00 to match the context supplied by GTK. Linux is tested on X11 and Wayland; CI also compiles and exercises the command-line interface on Windows and macOS. Actual GPU/driver combinations -are tracked in [COMPATIBILITY.md](COMPATIBILITY.md) rather than assumed to work -untested. +are tracked in [COMPATIBILITY.md](COMPATIBILITY.md) with their validation status. ## Build and install @@ -93,9 +90,9 @@ sudo meson install -C build Build the Beamer showcase explicitly with `meson compile -C build doc`. This documentation target requires TeX Live with Beamer and PGF/TikZ; see the [showcase build instructions](examples/showcase/README.md). -The default application build does not build documentation. +The `doc` target builds documentation separately from the default application target. -For a local build without installation: +Run directly from a local build: ```sh ./build/src/pdfcube presentation.pdf @@ -116,7 +113,7 @@ options remain available: - `--program n:x:...` programs durations and transition types, including cube (`c`), plain (`p`), and the documented PDF effects. - `--performance-log timings.csv` buffers animation frame timings and writes - them on exit, without file I/O during motion. + them on exit, keeping file I/O outside active animation. - `--no-fullscreen` starts in a window. When no PDF is supplied, PDFCube opens a GTK file chooser. Options can also @@ -135,20 +132,19 @@ See `man pdfcube` for the full list. ### Frame pacing -PDFCube follows GTK's compositor-synchronized frame clock rather than imposing -a fixed frame rate. Full-resolution previous and next pages are rendered in the +PDFCube follows GTK's compositor-synchronized frame clock. Full-resolution previous and next pages are rendered in the background and uploaded to the GPU while the current slide is idle. If a page is not ready when navigation is requested, the transition remains at its exact -first frame until preparation finishes; PDF rendering and texture upload do not -consume the active animation clock. +first frame until preparation finishes. The animation clock starts once both +full-resolution textures are ready. Use `--performance-log timings.csv` with a representative presentation. The CSV records displayed-frame intervals, CPU submission time, and whether a frame was the stationary preparation frame. At 60 Hz, animation-frame intervals should remain close to 16,667 microseconds; at 120 Hz, close to 8,333. CPU -submission does not include asynchronous GPU completion, but interval spikes -expose visible compositor or rendering stalls. Slide texture resolution is never -reduced to meet a frame budget. +submission measures CPU work; GPU completion is asynchronous. Interval spikes +help identify compositor or rendering stalls. Slides retain full texture resolution +throughout the animation. ## Tests @@ -157,10 +153,13 @@ meson test -C build --print-errorlogs ``` The suite checks the public command-line contract and produces deterministic -headless rendering snapshots under Xvfb. It covers page rendering, cube and +headless rendering snapshots in surfaceless EGL contexts, plus one GTK +integration snapshot under Xvfb. It covers page rendering, cube and plain programs, configured colors, and command-line-over-config precedence. `--snapshot` and `--snapshot-progress` are private regression-test hooks. +See the [documentation and usability roadmap](docs/ROADMAP.md) for planned improvements. + ## Release and packaging Meson can produce a tested source archive with: diff --git a/RELEASING.md b/RELEASING.md index e96f868..5758339 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -50,14 +50,10 @@ Linux, macOS, and Windows packages build independently. The publish job waits for all three platforms, verifies every expected artifact, and only then creates the automatic tag and GitHub Release. Pull requests build and smoke-test the DMG and MSI as downloadable workflow artifacts, preventing release-only failures. -The current automated packages are ad-hoc signed on macOS and unsigned on -Windows; notarization and Authenticode signing require project-owned signing -identities and should be added before presenting either package as trusted by -the operating-system vendor. The release build explicitly enables `-Dinstall_docs=true` and invokes the Meson `doc` target before staging the AppImage. Debian's build rules enable -that option and invoke `doc` independently, so packages do not depend on a PDF -left over from another build. The standalone PDF is copied into `release-assets` +that option and invoke `doc` independently, producing the PDF during each package +build. The standalone PDF is copied into `release-assets` before checksums are generated. Missing documentation fails the package or release checks. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..e1cd68e --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,115 @@ +# Documentation and usability roadmap + +This review covers the README, manual, install instructions, compatibility guide, +transition design, diagnostic examples, Beamer showcase, CLI help, and application +key handling. It compares their descriptions with the current source code. + +## 1. Give readers a clear first-run path + +The README opens with a long catalogue of rendering details; installation and +controls come later. Release binaries are described in the packaging section, +while the opening path assumes a source build. + +Lead with a short product description, the showcase, a link to release downloads, +and one command to open a PDF. Follow with a compact controls table and links to +source builds, transition design, and maintainer documentation. Keep the detailed +motion descriptions in the transition document. + +Acceptance: a new user can find a suitable download, open a deck, advance, go +back, toggle automatic playback, and quit from the first screen of instructions. +Check the path on Linux, Windows, and macOS. + +## 2. Make the feature and control reference consistent + +Fade reached the renderer before the README, showcase labels, manual, and CLI +help were updated. The numeric-page formula in the manual used an internal +zero-based index. Test dependencies also differed between README and INSTALL. +The accompanying documentation edits align these descriptions; CLI help still +needs Fade added to its effect list. + +Create a small authoritative effect catalogue for names, program tokens, PDF +mapping, and applicable parameters. Use it for CLI help and documentation checks. +Keep one user-facing controls reference linked from the README and showcase. +Describe `/Dur`, `/Trans /D`, programmed dwell, and automatic playback together, +with one worked example. Include `t` in the README's common controls. + +Acceptance: every implemented effect appears in help, the manual, diagnostic +examples, and showcase with matching names. Examples match the parser. Page +shortcuts use visible page numbers. Validate local links and the generated man +page as part of documentation checks. + +## 3. Complete and validate `--program` + +The current implementation selects every implemented effect, including Fade, +through `duration:type` pairs. It repeats the final pair and applies each effect +to the following slide, including wraparound. Animation duration is fixed at +750 ms; axis, direction, and other effect parameters come from the PDF. + +The remaining work is input validation and a complete, documented contract. +Unknown effect names currently select an instant change. Duration parsing uses +`std::stod` with a conversion to integer milliseconds, accepting numeric prefixes +and leaving finite-value and range checks implicit. Incomplete pairs, empty +fields, and entries beyond the page count also need explicit handling. CLI help +needs to list Fade. + +Define the grammar and validate the entire program before applying it. Require +complete pairs, recognized tokens, and finite durations representable in the +scheduler's millisecond range. Return actionable errors identifying the offending +pair. Define zero-duration behavior, the final-pair repetition rule, wraparound, +and precedence over PDF timing. Preserve the existing `c` and `p` tokens. +Use the shared effect catalogue from priority 2 for program names and help. + +Assess whether full programming should also expose animation duration, direction, +axis, and Fly parameters. Record the chosen scope before extending the syntax; +keep existing programs compatible and document PDF parameter inheritance. + +Acceptance: parser and playback tests cover every effect token, fractional and +zero dwell times, repeated final pairs, wraparound, one-page decks, PDF overrides, +and CLI/config precedence. Invalid-input cases cover unknown tokens, empty or +incomplete pairs, trailing garbage, negative/nonfinite/overflowing durations, and +extra pairs. Validate every input pair even when the deck is shorter than the +program. Tests assert deterministic errors and scheduled delays using injected +time and a recording scheduler. + +## 4. Add help at the moment of presentation + +Application key handling exposes many commands, while discovering them requires +leaving the presentation to read the manual. Automatic playback toggles with `t` +but provides no explicit mode indicator in the current key-handling path. +File and renderer errors are printed to standard error, which makes failures +hard to discover when launching from the desktop. + +Add a keyboard-help overlay opened by `?` or F1. Group controls into navigation, +zoom, playback, and exit. Show the current page and automatic-playback state in +a short-lived status overlay when those states change. Show file and graphics +errors in an actionable dialog with a way to choose another PDF or exit. Keep focus and keyboard +behavior predictable when overlays open and close. + +Acceptance: help is readable over light and dark slides, works in fullscreen +and windowed modes, and can be dismissed by keyboard. Page numbers match the +manual. Desktop-launched failures produce a visible explanation. Overlay +visibility follows injected time and scheduler tests; exported +frames remain tied to the presentation content. + +## 5. Keep the presentation and documentation current + +The transition document accumulated implementation history, profiling results, +approval labels, and product descriptions in one place. The accompanying rewrite +makes it a reference for current behavior. Historical measurements belong in +commit/PR records; future changes belong here. + +Use the Beamer showcase as the main visual introduction and diagnostic PDFs for +parameter inspection. Add a short documentation checklist to feature changes: +help, manual, examples, showcase labels, and transition design. Generate or +validate repeated feature lists and check local links. Keep platform evidence in +the compatibility guide and test procedures in the test contract. + +Acceptance: product docs describe current behavior in positive terms; future +work is collected in the roadmap. Showcase labels describe the rendered effect, +and generated media identifies its source revision. A reviewer can trace each +example to its generator and reproduce its output. + +Implement in this order: first-run documentation, reference consistency, +`--program` completion, presentation help, then automated documentation checks. +Program validation and presentation help are separate application changes; the +documentation and consistency work can ship in small increments. diff --git a/docs/transitions/README.md b/docs/transitions/README.md index 7624820..6fd5aba 100644 --- a/docs/transitions/README.md +++ b/docs/transitions/README.md @@ -1,396 +1,171 @@ -# Transition roadmap and review gallery - -Each effect starts from freshly fetched origin/master on its own branch. -Rebase before handoff. Effects advance from experimental, through visual -review, to accepted; a passing build is not visual approval. - -| Order | Effect | Interpretation | Acceptance focus | -| --- | --- | --- | --- | -| 1 | Blinds | Twelve rotating louvers, cascading across the page | Both axes, readable reverse faces, seamless endpoints | -| 2 | Split | Perspective double doors on fixed outer hinges | Both axes, opening/closing, seamless endpoints | -| 3 | Cover / Uncover | Flexible page flips | Curvature, correct front/back mapping | -| 4 | Fly | A page drifting like a leaf on a controlled gust | Direction, scale, readable flight, controlled landing | -| 5 | Push | Cube pushes a soft cube | Deterministic contact and recovery | -| 6 | Dissolve / Glitter | Glass shatter / flare and sparks | Stable geometry, bounded memory and brightness | -| 7 | Wipe | Raised physical wiper sweeping the outgoing image away | Four directions, exact reveal mask and endpoints | -| 8 | Fade / Replace | Crossfade (next) / instant (already available) | Clean endpoints | - -Box keeps the classic cube. Unsupported PDF effects currently replace the page. -Future effects must preserve document ratio, prepare full-resolution source -images before motion, and finish on the exact static destination image. -Geometry is a function of progress, never an uncontrolled physics simulation. - -## Wipe: experimental, awaiting visual approval - -A three-dimensional wiper sweeps straight across the viewport, clearing the outgoing -page and revealing the destination behind it. A rubber contact lip, bowed pressure -spine, raised brackets and metal pivot give it a mechanical silhouette. Directional -lighting shows the depth of each part. A small damped flex settles during the sweep. Beveled matte surfaces and a -soft contact shadow keep the blade grounded against the page. The pages stay flat, readable -and at full resolution. Travel eases in and out and extends beyond both edges, -so the blade enters and leaves completely. There is no simulation or accumulating -state, and no per-frame geometry allocation. - -PDF `/Di` values 0, 90, 180 and 270 select right, up, left and down. Missing or -invalid angles default to right. `--program 4:wipe` uses the same effect and the -normal 750 ms programmed duration; document playback uses PDF `/Trans /D`, with -`/Dur` reserved for dwell time. Explicit cube and instant navigation are unchanged. - -Each page keeps its own fitted aspect ratio. The unrevealed region includes the -outgoing letterbox, so a differently shaped incoming page cannot show through -early. The clipping edge is pixel-aligned and concealed by the blade. At 0% and -100% the renderer bypasses the wiper and draws the exact static page. - -Try the [right](../../examples/transitions/wipe-right.pdf), -[up](../../examples/transitions/wipe-up.pdf), [left](../../examples/transitions/wipe-left.pdf) -and [down](../../examples/transitions/wipe-down.pdf) diagnostic PDFs, or the -[Beamer showcase](../../examples/showcase/README.md). Candidate contact sheets: - -![Wipe right](wipe-right.png) - -![Wipe up](wipe-up.png) - -Other directions: [left](wipe-left.png), [down](wipe-down.png). -These are software-renderer candidates, not visual acceptance or hardware -performance measurements. Fade is the next unimplemented roadmap effect; -Replace already changes the page instantly. - -Local Wipe captures were inspected with GTK 4.22.4, Poppler 26.01 and software -OpenGL, including a midpoint from the updated Beamer deck. All 26 Wipe cases -pass with address/undefined-behavior sanitizers. In the optimized-blade run, 12 sanitizer cases -missed the 500 ms SLO (519–661 ms); this is recorded separately from -functional correctness and does not establish a hardware performance pass. - -### Wipe frame profiling - -A local 1920×1080 headless run on the RTX 3060, with `glFinish` after each -frame, measured 180 frames at explicit progress values after 20 warmup frames. -The debug build changed as follows; these measurements exclude the compositor: - -| Renderer | Median before → after | p95 before → after | +# Transition design + +PDFCube interprets PDF transitions as readable motion between complete slides. +Each page keeps its own fitted aspect ratio. Both full-resolution page textures +are prepared before the animation clock starts. At 0% and 100%, the renderer +uses the exact static source and destination page paths. + +Geometry and shading are deterministic functions of progress. This makes +interactive playback, explicit-progress captures, and video export reproducible. +The camera stays fixed during page effects; the classic cube and zoom controls +have their own camera motion. + +## Effects + +| PDF effect | PDFCube interpretation | Parameters | +| --- | --- | --- | +| Box | Classic cube rotation | Cube navigation also selects forward or backward rotation | +| Blinds | Twelve rotating louvers with a cascading start | Horizontal or vertical | +| Split | Two perspective doors on fixed outer hinges | Horizontal or vertical; inward or outward | +| Cover / Uncover | A curved incoming sheet folds over the page, or the outgoing sheet peels away | Four cardinal directions | +| Fly | A light sheet follows a shallow arc, banks, flutters, and lands flat | Arbitrary angle; inward or outward; starting scale | +| Push | Two shallow padded blocks meet, compress, and recover | Four cardinal directions | +| Dissolve | An irregular glass panel fractures into outward-tumbling pieces | Optional thin glass edges with `--experimental` | +| Glitter | A directional front releases fragments and warm sparks | Right, down, or diagonally down-right | +| Wipe | A raised mechanical wiper clears the outgoing slide | Four cardinal directions | +| Fade | Two stationary slides exchange light | Uniform linear-light blend with eased timing | +| Replace | Instant page change | Also used for unrecognized transition types | + +Ready-to-open [diagnostic PDFs](../../examples/transitions/README.md) cover the +parameter combinations. The [Beamer showcase](../../examples/showcase/README.md) +presents the effects with vector artwork and labels naming the next transition. + +## Playback and PDF metadata + +Space and Page Down use the destination page's PDF transition. Arrow keys +select instant navigation; `c` and `a` select forward and backward cube motion. + +PDF `/Trans /D` controls animation duration, including fractional seconds. +A nonpositive animation duration selects an instant change. PDF `/Dur` controls +slide dwell time during automatic playback; a positive dwell duration schedules +advancement after the slide settles. + +`--program 4:fade` sets a four-second dwell followed by Fade. Programmed effects +use a 750 ms animation and the destination page's applicable PDF parameters. +The final duration/effect pair repeats for the remaining slides. `c` selects +the classic cube and `p` selects an instant change. + +| PDF `/Di` | Travel | +| --- | --- | +| 0 | Right | +| 90 | Up | +| 180 | Left | +| 270 | Down | + +Cover, Uncover, Push, and Wipe use these four directions and default to right +for missing or other angles. Glitter uses 0, 270, and 315 degrees and defaults +to right. Fly supports arbitrary angles. + +## Surface and motion design + +### Blinds and Split + +Blinds divides the page into twelve thin louvers. The outgoing image appears +on the front and the incoming image on the reverse with correctly oriented +lettering. A cascading start and restrained angular shade give the motion depth. + +Split uses two thin doors clipped to the page. `/Dm /V` selects left/right +doors and `/Dm /H` selects top/bottom flaps. `/M /O` opens the outgoing page; +`/M /I` closes the incoming page. The outer hinges stay fixed and the open +position is edge-on. PDF defaults are horizontal and inward. + +### Cover and Uncover + +A connected sheet uses 64 joined strips with constant arc length to describe +its curve. Cover folds the incoming sheet over the outgoing page; Uncover peels +the outgoing sheet away. The sheet stops edge-on, with restrained angular shading. + +| Travel | Cover hinge | Uncover hinge | | --- | --- | --- | -| NVIDIA RTX 3060 | 0.65 → 0.52 ms | 0.83 → 0.80 ms | -| llvmpipe software | 10.4 → 8.1 ms | 14.1 → 12.0 ms | - -The incoming page is now clipped to its revealed region, avoiding hidden-page -fragment work. Lighting is evaluated once per quad rather than once per vertex; -the optimized mesh microbenchmark improved from 0.092 to 0.055 ms per update. -A second GPU comparison measured 0.68 → 0.63 ms median but 0.90 → 1.65 ms -p95, so these samples do not establish a consistent GPU tail-latency improvement. -Cold PDF rasterization/upload remains a separate cost (190–249 ms on the GPU -runs); warmed frame throughput does not establish that first-frame latency is solved. -The RSS replay check disables ASan's freed-block quarantine only for that check, -so retained freed driver buffers do not masquerade as live memory growth. Other -sanitizer frame cases retain their ordinary quarantine. - -## Glitter: experimental, awaiting visual approval - -A directional front releases small textured pieces of the outgoing slide, -revealing the destination beneath. Pieces rotate, drift and fade, with small -warm sparks highlighting release and fading during flight. Alpha blending -bounds brightness; the effect uses at most 336 pieces. Motion is deterministic. -The first and last frames use the exact static page rendering path. - -PDF `/Di` values 0, 270 and 315 select right, down and diagonal down-right. -Missing or invalid directions use right. `--program 4:glitter` selects the same -effect. Try the [right](../../examples/transitions/glitter-right.pdf), -[down](../../examples/transitions/glitter-down.pdf) and -[diagonal](../../examples/transitions/glitter-down-right.pdf) examples. -Rendering tests cover all directions and page ratios, including exact endpoint -images. No rendered gallery or visual approval is claimed until those checks -can run on a machine with GTK and OpenGL. - -## Blinds: experimental, awaiting visual approval - -Try the committed [horizontal](../../examples/transitions/blinds-horizontal.pdf) -and [vertical](../../examples/transitions/blinds-vertical.pdf) PDFs in PDFCube; -press Space to advance. [Example instructions](../../examples/transitions/README.md) -include the regeneration command. - -Candidate captures from the software renderer, at 0%, 25%, 50%, 75%, and 100%: - -![Horizontal rotating slats](blinds-horizontal.png) - -![Vertical rotating slats](blinds-vertical.png) - -Horizontal and vertical PDF Blinds are interpreted as a cascade of twelve -rotating slats. The old page appears on the front; the new page appears on the -reverse without mirrored lettering. A restrained angular shade suggests depth. -This first version uses thin surfaces, without cast shadows or physical thickness. -Different page ratios retain independent fitting; visible background between -slats is intentional. - -Space, Page Down, and automatic advance read the destination page's PDF -transition. Explicit cube/instant keys keep their existing meaning. -PDF /Trans /D is animation duration (including fractions); /Dur is page dwell -time. Missing dwell time does not auto-advance. This corrects the previous -conflation of those fields. Nonpositive animation duration means instant change. -Existing --program duration:c and duration:p remain supported; duration:blinds -selects Blinds with the existing 750 ms programmed animation duration. - -## Split: experimental, awaiting visual approval - -Split uses two rigid, thin doors with perspective and restrained angular shading. -PDF `/Dm /V` selects left/right doors; `/Dm /H` selects top/bottom flaps. -`/M /O` opens the outgoing slide to reveal the destination; `/M /I` closes the -incoming slide over the old one. The outer edges are fixed hinges. Motion eases -in and out, ending edge-on when open, without mirrored text or a camera jump. -There is no physical thickness or cast shadow. Doors are clipped to their page; -different page ratios are independently fitted, never stretched. - -Try all four [committed example PDFs](../../examples/transitions/README.md). -These captures use the same generated PDFs as the tests and review videos: - -![Horizontal closing doors](split-horizontal-inward.png) - -![Horizontal opening doors](split-horizontal-outward.png) - -![Vertical closing doors](split-vertical-inward.png) - -![Vertical opening doors](split-vertical-outward.png) - -The PDF animation duration is respected. `--program duration:split` selects -Split with a 750 ms animation, using the destination PDF's axis and direction -(PDF defaults: horizontal, inward). Explicit cube and instant keys are unchanged. -Both full-resolution pages are prepared before the animation clock starts. -At 0% and 100% the renderer draws the exact static source/destination page. - -## Cover / Uncover: experimental, awaiting visual approval - -Cover folds a gently curved incoming sheet over the old slide. Uncover peels the -outgoing sheet away to reveal the next slide. Unlike Split's two rigid doors, -this is one connected sheet with a fixed outer edge, perspective and angular -shading. Sixty-four joined strips approximate the curve with constant arc length; -there is no physics simulation, cast shadow, thickness, or mirrored reverse face. -The sheet stops edge-on, rather than turning its back toward the audience. - -| PDF `/Di` | Travel | Cover hinge | Uncover hinge | -| --- | --- | --- | --- | -| 0 (default) | Right | Left | Right | -| 90 | Up | Bottom | Top | -| 180 | Left | Right | Left | -| 270 | Down | Top | Bottom | - -Unsupported angles fall back to 0. The destination PDF's `/Trans /D` controls -duration; `/Dur` remains dwell time. `--program duration:cover` and -`duration:uncover` select a 750 ms animation with the PDF direction. Existing -cube/instant navigation is unchanged. Full-resolution source and destination -pages are ready before motion, with their own aspect ratios and exact static -endpoints. Moving geometry stays inside its page viewport. - -All eight [example PDFs](../../examples/transitions/README.md) use the same -generator as the tests and gallery. Representative captures: - -![Cover traveling right](cover-right.png) - -![Uncover traveling left](uncover-left.png) - -Other Cover directions: [up](cover-up.png), [left](cover-left.png), -[down](cover-down.png). Other Uncover directions: [right](uncover-right.png), -[up](uncover-up.png), [down](uncover-down.png). -Each direction also has a video in the generated review bundle. - -## Fly: experimental, awaiting visual approval - -Fly treats the moving page as a light sheet caught by a controlled gust. It -follows a shallow 3D arc, banks into its path, adds one small deterministic -flutter, then progressively loses both motions and lands exactly flat. Incoming -(`/M /I`) pages travel from beyond the opposite edge in the PDF `/Di` direction; -outgoing (`/M /O`) pages leave in that direction. Arbitrary angles are supported, -including the eight compass directions used by the examples. +| Right | Left | Right | +| Up | Bottom | Top | +| Left | Right | Left | +| Down | Top | Bottom | + +### Fly + +The moving page follows a shallow 3D arc, banks into its path, and makes one +small deterministic flutter. Both motions settle as the page lands flat. +Incoming pages (`/M /I`) start beyond the opposite edge and move in the PDF +`/Di` direction; outgoing pages (`/M /O`) leave in that direction. + +`/SS` sets the off-screen scale. Valid scales are greater than zero and at most +one; other values use one. `/B` metadata is retained. Page textures include the +normal opaque white PDF background. The moving sheet can cross the letterbox +area while retaining its aspect ratio. + +### Push -`/SS` sets the off-screen start/end scale; valid values are greater than zero -and at most one, with invalid values conservatively treated as one. `/B` metadata -is retained. PDF pages are rendered against their normal opaque white page -background, so `/B` does not erase actual page artwork. The moving page may pass -through the letterbox area, but its aspect ratio never changes. Both pages are -prepared at full output resolution before the animation clock starts, and the -static endpoints bypass animated geometry. +The pages are shallow padded blocks whose contact plane travels in the PDF +direction. The outgoing block compresses by about 10% along the travel axis, +bulges by up to 3.5% across it, and makes one damped wobble. The incoming block +yields by up to 2.5%. Both lift slightly and recover fully before landing. +`--top-color` sets their side-face color as well as the classic cube caps. -The [eight example PDFs](../../examples/transitions/README.md) alternate inward -and outward motion while covering every 45-degree direction. Representative -captures: - -![Fly inward to the right](fly-in-right.png) - -![Fly outward toward the upper right](fly-out-up-right.png) - -The other six direction captures and all eight videos are in the same gallery. -`--program duration:fly` uses the destination PDF's direction, scale and motion -with the normal 750 ms programmed duration. Explicit cube and instant keys are -unchanged. - -## Push: experimental, awaiting visual approval - -Push treats the outgoing and incoming pages as two shallow padded blocks. Their -shared contact plane travels in the PDF `/Di` direction. At impact the softer -outgoing block squashes by about ten percent along the travel axis, bulges by up -to 3.5 percent across it, and makes one small damped wobble. The firmer incoming -block yields by at most 2.5 percent. Both lift slightly toward the viewer and -recover completely before the final frame. These are closed-form easing curves, -not a free-running simulation, so replay, reverse navigation and review captures -remain deterministic. - -PDF directions 0, 90, 180 and 270 select right, up, left and down. Missing or -unsupported angles use the PDF default of right. Both pages are rendered at -full output resolution before motion and retain their own fitted aspect ratios. -The 0% and 100% paths draw the exact static source and destination pages. -`--program duration:push` selects the effect with the destination PDF's -direction and the normal 750 ms programmed duration. +### Dissolve -The four committed examples use the same generator as the tests and gallery: +An irregular Voronoi partition divides the outgoing page into 64 interlocking +polygons, with a denser pattern near the central impact. Each polygon is a rigid +piece rendered as a triangle fan. The impact immediately separates and tilts the +pieces, with the strongest forward impulse at the center. -![Push right](push-right.png) +Pieces tumble around fixed axes tangent to their flight directions, each with +its own constant angular velocity. Their launch speeds rise continuously to +15% above the initial speed by the end. Perspective is compensated at the pivot +to preserve the speed profile. Continuous outward travel clears every piece +from the viewport by 99% progress. Both faces retain the outgoing page texture. -![Push up](push-up.png) - -Other directions: [left](push-left.png) and [down](push-down.png). All four -videos are included in the generated review bundle. `--top-color` controls the -shallow side faces as well as the classic cube caps. - -## Dissolve: experimental, awaiting visual approval - -Dissolve treats the outgoing page as a thin glass panel hit from behind at -its center. An irregular Voronoi partition divides the panel into 64 -interlocking polygonal pieces, slightly denser around the impact. Cracks do -not follow spokes or concentric rings. Each polygon remains one rigid piece -when the GPU draws its triangle fan. - -The impact cuts directly from the intact page to separated, tilted fragments; -there is no slow crack-opening stage. The blow is strongest at the center, -with a modest outward-speed taper and a stronger forward impulse toward the -viewer. Each piece retains its launch speed and then accelerates gently and -continuously, reaching 15% more speed by the end. Perspective is compensated -at the pivot so it does not add an unintended speed ramp. Every piece is -completely outside the viewport by 99%, without a late sweep, shrink, or fade. - -Each piece keeps a fixed tumbling axis tangent to its flight direction and a -constant individual angular velocity. Distinct angular speeds avoid a -synchronized edge-on flash. Restrained two-sided lighting makes the rotation -readable. Perspective gives the forward-moving splinters depth without a -late speed boost. There is no gravity or camera movement. Both faces retain -the outgoing slide texture. +The default uses thin surfaces. `--experimental` adds a pale rim and a second +textured face, about two pixels thick at a 960-pixel page width. Both variants +share the same trajectory and timing. The renderer uploads all shard vertices +in one dynamic buffer and draws them in one call. -Pass `--experimental` to give the pieces a very thin wafer volume with two -textured faces and a pale rim, roughly two pixels thick at a 960-pixel page -width. The default keeps zero-thickness surfaces. This flag does not change -the launch timing or velocity, and `--experimental=false` disables it explicitly. +### Glitter -The fracture and motion are closed-form functions of progress: there is no -runtime randomness or accumulating physics state. All shard vertices are sent -in one small dynamic GPU buffer and rendered in one draw call. Both page -textures are prepared at full output resolution before motion begins. The exact -0% and 100% frames bypass the fracture mesh and draw the static source and -destination pages without cracks, seams or reduced resolution. +A directional front releases up to 336 textured fragments from the outgoing +page. Pieces rotate, drift, and fade, accompanied by small warm sparks. +Alpha blending bounds brightness. The destination remains behind the pieces. -Try the committed [Dissolve example](../../examples/transitions/dissolve.pdf). -The image below records an earlier candidate; regenerate the review bundle -with the current renderer to review the redesigned motion: - -![Glass-shatter Dissolve](dissolve.png) - -`--program duration:dissolve` selects the effect with the normal 750 ms -programmed duration. PDF `/Trans /D` controls document-driven duration. - -## Reproduce the review bundle - -Install Pillow and ffmpeg in addition to the normal build/test dependencies: +### Wipe + +A rubber lip, bowed pressure spine, raised brackets, and metal pivot form a +three-dimensional wiper. Beveled matte surfaces, directional lighting, and a +soft contact shadow make it readable against the slide. One damped flex settles +during the straight sweep; eased travel carries the blade fully across both edges. + +The pixel-aligned clipping edge stays concealed by the blade. The outgoing +composition includes its letterbox, keeping the incoming page visible only in +the revealed region. Scissor regions limit drawing to each page's visible area. +The mesh uses reusable fixed-capacity storage, with lighting evaluated per quad. + +### Fade + +Fade decodes sRGB into linear light, blends both slide compositions, and encodes +the result back to sRGB. White artwork stays steady and the midpoint preserves +brightness. The weight is smoothstep (`3t² − 2t³`), giving a gentle start and finish. +Each composition includes its fitted page and surrounding background. + +## Render and inspect + +From a configured checkout with the build/test dependencies, Pillow, Xvfb, +Xauth, and FFmpeg installed: ```sh -meson setup build -Dwerror=true meson compile -C build meson test -C build --print-errorlogs python3 tools/transition_gallery.py build/src/pdfcube build/transition-gallery ``` -Add `--update-docs` to regenerate the checked-in contact sheets from those same -captures. The default 33 samples include exact quarter-progress frames. -Use `--effects Wipe` for a focused wiper review. The manual transition-review -workflow captures all implemented effects by default. Gallery capture reuses one Xvfb display and -captures up to four frames concurrently. Use `--jobs N` to tune that limit for -the available CPU and memory; this does not change the generated frames. - -Open build/transition-gallery/index.html. Each effect/axis/direction has a looping video, -five-frame contact sheet, full-resolution individual frames, and its input PDF. -The manifest records the source revision and binary version. Rebuild first; -captures must come from the revision under review. The manual transition-review -workflow uploads the bundle; ordinary CI keeps only one GUI smoke capture. Download and unzip it to view locally. - -Candidate captures are never automatically promoted to golden references. -The reviewer checks framing, direction, readability, motion and final alignment, -then records approval with the commit and platform. Subsequent effects use this -same process and add their own fixtures and gallery entry. - -## Validation record - -Blinds, Split, Cover/Uncover, Fly, Push and Dissolve: Ubuntu 24.04, GTK 4.14.5, -Poppler 24.02, Xvfb/software OpenGL. Tests pass with warnings treated as errors, -both normally and with address/undefined-behavior sanitizers. -The horizontal and vertical contact sheets were inspected for direction and -readable reverse faces. Page-flip captures were inspected for curvature, -direction and readable text; a Split midpoint still matched the previous -capture byte-for-byte. Fly contact sheets were inspected for direction, -readable text, restrained -flutter and a flat landing. This is not yet native-platform or motion approval. -Push captures were inspected for continuous contact, asymmetric squash, a -restrained settling wobble, direction and complete recovery. This is not yet -native-platform or motion approval. - -- Automated: PDF metadata (fractional animation duration distinct from dwell), - both axes, portrait/landscape/mixed dimensions, deterministic midpoint, - byte-identical start/end renders compared with static pages. -- Split: fixed hinges and edge-on finish, reversible monotonic geometry, - both directions, default PDF parameters, program selection, and reproducible - committed PDFs. The gallery includes all four Split parameter combinations. -- Existing cube, zoom, document resolution, CLI and navigation tests remain required. -- Cover/Uncover checks cover connected curvature, reversible geometry, fixed - hinges, unmirrored flat UV mapping, edge-on finish, all four PDF directions, - omitted/invalid angle fallback, program selection and reproducible example PDFs. -- Fly checks cover all eight compass directions, inward/outward reversibility, - bounded height and monotonic landing, `/SS` validation, PDF metadata, program - selection, deterministic rendering, exact endpoints and reproducible examples. -- Push checks cover all four PDF directions, default/invalid direction fallback, - monotonic contact, bounded squash/stretch/wobble, PDF metadata, program - selection, deterministic rendering, exact endpoints and reproducible examples. -- Dissolve checks deterministic fracture geometry, bounded shard parameters, - portrait/landscape/mixed ratios, PDF metadata, program selection, distinct - sampled stages, exact endpoints and a reproducible example. -- Wipe checks bounded and monotonic clipping, symmetric travel, all four directions, - omitted/invalid angle fallback, exact revealed/unrevealed pixel regions including - mixed page letterboxes, deterministic replay, GL state cleanup, PDF metadata, - program dispatch, complete frame sweeps, repeated-playback RSS bounds in one - shared context, and reproducible example PDFs. -- Pending manual approval: realistic Beamer deck; HD/2K/4K frame-time measurements; - native Wayland/NVIDIA, X11, Windows and macOS playback; long-running playback - memory checks. Still-frame and geometry checks are not substitutes for these. -- Captures use software OpenGL; they do not establish hardware performance. - -For every new effect, add parameter/default/invalid-value coverage, sampled -geometry and image checks, repeated-playback memory checks, and a review bundle. -Record renderer, resolution, duration and frame-time distribution for performance -reviews; do not infer a performance pass from still-image tests. - -## Fade: experimental, awaiting visual approval - -Fade exchanges light between two stationary slides. An sRGB decode, linear-light -blend, and sRGB encode keep white artwork steady and avoid a dark midpoint. -The blend weight is smoothstep (`3t² − 2t³`), with gentle starts and finishes. -Each slide retains its own fitted aspect ratio; the surrounding background is -part of each composition. Both full-resolution textures are ready before motion, -and endpoints use the exact static page path. - -PDF `/S /Fade` uses `/Trans /D` for animation duration. `--program 4:fade` uses -the normal 750 ms programmed animation. Direction and axis have no effect. -Try [fade.pdf](../../examples/transitions/fade.pdf). - -Candidate headless software-renderer captures (light, dark, and colored slides): - -![Fade review checkpoints](fade-review/contact-sheet.png) - -Animated previews: [light](fade-review/white.gif), [dark](fade-review/dark.gif), -[colored](fade-review/color.gif). These synthetic artwork pairs check shared -labels and brightness; photographic material still needs visual review. -Regenerate these previews with `python3 tools/fade_review.py` after building. -Tests compare rendered pixels against the sRGB transfer function at three -intermediate times, including mixed aspect ratios, and verify unchanged identical -slides, deterministic output, PDF metadata, CLI dispatch, and exact endpoints. +Open `build/transition-gallery/index.html` for looping videos, five-frame contact +sheets, individual frames, and input PDFs. The manifest records the source +revision and binary version. `--effects Wipe` selects one effect; `--jobs N` +controls concurrent captures. `--effects Fade` includes colored, light, and dark +slide pairs. The default 33 samples include exact quarter-progress frames. +Generated media is written to the chosen output directory under `build/`. + +The **Transition review gallery** workflow produces the same downloadable bundle +for a selected branch. + +The [test contract](../../tests/README.md) describes endpoint, aspect-ratio, +metadata, determinism, timing, and process-isolation coverage. The +[compatibility matrix](../../COMPATIBILITY.md) records platform coverage. diff --git a/docs/transitions/blinds-horizontal.png b/docs/transitions/blinds-horizontal.png deleted file mode 100644 index a303e5d..0000000 Binary files a/docs/transitions/blinds-horizontal.png and /dev/null differ diff --git a/docs/transitions/blinds-vertical.png b/docs/transitions/blinds-vertical.png deleted file mode 100644 index 8ec9c65..0000000 Binary files a/docs/transitions/blinds-vertical.png and /dev/null differ diff --git a/docs/transitions/cover-down.png b/docs/transitions/cover-down.png deleted file mode 100644 index 2d967b3..0000000 Binary files a/docs/transitions/cover-down.png and /dev/null differ diff --git a/docs/transitions/cover-left.png b/docs/transitions/cover-left.png deleted file mode 100644 index 80ea97f..0000000 Binary files a/docs/transitions/cover-left.png and /dev/null differ diff --git a/docs/transitions/cover-right.png b/docs/transitions/cover-right.png deleted file mode 100644 index 8b7ab81..0000000 Binary files a/docs/transitions/cover-right.png and /dev/null differ diff --git a/docs/transitions/cover-up.png b/docs/transitions/cover-up.png deleted file mode 100644 index 87c604d..0000000 Binary files a/docs/transitions/cover-up.png and /dev/null differ diff --git a/docs/transitions/dissolve.png b/docs/transitions/dissolve.png deleted file mode 100644 index 307a860..0000000 Binary files a/docs/transitions/dissolve.png and /dev/null differ diff --git a/docs/transitions/fade-review/color.gif b/docs/transitions/fade-review/color.gif deleted file mode 100644 index 6f48c3b..0000000 Binary files a/docs/transitions/fade-review/color.gif and /dev/null differ diff --git a/docs/transitions/fade-review/color.pdf b/docs/transitions/fade-review/color.pdf deleted file mode 100644 index f17e002..0000000 Binary files a/docs/transitions/fade-review/color.pdf and /dev/null differ diff --git a/docs/transitions/fade-review/color.png b/docs/transitions/fade-review/color.png deleted file mode 100644 index 1550c70..0000000 Binary files a/docs/transitions/fade-review/color.png and /dev/null differ diff --git a/docs/transitions/fade-review/contact-sheet.png b/docs/transitions/fade-review/contact-sheet.png deleted file mode 100644 index e730043..0000000 Binary files a/docs/transitions/fade-review/contact-sheet.png and /dev/null differ diff --git a/docs/transitions/fade-review/dark.gif b/docs/transitions/fade-review/dark.gif deleted file mode 100644 index b450bcc..0000000 Binary files a/docs/transitions/fade-review/dark.gif and /dev/null differ diff --git a/docs/transitions/fade-review/dark.pdf b/docs/transitions/fade-review/dark.pdf deleted file mode 100644 index d96d934..0000000 Binary files a/docs/transitions/fade-review/dark.pdf and /dev/null differ diff --git a/docs/transitions/fade-review/dark.png b/docs/transitions/fade-review/dark.png deleted file mode 100644 index c08a94b..0000000 Binary files a/docs/transitions/fade-review/dark.png and /dev/null differ diff --git a/docs/transitions/fade-review/white.gif b/docs/transitions/fade-review/white.gif deleted file mode 100644 index 50d1e57..0000000 Binary files a/docs/transitions/fade-review/white.gif and /dev/null differ diff --git a/docs/transitions/fade-review/white.pdf b/docs/transitions/fade-review/white.pdf deleted file mode 100644 index 30bb00b..0000000 Binary files a/docs/transitions/fade-review/white.pdf and /dev/null differ diff --git a/docs/transitions/fade-review/white.png b/docs/transitions/fade-review/white.png deleted file mode 100644 index d5dcf57..0000000 Binary files a/docs/transitions/fade-review/white.png and /dev/null differ diff --git a/docs/transitions/fly-in-down.png b/docs/transitions/fly-in-down.png deleted file mode 100644 index 82e3510..0000000 Binary files a/docs/transitions/fly-in-down.png and /dev/null differ diff --git a/docs/transitions/fly-in-left.png b/docs/transitions/fly-in-left.png deleted file mode 100644 index 151531e..0000000 Binary files a/docs/transitions/fly-in-left.png and /dev/null differ diff --git a/docs/transitions/fly-in-right.png b/docs/transitions/fly-in-right.png deleted file mode 100644 index 10d69f5..0000000 Binary files a/docs/transitions/fly-in-right.png and /dev/null differ diff --git a/docs/transitions/fly-in-up.png b/docs/transitions/fly-in-up.png deleted file mode 100644 index 3dfc7ef..0000000 Binary files a/docs/transitions/fly-in-up.png and /dev/null differ diff --git a/docs/transitions/fly-out-down-left.png b/docs/transitions/fly-out-down-left.png deleted file mode 100644 index 0bf4ee8..0000000 Binary files a/docs/transitions/fly-out-down-left.png and /dev/null differ diff --git a/docs/transitions/fly-out-down-right.png b/docs/transitions/fly-out-down-right.png deleted file mode 100644 index 1c87c0b..0000000 Binary files a/docs/transitions/fly-out-down-right.png and /dev/null differ diff --git a/docs/transitions/fly-out-up-left.png b/docs/transitions/fly-out-up-left.png deleted file mode 100644 index f9a5688..0000000 Binary files a/docs/transitions/fly-out-up-left.png and /dev/null differ diff --git a/docs/transitions/fly-out-up-right.png b/docs/transitions/fly-out-up-right.png deleted file mode 100644 index a4acf51..0000000 Binary files a/docs/transitions/fly-out-up-right.png and /dev/null differ diff --git a/docs/transitions/push-down.png b/docs/transitions/push-down.png deleted file mode 100644 index ce3fb09..0000000 Binary files a/docs/transitions/push-down.png and /dev/null differ diff --git a/docs/transitions/push-left.png b/docs/transitions/push-left.png deleted file mode 100644 index 7ac9c23..0000000 Binary files a/docs/transitions/push-left.png and /dev/null differ diff --git a/docs/transitions/push-right.png b/docs/transitions/push-right.png deleted file mode 100644 index ff1697d..0000000 Binary files a/docs/transitions/push-right.png and /dev/null differ diff --git a/docs/transitions/push-up.png b/docs/transitions/push-up.png deleted file mode 100644 index b6a61d5..0000000 Binary files a/docs/transitions/push-up.png and /dev/null differ diff --git a/docs/transitions/split-horizontal-inward.png b/docs/transitions/split-horizontal-inward.png deleted file mode 100644 index d81ea0a..0000000 Binary files a/docs/transitions/split-horizontal-inward.png and /dev/null differ diff --git a/docs/transitions/split-horizontal-outward.png b/docs/transitions/split-horizontal-outward.png deleted file mode 100644 index 883db49..0000000 Binary files a/docs/transitions/split-horizontal-outward.png and /dev/null differ diff --git a/docs/transitions/split-vertical-inward.png b/docs/transitions/split-vertical-inward.png deleted file mode 100644 index 00dd335..0000000 Binary files a/docs/transitions/split-vertical-inward.png and /dev/null differ diff --git a/docs/transitions/split-vertical-outward.png b/docs/transitions/split-vertical-outward.png deleted file mode 100644 index 007cb81..0000000 Binary files a/docs/transitions/split-vertical-outward.png and /dev/null differ diff --git a/docs/transitions/uncover-down.png b/docs/transitions/uncover-down.png deleted file mode 100644 index a538cfb..0000000 Binary files a/docs/transitions/uncover-down.png and /dev/null differ diff --git a/docs/transitions/uncover-left.png b/docs/transitions/uncover-left.png deleted file mode 100644 index 4dff4a3..0000000 Binary files a/docs/transitions/uncover-left.png and /dev/null differ diff --git a/docs/transitions/uncover-right.png b/docs/transitions/uncover-right.png deleted file mode 100644 index edf0694..0000000 Binary files a/docs/transitions/uncover-right.png and /dev/null differ diff --git a/docs/transitions/uncover-up.png b/docs/transitions/uncover-up.png deleted file mode 100644 index ef13dc8..0000000 Binary files a/docs/transitions/uncover-up.png and /dev/null differ diff --git a/docs/transitions/wipe-down.png b/docs/transitions/wipe-down.png deleted file mode 100644 index 576ea8e..0000000 Binary files a/docs/transitions/wipe-down.png and /dev/null differ diff --git a/docs/transitions/wipe-left.png b/docs/transitions/wipe-left.png deleted file mode 100644 index e1791e2..0000000 Binary files a/docs/transitions/wipe-left.png and /dev/null differ diff --git a/docs/transitions/wipe-right.png b/docs/transitions/wipe-right.png deleted file mode 100644 index c475778..0000000 Binary files a/docs/transitions/wipe-right.png and /dev/null differ diff --git a/docs/transitions/wipe-up.png b/docs/transitions/wipe-up.png deleted file mode 100644 index ca9b872..0000000 Binary files a/docs/transitions/wipe-up.png and /dev/null differ diff --git a/examples/showcase/README.md b/examples/showcase/README.md index 420d8b0..3d6c5ec 100644 --- a/examples/showcase/README.md +++ b/examples/showcase/README.md @@ -19,24 +19,20 @@ meson compile -C build doc ``` The explicit `doc` target runs pdfLaTeX twice and writes -`build/examples/showcase/showcase.pdf`. It is always defined and is excluded from the -default application build. Invoking it without pdfLaTeX or the required TeX -packages fails; documentation is never silently skipped. CI runs this same -target and uploads the PDF as the `beamer-showcase` artifact. +`build/examples/showcase/showcase.pdf`. Build it explicitly with pdfLaTeX and the required TeX packages installed. +CI runs this same target and uploads the PDF as the `beamer-showcase` artifact. For package installation, configure with `-Dinstall_docs=true`, build `doc`, then install normally. This installs `showcase.pdf`, `showcase.tex` and this README under `/share/doc/pdfcube/examples/` (or the configured -`datadir`). The option defaults to false and does not depend on installed tools. -The target tracks source changes, so unchanged PDFs are not rebuilt. +`datadir`). The option defaults to false. The target rebuilds the PDF when its source changes. Debian packages and AppImages include these files. GitHub releases also attach `pdfcube-VERSION-showcase.pdf`, covered by the release's `SHA256SUMS`. The source archive contains the editable source and build instructions. -Use pdfLaTeX with the normal Beamer presentation mode. Do not enable -`\DocumentMetadata`: this source uses Hyperref's classic PDF transition -syntax, including explicit Fly scale and motion parameters. +Use pdfLaTeX with the normal Beamer presentation mode and Hyperref's classic +PDF transition syntax, including explicit Fly scale and motion parameters. ## Playback @@ -93,9 +89,7 @@ MP4 output. Recording ignores interactive demo mode and requires no `--program`. - Dissolve, Glitter (right/down/lower right), Fade and Replace. Glitter and Wipe are animated in PDFCube. Glitter releases fading fragments and -sparks; Wipe moves a raised blade in the requested direction. Fade currently -falls back to an instant change; its slide says so and preserves the real PDF -metadata for the planned implementation. Other viewers may implement them +sparks; Wipe moves a raised blade in the requested direction. Fade blends the stationary slides in linear light with gently eased timing. Other viewers may implement them or ignore PDF transitions entirely. Replace is deliberately instant. PDF transitions are stored on the **destination** page. Each `\ShowSlide` call diff --git a/examples/showcase/showcase.tex b/examples/showcase/showcase.tex index fd78c8f..6262516 100644 --- a/examples/showcase/showcase.tex +++ b/examples/showcase/showcase.tex @@ -187,7 +187,7 @@ % Slide 45: transition entering this page. \ShowSlide{Wipe /Di 270 /D 1.25}{Wipe / down}{A raised wiper sweeps down\\and reveals the next slide.}{Fade}{44}{plum}{Animated in PDFCube} % Slide 46: transition entering this page. -\ShowSlide{Fade /D 1.25}{Fade}{A standard PDF crossfade\\ready for a future renderer.}{Replace}{45}{night}{PDFCube currently replaces instantly} +\ShowSlide{Fade /D 1.25}{Fade}{Two stationary slides exchange light\\with gently eased timing.}{Replace}{45}{night}{Linear-light crossfade} % Slide 47: transition entering this page. \ShowSlide{R /D 1.25}{Replace}{An immediate page change.\\The reference for clean endpoints.}{Play it again}{46}{sea}{Instant in PDFCube} % Slide 48: transition entering this page. diff --git a/examples/transitions/README.md b/examples/transitions/README.md index 29869db..e1f5f02 100644 --- a/examples/transitions/README.md +++ b/examples/transitions/README.md @@ -22,6 +22,7 @@ Ready-to-open diagnostic slides: [lower right](fly-out-down-right.pdf) - Push: [right](push-right.pdf), [up](push-up.pdf), [left](push-left.pdf), [down](push-down.pdf) +- [Fade](fade.pdf) - [Dissolve](dissolve.pdf) - Glitter: [right](glitter-right.pdf), [down](glitter-down.pdf), [lower right](glitter-down-right.pdf) @@ -58,7 +59,7 @@ Regenerate all PDFs deterministically with Python's standard library: python3 tests/transition_fixture.py examples/transitions ``` -The generator is the source of truth; do not edit the PDFs by hand. +Regenerate the PDFs from the generator, which is their source of truth. See the [review roadmap](../../docs/transitions/README.md) for videos and tests. ## Wipe diff --git a/man/pdfcube.1 b/man/pdfcube.1 index 78e0e08..f35bacd 100644 --- a/man/pdfcube.1 +++ b/man/pdfcube.1 @@ -31,7 +31,10 @@ edge and recover fully. PDF direction selects right, up, left, or down; missing or unsupported angles default to right. PDF Dissolve cracks the outgoing page into irregular glass-like shards, then moves the fragments away to reveal the destination page. -Other unsupported PDF transitions currently change pages instantly. +PDF Glitter releases directional fragments and warm sparks. +PDF Wipe sweeps a raised physical wiper across the slide. +PDF Fade exchanges light between stationary slides with gently eased timing. +PDF Replace and unrecognized transition types change pages instantly. PDF transition duration controls the animation; page duration controls how long automatic playback displays a slide. These are separate values. .TP @@ -85,6 +88,8 @@ selects directional fragments and sparks with a 750 ms animation. selects a raised physical wiper with a 750 ms animation and the PDF direction (0/right, 90/up, 180/left, 270/down; other angles default to right). Both pages retain their fitted aspect ratios. +.B fade +selects a linear-light crossfade with a 750 ms animation. The last pair is repeated for remaining slides. For example, .B \-\-program 5:c:3:p @@ -127,7 +132,7 @@ Toggle fullscreen mode. Toggle automatic advancement. .TP .B 1 through 9 -Jump to page 5*(n-1). These unmodified keys are page shortcuts. +Jump to pages 1, 6, 11, and so on (page 5*(n-1)+1). These unmodified keys are page shortcuts. .TP .B Ctrl-1 through Ctrl-9 Jump to the corresponding PDF or programmed cube section. The Ctrl modifier diff --git a/tests/transition_fixture.py b/tests/transition_fixture.py index af6c6cf..8b20793 100644 --- a/tests/transition_fixture.py +++ b/tests/transition_fixture.py @@ -27,7 +27,8 @@ EXAMPLES += [("fade", "Fade", "H", "I", None)] def make_pdf(path, orientation="H", sizes=((640, 360), (640, 360)), pages=(0, 1), - effect="Blinds", motion="I", angle=None, start_scale=.35, rectangular=True): + effect="Blinds", motion="I", angle=None, start_scale=.35, rectangular=True, + colors=("0.12 0.32 0.65", "0.65 0.23 0.12"), ink="1 1 1"): objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b""] objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") kids = [] @@ -44,8 +45,8 @@ def make_pdf(path, orientation="H", sizes=((640, 360), (640, 360)), pages=(0, 1) f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] " f"/Resources << /Font << /F1 3 0 R >> >> /Contents {page_id + 1} 0 R " f"{transition if number == 1 else ''} >>").encode()) - color = "0.12 0.32 0.65" if number == 0 else "0.65 0.23 0.12" - commands = [f"{color} rg 0 0 {width} {height} re f", "1 1 1 RG 0.5 w"] + color = colors[number] + commands = [f"{color} rg 0 0 {width} {height} re f", f"{ink} RG 0.5 w"] for i in range(1, 12): x, y = width * i / 12, height * i / 12 commands += [f"{x} 0 m {x} {height} l S", f"0 {y} m {width} {y} l S"] @@ -54,7 +55,7 @@ def make_pdf(path, orientation="H", sizes=((640, 360), (640, 360)), pages=(0, 1) ("BOTTOM LEFT", 10, 12), ("BOTTOM RIGHT", width - 110, 12), ("OUTGOING" if number == 0 else "INCOMING", width / 2 - 50, height / 2), ): - commands.append(f"1 1 1 rg BT /F1 14 Tf {x} {y} Td ({label}) Tj ET") + commands.append(f"{ink} rg BT /F1 14 Tf {x} {y} Td ({label}) Tj ET") stream = "\n".join(commands).encode() objects.append(f"<< /Length {len(stream)} >>\nstream\n".encode() + stream + b"\nendstream") objects[1] = f"<< /Type /Pages /Kids [{' '.join(kids)}] /Count {len(kids)} >>".encode() diff --git a/tools/fade_review.py b/tools/fade_review.py deleted file mode 100644 index 50b69bf..0000000 --- a/tools/fade_review.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -"""Generate synthetic Fade review strips and animations from the headless driver. - -Run from the repository root after meson compile -C build. Requires Pillow. -All frames use explicit progress in a single headless context. -""" -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'tests')) -from transition_fixture import make_pdf -from render_client import Frames -from PIL import Image, ImageDraw -root=Path.cwd()/'docs/transitions/fade-review' -root.mkdir(exist_ok=True) -# Each fixture is generated with valid xref offsets after changing its palette. -import transition_fixture as fixture -source=Path('tests/transition_fixture.py').read_text() -frames=Frames('build/tests/render-driver',root) -sheets=[] -for name,old,new in [('white','0.98 0.98 0.98','0.94 0.96 1'),('dark','0.015 0.025 0.06','0.1 0.025 0.015'),('color','0.12 0.32 0.65','0.65 0.23 0.12')]: - scope={'__name__':'preview'} - themed = source.replace('0.12 0.32 0.65',old).replace('0.65 0.23 0.12',new) - if name == 'white': - themed = themed.replace('1 1 1 RG', '0.15 0.15 0.15 RG').replace('1 1 1 rg', '0.15 0.15 0.15 rg') - exec(themed,scope) - pdf=root/f'{name}.pdf' - scope['make_pdf'](pdf,effect='Fade') - sheet=Image.new('RGB',(1600,210),'#202020') - animation=[] - for i in range(31): - output=root/f'{name}-frame.png' - frames.render('--bgcolor','0.04:0.04:0.04','--snapshot',output,'--snapshot-progress',str(i/30),pdf) - image=Image.open(output).convert('RGB'); image.thumbnail((640,360)); animation.append(image.copy()) - for column,t in enumerate((0,.25,.5,.75,1)): - output=root/f'{name}-{column}.png' - frames.render('--bgcolor','0.04:0.04:0.04','--snapshot',output,'--snapshot-progress',str(t),pdf) - image=Image.open(output).convert('RGB');image.thumbnail((320,180)) - sheet.paste(image,(column*320,30)) - ImageDraw.Draw(sheet).text((column*320+8,8),f'{name} {t:.0%}',fill='white') - output.unlink() - sheet.save(root/f'{name}.png'); sheets.append(sheet) - animation[0].save(root/f'{name}.gif',save_all=True,append_images=animation[1:],duration=40,loop=0) - (root/f'{name}-frame.png').unlink() -frames.close() -combined=Image.new('RGB',(1600,630)) -for i,sheet in enumerate(sheets):combined.paste(sheet,(0,i*210)) -combined.save(root/'contact-sheet.png') diff --git a/tools/transition_gallery.py b/tools/transition_gallery.py index 888b5e1..80829dd 100644 --- a/tools/transition_gallery.py +++ b/tools/transition_gallery.py @@ -19,9 +19,7 @@ parser.add_argument("--frames", type=int, default=33) parser.add_argument("--jobs", type=int, default=min(4, os.cpu_count() or 1), help="Number of snapshots to capture concurrently (default: up to 4)") -parser.add_argument("--update-docs", action="store_true", - help="Refresh the checked-in contact sheets from these captures") -parser.add_argument("--effects", nargs="+", choices=("Blinds", "Split", "Cover", "Uncover", "Fly", "Push", "Dissolve", "Glitter", "Wipe"), +parser.add_argument("--effects", nargs="+", choices=("Blinds", "Split", "Cover", "Uncover", "Fly", "Push", "Dissolve", "Glitter", "Wipe", "Fade"), help="Capture only selected effects (default: all)") parser.add_argument("--inside-xvfb", action="store_true", help=argparse.SUPPRESS) args = parser.parse_args() @@ -48,11 +46,20 @@ env = dict(os.environ, GDK_BACKEND="x11", LIBGL_ALWAYS_SOFTWARE="1", GTK_A11Y="none") examples = [entry for entry in EXAMPLES if not args.effects or entry[1] in args.effects] +# Fade uses the same capture pipeline for colored, light, and dark slides. +if any(entry[1] == "Fade" for entry in examples): + examples += [("fade-light", "Fade", "H", "I", None), + ("fade-dark", "Fade", "H", "I", None)] +palettes = { + "fade-light": dict(colors=("0.98 0.98 0.98", "0.94 0.96 1"), ink="0.15 0.15 0.15"), + "fade-dark": dict(colors=("0.015 0.025 0.06", "0.1 0.025 0.015")), +} for name, effect, orientation, motion, angle in examples: folder = args.output / name folder.mkdir(exist_ok=True) pdf = folder / "fixture.pdf" - make_pdf(pdf, orientation, effect=effect, motion=motion, angle=angle) + make_pdf(pdf, orientation, effect=effect, motion=motion, angle=angle, + **palettes.get(name, {})) for old_frame in folder.glob("frame-*.png"): old_frame.unlink() @@ -72,8 +79,6 @@ def capture(i): sheet.paste(image, (column * 480, 25)) ImageDraw.Draw(sheet).text((column * 480 + 8, 6), f"{frame / (args.frames - 1):.0%}", fill="white") sheet.save(folder / "contact-sheet.png") - if args.update_docs: - sheet.save(Path(__file__).resolve().parents[1] / "docs" / "transitions" / f"{name}.png") subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", str((args.frames - 1) / 1.25), "-i", str(folder / "frame-%03d.png"), "-vf", "tpad=start_duration=0.5:start_mode=clone:stop_duration=0.5:stop_mode=clone", @@ -85,12 +90,12 @@ def capture(i): "source_dirty": bool(subprocess.check_output(["git", "status", "--porcelain"])), "frames": args.frames, "backend": "X11", "examples": [entry[0] for entry in examples], - "renderer": "LIBGL_ALWAYS_SOFTWARE=1", "status": "candidate, not approved", + "renderer": "LIBGL_ALWAYS_SOFTWARE=1", "binary_version": subprocess.check_output([str(args.binary.resolve()), "--version"], text=True).strip(), }, indent=2) + "\n") (args.output / "index.html").write_text( - "Transition review" - "

Transitions — candidates

" + "".join( + "Transition gallery" + "

Transition gallery

" + "".join( f"

{name}

" f"

Fixture PDF

" f""