@embedpdf/plugin-stage

中文文档
3.2.0发布于 2026/11/12Apache-2.0TypeScriptESM + CJS近 30 天下载 6.6万

The Stage plugin: one coordinate core (scroll + viewport + zoom + pan + spread) built on @embedpdf/core-stage. Pure intents & selectors; no DOM.

原始使用文档
文档目录8 节
  1. The camera: one mental model
  2. arrivalAlign — where navigation lands
  3. zoomAlign — what focal-less zoom zooms around
  4. anchorAlign — what survives a reframe
  5. viewRotation — rotate the view, not the document
  6. Recipes
  7. The scroller contract — scrollbars, minimaps, "% read"
  8. What these settings never touch

@embedpdf/plugin-stage

The Stage is the viewer's coordinate system: it lays pages out into a scene, points a camera at them, and turns every scroll, zoom, resize, and "go to page 5" into one camera move. Everything you see is the camera; everything you configure is a policy about how the camera is allowed to move.

The camera: one mental model

Every camera move is defined by what it holds fixed. There are five kinds of move, and each one keeps exactly one thing still:

You do…What stays fixedControlled by
drag, pinch, ctrl+wheelthe content under your pointernothing — that's physics
click zoom in/out, zoomTo, switch to fit-widtha focal point in the viewportzoomAlign
resize the container, rotate a page, change spreadthe spot you were looking atanchorAlign
goToPage, nextPage/previousPage, resetViewnothing — a fresh landingarrivalAlign
click an outline entry, a search hit, a PDF linkwhatever the call specifiesthe call itself, per-reveal

One more setting stands outside the table. When an axis of content fits the viewport, the camera has no freedom on that axis — there is nowhere to scroll. fitAlign says where content rests in that case (default: centered). It is a standing constraint applied to every move above, not a move of its own.

All four settings speak the same per-axis vocabulary — 'start' | 'center' | 'end', plus a viewport fraction for the three move policies — and x/y are independent:

stagePlugin({
  arrivalAlign: { x: 'start', y: 'start' },
  zoomAlign: { x: 'center', y: 'center' },
  anchorAlign: { x: 'start', y: 'start' },
  fitAlign: { x: 'center', y: 'center' },
});

(The values above are the defaults — a document-reading feel. If that's what you want, configure nothing.)

arrivalAlign — where navigation lands

When you navigate (goToPage, nextPage, previousPage, resetView), the target page lands at the same place at every zoom level — landing is a policy, never a side effect of how zoomed in you happen to be. Per axis:

ValueLanding
'start'reading edge flush with the viewport edge (top / reading-start) — default
'center'page centered — the presentation feel
'end'far edge flush
0–1page center at this viewport fraction (0.35 ≈ a browser find-bar)
'keep'this axis doesn't move at all

Landings clamp against the document edges, so they are best-effort on the first and last pages — exactly like a browser.

One value deserves a second look: x: 'keep' — page forward without losing your horizontal position. Zoomed into the left column of a two-column paper, nextPage() takes you to the left column of the next page. (It's the PDF /XYZ null semantic, as a default.)

Any single navigation can override the setting:

stage.goToPage(page, { arrivalAlign: { y: 'center' } });

zoomAlign — what focal-less zoom zooms around

Pinch and ctrl+wheel always zoom around the pointer — that is not configurable, because anything else feels broken. zoomAlign answers the remaining case: zooming with no pointer position — toolbar buttons, keyboard shortcuts, zoomTo, switching between fit modes.

Default { x: 'center', y: 'center' }: the middle of what you see stays put and the view inflates around it. Set y: 'start' for top-stable zoom — the first visible line holds still while everything grows below it (the text-editor feel). Values: 'start' | 'center' | 'end' | fraction.

anchorAlign — what survives a reframe

When the world reshapes under a passive camera — the container resizes, a page is rotated or deleted, the gap or spread changes — the Stage keeps you looking at what you were looking at. anchorAlign says where in the viewport that reference point lives: it is captured there before the change and restored there after.

Default { x: 'start', y: 'start' } — the browser scroll model: the top of what you see is pinned, and growth or shrinkage happens below. This is why a container that mounts small and then expands doesn't shove the document downward: the top stays where it was and the extra height reveals more page.

Set { x: 'center', y: 'center' } for canvas-style reframes: the middle is pinned and resizes balloon symmetrically (the Figma feel).

viewRotation — rotate the view, not the document

Adobe's "Rotate View": a quarter-turn applied to how every page displays in this lens, on top of each page's own /Rotate.

stage.rotateViewBy(90); // toolbar verb: one quarter-turn clockwise from here
stage.setViewRotation(180); // absolute
stage.getViewRotation(); // 0 | 90 | 180 | 270
stagePlugin({ viewRotation: 90 }); // or start rotated

Three properties define it:

  • Non-persistent. Nothing is written to the PDF — no engine call, no registry revision. Saving the document saves what was loaded. The permanent per-page rotation is plugin-page-edit's rotateBy/setRotation; keep the two on different buttons.
  • Per lens. It is a StageSettings field like zoom or layout, so the main viewer rotates while a thumbnail lens stays upright — or share one setting across lenses if your product wants them to agree.
  • A reframe. Changing it holds the anchorAlign point like any other scene change (the "rotate a page" row in the table above), and fit-modes re-resolve against the swapped footprint.

Annotation tools that place content with a reading orientation (stamp, free-text) counter-rotate against the total display rotation — /Rotate + viewRotation — via their upright option, so what the author places reads horizontally exactly as they see it.

Recipes

Document reading — the defaults. Configure nothing.

Presentation / construction sheets — every move keeps the current sheet centered; each arrival presents the sheet like a slide:

stagePlugin({
  arrivalAlign: { x: 'center', y: 'center' },
  zoomAlign: { x: 'center', y: 'center' },
  anchorAlign: { x: 'center', y: 'center' },
});

Zoomed-in reading of scanned two-column papers — hold the column while paging, land a comfortable third from the top:

stagePlugin({ arrivalAlign: { x: 'keep', y: 0.35 } });

A "preset" is just an object you keep and pass — to stagePlugin() at setup or stage.updateSettings() at runtime. The plugin ships no named presets; that taxonomy belongs to your product.

The scroller contract — scrollbars, minimaps, "% read"

The Stage doubles as a virtual scroll element. getScrollMetrics() projects the camera into the native DOM vocabulary — every field means exactly what it means on a <div>, in screen px. It lives on the host lens (@embedpdf/plugin-stage/contract/host), the same runtime capability typed wider, because a scrollbar is chrome that drives the camera:

import { StageToken } from '@embedpdf/plugin-stage/contract/host';

const stage = kernel.capability(StageToken);
const metrics = stage.getScrollMetrics();
// metrics.scrollTop / metrics.scrollLeft       — where you are
// metrics.scrollHeight / metrics.scrollWidth   — how much there is
// metrics.clientHeight / metrics.clientWidth   — how much you see
// metrics.scrollableY / metrics.scrollableX    — false = nothing to scroll (hide the bar)

stage.scrollTo({ top: 0 }); // Element.scrollTo semantics
stage.scrollBy({ top: metrics.clientHeight * 0.9 }); // page down

In React, useScrollMetrics() reads the same numbers reactively and <Scrollbar axis="y" /> draws a native-feeling bar from them (both from @embedpdf/react/stage).

That is the whole contract. A scrollbar thumb is clientHeight / scrollHeight of the track, positioned at scrollTop / scrollHeight; a reading-progress indicator is scrollTop / (scrollHeight − clientHeight); a minimap is the same numbers drawn small. You never touch camera math.

The metrics are derived from the same travel range the pan clamp uses, so a scrollbar can never disagree with where panning actually stops:

  • Zoom reshapes the range live — zoom in and the thumb shrinks, exactly like a longer document.
  • Paged flow scrolls the current item: the bar reflects one page (or spread), and hides when it fits.
  • A fitting axis reports scrollableX: false (or scrollableY) with scrollWidth === clientWidth — the native "no bar" condition, for free.
  • RTL stays physical: scrollLeft is the offset from the range's left edge, deliberately sidestepping the DOM's negative-scrollLeft behavior.

Unbounded stages get the Figma scrollbar. With bounded: false the range is the union of the content and your current view: pan off into empty canvas and scrollHeight grows, the thumb shrinking toward the edge — but dragging it to the other end always rides you back across the content. When everything is in view, both axes report unscrollable and the bars disappear.

scrollTo/scrollBy default to behavior: 'instant' (the DOM's 'auto'); pass 'smooth' for the camera tween. The scrollBehavior setting is not consulted — it governs navigation verbs (goToPage, nextPage), not scrolling.

What these settings never touch

  • Gestures. Pan and pinch hold the pointer. Physics, not policy.
  • Explicit arrivals. reveal(page, { rect, zoom, anchor }) — search hits, outline clicks, PDF destinations — carries its own anchor and always beats the settings. Bare reveal(page) stays minimal-movement: it scrolls only as far as needed to make the page visible, and not a pixel further.
  • Saved viewpoints. goToPage(page, { viewpoint }) restores exactly what was captured.

版本与发布记录

@embedpdf/plugin-stage 共发布 27 个版本,首次发布于 2026/08/06;过去 12 个月发布了 27 个版本。

版本发布日期距今
3.2.0latest2026/11/121 天前
3.1.02026/11/094 天前
3.0.02026/11/076 天前
3.0.0-next.23next2026/11/076 天前
3.0.0-next.222026/11/049 天前
3.0.0-next.212026/11/0310 天前

关于 @embedpdf/plugin-stage 的常见问题

@embedpdf/plugin-stage 是什么?

The Stage plugin: one coordinate core (scroll + viewport + zoom + pan + spread) built on @embedpdf/core-stage. Pure intents & selectors; no DOM.

如何安装 @embedpdf/plugin-stage?

在项目目录运行 npm install @embedpdf/plugin-stage,或使用 pnpm add / yarn add / bun add。

@embedpdf/plugin-stage 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@embedpdf/plugin-stage 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@embedpdf/plugin-stage 可以商用吗?

@embedpdf/plugin-stage 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@embedpdf/plugin-stage 的下载量有多少?

根据 npm 官方下载统计,@embedpdf/plugin-stage 在 2026/10/14 – 2026/11/12 这 30 天内被下载 66,483 次,日均约 2,216 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@embedpdf/plugin-stage 还在维护吗?

最新版本 3.2.0 发布于 2026/11/12(1 天前);至今共发布 27 个版本,其中过去 12 个月发布了 27 个;首次发布于 2026/08/06。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。