feat(pdf): restore smooth crossfade for Chrome 111+ using document.startViewTransition
Build and Deploy Verso / deploy (push) Successful in 11m24s

The original code used container.startViewTransition(setDocument) with a
synchronous callback, giving a 250ms CSS crossfade that looked smooth when
the PDF happened to re-render before the animation ended — but was a race.

Now there are three tiers:
- Chrome 126+: element-level startViewTransition, async, waits for pagerendered
- Chrome 111+ (Brave 138, Edge 111+): document-level startViewTransition with
  view-transition-name scoped to the PDF container, same async pattern
- Firefox / Safari / older Chromium: canvas snapshot overlay (no animation,
  but seamless — introduced in build #108)

The document-level path restores the smooth fade the user saw on Edge build
#93, now guaranteed to crossfade old→new rather than old→blank.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-07 12:22:46 +00:00
parent 4410a83146
commit d895e14e48
@@ -118,9 +118,25 @@ export default class PDFJSWrapper {
})
const container = this.container as typeof this.container & {
// supported since Chrome 126
// element-level View Transitions (Level 2), Chrome 126+
startViewTransition?: (cb: () => void | Promise<void>) => {
ready: Promise<void>
finished: Promise<void>
}
}
const doc_ = document as typeof document & {
// document-level View Transitions (Level 1), Chrome 111+
startViewTransition?: (cb: () => void | Promise<void>) => {
ready: Promise<void>
finished: Promise<void>
}
}
// Shared error handler for transition.ready rejections.
const onTransitionError = (err: any) => {
// InvalidStateError just means the document was hidden during the transition.
if (err?.name !== 'InvalidStateError') {
captureException(err, { tags: { handler: 'pdf-preview' } })
}
}
@@ -128,27 +144,33 @@ export default class PDFJSWrapper {
typeof container.startViewTransition === 'function' &&
document.visibilityState !== 'hidden'
) {
// Chrome 126+: element-level View Transition API.
// The async callback holds the "before" snapshot until pagerendered fires,
// so the crossfade goes old→new rather than old→blank.
// Chrome 126+: element-level View Transition, scoped to this container.
// Async callback holds the "before" snapshot until pagerendered fires.
const transition = container.startViewTransition(async () => {
setDocument()
await firstPageRendered
})
transition.ready.catch(err => {
// ignore InvalidStateError, it just means the document was hidden
if (err?.name !== 'InvalidStateError') {
captureException(err, {
tags: { handler: 'pdf-preview' },
})
}
transition.ready.catch(onTransitionError)
} else if (
typeof doc_.startViewTransition === 'function' &&
document.visibilityState !== 'hidden'
) {
// Chrome 111+ / Edge 111+: document-level View Transition.
// Give the PDF container a unique view-transition-name so only it
// crossfades; the rest of the page stays static.
const name = 'ol-pdf-viewer'
this.container.style.viewTransitionName = name
const transition = doc_.startViewTransition(async () => {
setDocument()
await firstPageRendered
})
transition.finished.finally(() => {
this.container.style.viewTransitionName = ''
})
transition.ready.catch(onTransitionError)
} else {
// All other browsers: snapshot the currently-rendered canvases into a
// temporary overlay so setDocument()'s synchronous canvas-clear doesn't
// produce a white flash. The overlay is removed once the first page of
// the new document has been painted.
// Firefox / Safari / very old Chromium: canvas snapshot overlay keeps
// the old content visible while setDocument() clears and repaints.
const snap = this._snapshotCanvases()
setDocument()
if (snap) {