Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Fly adds a controlled leaf-like drift in arbitrary PDF directions, with the
same reproducible review assets. Push transfers momentum between two softly
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
raised, beveled wiper 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.

[![CI](https://github.com/baol/pdfcube/actions/workflows/ci.yml/badge.svg)](https://github.com/baol/pdfcube/actions/workflows/ci.yml)

Expand Down
55 changes: 50 additions & 5 deletions docs/transitions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,53 @@ review, to accepted; a passing build is not visual approval.
| 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 / Fade / Replace | Physical wiper / crossfade / instant | Direction, clean endpoints |
| 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 shallow squeegee rail sweeps across the viewport, clearing the outgoing page
and revealing the destination behind it. A dark rubber lip, metal face and
highlighted bevel make the moving edge visible. The pages stay flat, readable
and at full resolution. Travel eases in and out and extends beyond both edges,
so the rail 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 rail. 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. Cold sanitizer checkpoint cases
missed the 500 ms SLO (up to 1.47 seconds); this is recorded separately from
functional correctness and does not establish a hardware performance pass.

## Glitter: experimental, awaiting visual approval

A directional front releases small textured pieces of the outgoing slide,
Expand Down Expand Up @@ -241,16 +281,16 @@ 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 Cover Uncover` for a focused page-flip review; CI captures all
implemented effects by default. Gallery capture reuses one Xvfb display and
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. CI uploads this bundle on
every pull request. Download and unzip it to view locally.
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,
Expand Down Expand Up @@ -291,6 +331,11 @@ native-platform or motion approval.
- 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.
Expand Down
Binary file added docs/transitions/wipe-down.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/wipe-left.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/wipe-right.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/transitions/wipe-up.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions examples/showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ MP4 output. Recording ignores interactive demo mode and requires no `--program`.
- Push and Wipe: all four cardinal directions.
- Dissolve, Glitter (right/down/lower right), Fade and Replace.

Glitter, Wipe and Fade currently fall back to an instant change
in PDFCube. Their slides say so; they preserve the real PDF metadata to provide
a showcase as these effects are implemented. Other viewers may implement them
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
or ignore PDF transitions entirely. Replace is deliberately instant.

PDF transitions are stored on the **destination** page. Each `\ShowSlide` call
Expand Down
16 changes: 8 additions & 8 deletions examples/showcase/showcase.tex
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,21 @@
% Slide 37: transition entering this page.
\ShowSlide{Push /Di 270 /D 1.25}{Push / down}{Soft slide blocks transfer\\momentum and briefly deform.}{Dissolve}{36}{night}{Animated in PDFCube}
% Slide 38: transition entering this page.
\ShowSlide{Dissolve /D 1.25}{Dissolve}{The outgoing slide cracks\\into falling glass shards.}{Glitter / right}{37}{sea}{Animated in PDFCube}
\ShowSlide{Dissolve /D 1.25}{Dissolve}{The outgoing slide shatters\\into tumbling glass fragments.}{Glitter / right}{37}{sea}{Animated in PDFCube}
% Slide 39: transition entering this page.
\ShowSlide{Glitter /Di 0 /D 1.25}{Glitter / right}{A standard PDF sparkle effect\\ready for a future renderer.}{Glitter / down}{38}{plum}{PDFCube currently replaces instantly}
\ShowSlide{Glitter /Di 0 /D 1.25}{Glitter / right}{A moving front releases pieces\\with a brief trail of sparks.}{Glitter / down}{38}{plum}{Animated in PDFCube}
% Slide 40: transition entering this page.
\ShowSlide{Glitter /Di 270 /D 1.25}{Glitter / down}{A standard PDF sparkle effect\\ready for a future renderer.}{Glitter / lower right}{39}{night}{PDFCube currently replaces instantly}
\ShowSlide{Glitter /Di 270 /D 1.25}{Glitter / down}{A moving front releases pieces\\with a brief trail of sparks.}{Glitter / lower right}{39}{night}{Animated in PDFCube}
% Slide 41: transition entering this page.
\ShowSlide{Glitter /Di 315 /D 1.25}{Glitter / lower right}{A standard PDF sparkle effect\\ready for a future renderer.}{Wipe / right}{40}{sea}{PDFCube currently replaces instantly}
\ShowSlide{Glitter /Di 315 /D 1.25}{Glitter / lower right}{A moving front releases pieces\\with a brief trail of sparks.}{Wipe / right}{40}{sea}{Animated in PDFCube}
% Slide 42: transition entering this page.
\ShowSlide{Wipe /Di 0 /D 1.25}{Wipe / right}{A standard PDF directional reveal\\ready for a future renderer.}{Wipe / up}{41}{plum}{PDFCube currently replaces instantly}
\ShowSlide{Wipe /Di 0 /D 1.25}{Wipe / right}{A raised wiper sweeps right\\and reveals the next slide.}{Wipe / up}{41}{plum}{Animated in PDFCube}
% Slide 43: transition entering this page.
\ShowSlide{Wipe /Di 90 /D 1.25}{Wipe / up}{A standard PDF directional reveal\\ready for a future renderer.}{Wipe / left}{42}{night}{PDFCube currently replaces instantly}
\ShowSlide{Wipe /Di 90 /D 1.25}{Wipe / up}{A raised wiper sweeps up\\and reveals the next slide.}{Wipe / left}{42}{night}{Animated in PDFCube}
% Slide 44: transition entering this page.
\ShowSlide{Wipe /Di 180 /D 1.25}{Wipe / left}{A standard PDF directional reveal\\ready for a future renderer.}{Wipe / down}{43}{sea}{PDFCube currently replaces instantly}
\ShowSlide{Wipe /Di 180 /D 1.25}{Wipe / left}{A raised wiper sweeps left\\and reveals the next slide.}{Wipe / down}{43}{sea}{Animated in PDFCube}
% Slide 45: transition entering this page.
\ShowSlide{Wipe /Di 270 /D 1.25}{Wipe / down}{A standard PDF directional reveal\\ready for a future renderer.}{Fade}{44}{plum}{PDFCube currently replaces instantly}
\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}
% Slide 47: transition entering this page.
Expand Down
11 changes: 11 additions & 0 deletions examples/transitions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Ready-to-open diagnostic slides:
- Push: [right](push-right.pdf), [up](push-up.pdf),
[left](push-left.pdf), [down](push-down.pdf)
- [Dissolve](dissolve.pdf)
- Glitter: [right](glitter-right.pdf), [down](glitter-down.pdf),
[lower right](glitter-down-right.pdf)
- Wipe: [right](wipe-right.pdf), [up](wipe-up.pdf),
[left](wipe-left.pdf), [down](wipe-down.pdf)

From the repository root:

Expand Down Expand Up @@ -56,6 +60,13 @@ python3 tests/transition_fixture.py examples/transitions

The generator is the source of truth; do not edit the PDFs by hand.
See the [review roadmap](../../docs/transitions/README.md) for videos and tests.
## Wipe

The four Wipe examples move a raised, beveled blade in the named PDF direction.
The destination is revealed behind its rubber lip; the unrevealed part stays
exactly on the outgoing page. Both pages retain their aspect ratios. Use the
normal Space/Page Down playback or `--program 4:wipe`.

## Glitter

`glitter-right.pdf`, `glitter-down.pdf` and `glitter-down-right.pdf` demonstrate
Expand Down
Binary file added examples/transitions/wipe-down.pdf
Binary file not shown.
Binary file added examples/transitions/wipe-left.pdf
Binary file not shown.
Binary file added examples/transitions/wipe-right.pdf
Binary file not shown.
Binary file added examples/transitions/wipe-up.pdf
Binary file not shown.
6 changes: 6 additions & 0 deletions man/pdfcube.1
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ selects the soft-block Push effect with a 750 ms animation and the document's
direction.
.B dissolve
selects the glass-shatter Dissolve effect with a 750 ms animation.
.B glitter
selects directional fragments and sparks with a 750 ms animation.
.B wipe
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.
The last pair is repeated for remaining slides.
For example,
.B \-\-program 5:c:3:p
Expand Down
2 changes: 1 addition & 1 deletion src/options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ ParseResult parse_options(int argc, char** argv) {
("experimental", po::value<bool>()->implicit_value(true),
"Enable experimental animation details, including thin glass shard edges.")
("program", po::value<std::string>(),
"Transition program duration:type pairs (c, p, blinds, split, cover, uncover, fly, push, dissolve, glitter), for example 5:c:3:p.")
"Transition program duration:type pairs (c, p, blinds, split, cover, uncover, fly, push, dissolve, glitter, wipe), for example 5:c:3:p.")
("performance-log", po::value<std::string>(),
"Write buffered animation frame timings to a CSV file on exit.")
("no-fullscreen,n", po::value<bool>()->implicit_value(true),
Expand Down
2 changes: 2 additions & 0 deletions src/playback.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ inline Transition transition_from_name(const std::string& effect) {
if (effect == "fly") return Transition::fly;
if (effect == "push") return Transition::push;
if (effect == "dissolve") return Transition::dissolve;
if (effect == "wipe") return Transition::wipe;
if (effect == "glitter") return Transition::glitter;
return Transition::switch_page;
}
Expand All @@ -32,6 +33,7 @@ inline Transition transition_from_page(const PageInfo& page) {
case POPPLER_PAGE_TRANSITION_FLY: return Transition::fly;
case POPPLER_PAGE_TRANSITION_PUSH: return Transition::push;
case POPPLER_PAGE_TRANSITION_DISSOLVE: return Transition::dissolve;
case POPPLER_PAGE_TRANSITION_WIPE: return Transition::wipe;
case POPPLER_PAGE_TRANSITION_GLITTER: return Transition::glitter;
default: return Transition::switch_page;
}
Expand Down
47 changes: 47 additions & 0 deletions src/renderer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "push.hpp"
#include "dissolve.hpp"
#include "glitter.hpp"
#include "wipe.hpp"

#include <algorithm>
#include <array>
Expand Down Expand Up @@ -555,6 +556,52 @@ void Renderer::render(int width, int height, const FrameState& frame) {
texture_size, Matrix::identity().value.data());
return;
}
if (frame.transition == Transition::wipe) {
const auto wipe = wipe_geometry(frame.progress, frame.direction_degrees, width, height);
const auto draw_fitted = [&](int index) {
const auto& info = document_.page_info(index);
const auto fit = content_viewport(width, height, info.width / info.height);
glViewport(fit.x, fit.y, fit.width, fit.height);
draw_page(index, page_size(index), Matrix::identity().value.data());
};
glDisable(GL_DEPTH_TEST);
draw_fitted(frame.next_page);
glEnable(GL_SCISSOR_TEST);
glScissor(wipe.outgoing.x, wipe.outgoing.y,
wipe.outgoing.width, wipe.outgoing.height);
// The outgoing letterbox is part of its image too. Clear the unrevealed
// region before drawing it, so a taller incoming page cannot leak through.
glClear(GL_COLOR_BUFFER_BIT);
draw_fitted(frame.current_page);
glDisable(GL_SCISSOR_TEST);

// A shallow squeegee rail, with dark rubber lip and a bright metal bevel.
// Its center covers the pixel-aligned reveal edge. Fixed geometry and
// closed-form travel keep memory bounded and replay exactly reproducible.
glViewport(0, 0, width, height);
const Matrix view = perspective(2 * std::atan(1 / flip_camera), 1, .5F, 10) *
translate(0, 0, -flip_camera);
const Matrix rail = translate(wipe.horizontal ? wipe.center : 0,
wipe.horizontal ? 0 : wipe.center, 0) *
rotate_z(wipe.horizontal ? 0 : pi / 2);
const float half = wipe.half_width;
constexpr float depth = .035F;
for (const float side : {-1.0F, 1.0F}) {
draw_color(Color{.11F,.14F,.17F,1},
(view * rail * translate(side * half,0,depth/2) *
rotate_y(side * pi/2) * scale(depth/2,1.08F,1)).value.data());
}
draw_color(Color{.14F,.17F,.20F,1},
(view * rail * translate(0,0,depth) * scale(half,1.08F,1)).value.data());
draw_color(Color{.43F,.48F,.53F,1},
(view * rail * translate(-half*.12F,0,depth) * scale(half*.58F,1.08F,1)).value.data());
draw_color(Color{.78F,.82F,.85F,1},
(view * rail * translate(-half*.67F,0,depth) * scale(half*.10F,1.08F,1)).value.data());
draw_color(Color{.035F,.045F,.055F,1},
(view * rail * translate(half*.78F,0,depth) * scale(half*.22F,1.08F,1)).value.data());
glEnable(GL_DEPTH_TEST);
return;
}
if (frame.transition == Transition::push) {
const PushPose pose = push_pose(frame.progress, frame.direction_degrees);
const Matrix view = perspective(2 * std::atan(1 / flip_camera), 1, .5F, 10) *
Expand Down
5 changes: 3 additions & 2 deletions src/renderer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ namespace pdfcube {

enum class Transition {
none, cube_forward, cube_backward, switch_page, blinds, split, cover, uncover, fly, push,
dissolve, glitter
dissolve, glitter, wipe
};

inline bool page_effect(Transition transition) {
return transition == Transition::blinds || transition == Transition::split ||
transition == Transition::cover || transition == Transition::uncover ||
transition == Transition::fly || transition == Transition::push ||
transition == Transition::dissolve || transition == Transition::glitter;
transition == Transition::dissolve || transition == Transition::glitter ||
transition == Transition::wipe;
}

struct FrameState {
Expand Down
46 changes: 46 additions & 0 deletions src/wipe.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include <algorithm>
#include <cmath>

namespace pdfcube {

inline int wipe_direction(int degrees) {
return degrees == 90 || degrees == 180 || degrees == 270 ? degrees : 0;
}

struct WipeClip { int x, y, width, height; };
struct WipeGeometry {
WipeClip outgoing;
bool horizontal;
float center; // Clip-space blade center along its travel axis.
float half_width;
int direction;
};

// The blade travels just beyond both edges, including its bevel. Pixel-aligned
// clipping leaves no partially sampled seam between independently fitted pages.
inline WipeGeometry wipe_geometry(double progress, int degrees, int width, int height) {
const int direction = wipe_direction(degrees);
const bool horizontal = direction == 0 || direction == 180;
const bool positive = direction == 0 || direction == 90;
const int extent = horizontal ? width : height;
const float half_pixels = std::max(2.0F, .014F * std::min(width, height));
const float margin = 2 * half_pixels / extent;
const double p = std::clamp(progress, 0.0, 1.0);
const double eased = p * p * (3 - 2 * p);
const double front = -margin + (1 + 2 * margin) * eased;
const int swept = std::clamp(static_cast<int>(std::lround(front * extent)), 0, extent);
WipeClip clip{0, 0, width, height};
if (horizontal) {
clip.x = positive ? swept : 0;
clip.width = width - swept;
} else {
clip.y = positive ? swept : 0;
clip.height = height - swept;
}
return {clip, horizontal, static_cast<float>((positive ? 1 : -1) * (2 * front - 1)),
2 * half_pixels / extent, direction};
}

} // namespace pdfcube
5 changes: 5 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ and reads back all 61 frames of a 60 Hz second, including experimental glass.
A small Fontconfig fixture uses DejaVu Sans instead of scanning all system font folders;
both cold font and shader initialization remain inside the measured budget.

Wipe cases compare the regions ahead of and behind the blade with exact static
page pixels, including mixed page letterboxes. They verify that the next static
frame is unaffected by clipping/depth state. Geometry tests sample 1001 explicit
progress values per direction and viewport shape without wall-clock waits.

Clock and auto-advance tests use synthetic timestamps and a recording
scheduler. They verify frame progress and the exact millisecond delays sent to
the backend, including cancellation and idle dispatch. They never sleep.
Expand Down
Loading
Loading