@embedpdf/plugin-stage 3.0.0-next.0 → 3.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +594 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +201 -49
- package/dist/index.d.ts +201 -49
- package/dist/index.js +590 -92
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,8 +1,151 @@
|
|
|
1
1
|
import { createCapabilityToken, definePlugin } from "@embedpdf/core";
|
|
2
|
-
import { InteractionToken } from "@embedpdf/plugin-interaction";
|
|
3
2
|
import * as S from "@embedpdf/core-stage";
|
|
4
3
|
import { ZoomMode, ZoomMode as ZoomMode$1 } from "@embedpdf/core-stage";
|
|
5
4
|
import { addRotations, applyPoint, applyRect, displaySize, intersectRects, pageGeometry, pageTransform, rotateScaleMatrix, snapToDevice } from "@embedpdf/core-geometry";
|
|
5
|
+
//#region src/motion.ts
|
|
6
|
+
/**
|
|
7
|
+
* The camera's MOTION vocabulary — every formula, pure.
|
|
8
|
+
*
|
|
9
|
+
* The capability owns the impure drivers (scheduler frames, dispatch, gesture
|
|
10
|
+
* state); the LAWS those drivers integrate live here, parameterized by dt/t
|
|
11
|
+
* with no state, no scheduler, no dispatch — unit-testable in isolation, and
|
|
12
|
+
* with zero plugin dependencies, liftable into stage-core if the native port
|
|
13
|
+
* ever wants them.
|
|
14
|
+
*
|
|
15
|
+
* Two families:
|
|
16
|
+
* • ballistic — one per-axis law for every free motion: `glideStep` (the
|
|
17
|
+
* touch fling, decaying on UIScrollView's curve) and `springStep` (a
|
|
18
|
+
* critically-damped return to the clamp). A glide that reaches a content
|
|
19
|
+
* edge converts to a spring with its velocity carried in, so the bounce
|
|
20
|
+
* grows out of physics rather than a scripted overshoot.
|
|
21
|
+
* • resistance — the iOS rubber-band curve `rubberOut` and its exact
|
|
22
|
+
* inverse `rubberIn`: dragging `d` px past an edge displays an
|
|
23
|
+
* asymptotically-bounded stretch, and re-entering a gesture over a
|
|
24
|
+
* stretched camera (the mid-bounce catch) reconstructs the
|
|
25
|
+
* finger-integrated position the curve came from.
|
|
26
|
+
* Plus the tween interpolators: `easeOutCubic` for coordinate tweens and
|
|
27
|
+
* `zoomLerp` — GEOMETRIC zoom interpolation (zoom is multiplicative; equal
|
|
28
|
+
* ratios per unit time, not equal deltas — the linear lerp's visible "dip").
|
|
29
|
+
*/
|
|
30
|
+
/** Resistance coefficient of the rubber-band curve (the iOS feel). */
|
|
31
|
+
const RUBBER = .55;
|
|
32
|
+
/** Glide decay per ms — UIScrollView's normal deceleration rate. */
|
|
33
|
+
const FLING_DECAY = .998;
|
|
34
|
+
/** rad/ms — critically damped, ~400ms to settle. */
|
|
35
|
+
const SPRING_OMEGA = .015;
|
|
36
|
+
/**
|
|
37
|
+
* Overshoot distance → displayed stretch: asymptotic to the viewport
|
|
38
|
+
* dimension `dim`, so the content never leaves the screen however far the
|
|
39
|
+
* finger travels. `rubberOut(0) === 0` — inside the bounds the curve is
|
|
40
|
+
* invisible.
|
|
41
|
+
*/
|
|
42
|
+
const rubberOut = (d, dim) => (1 - 1 / (d * RUBBER / dim + 1)) * dim;
|
|
43
|
+
/** Exact inverse of `rubberOut` (defined for `out < dim`). */
|
|
44
|
+
const rubberIn = (out, dim) => out * dim / (RUBBER * (dim - out));
|
|
45
|
+
/**
|
|
46
|
+
* One frame of free glide: exponential velocity decay, position advanced by
|
|
47
|
+
* the midpoint integral of the decay over `dt` (exact for the exponential —
|
|
48
|
+
* frame-rate independent).
|
|
49
|
+
*/
|
|
50
|
+
const glideStep = (p, v, dt) => {
|
|
51
|
+
const k = Math.pow(FLING_DECAY, dt);
|
|
52
|
+
const np = p + v * (dt * (1 + k) / 2);
|
|
53
|
+
const nv = v * k;
|
|
54
|
+
return Math.abs(nv) < .02 ? {
|
|
55
|
+
p: np,
|
|
56
|
+
v: 0,
|
|
57
|
+
done: true
|
|
58
|
+
} : {
|
|
59
|
+
p: np,
|
|
60
|
+
v: nv,
|
|
61
|
+
done: false
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* One frame of critically-damped spring toward `edge` — semi-implicit Euler
|
|
66
|
+
* on x'' = −ω²(x−e) − 2ωx'. Lands exactly ON the edge (position snapped,
|
|
67
|
+
* velocity zeroed) once inside half a pixel at negligible speed.
|
|
68
|
+
*/
|
|
69
|
+
const springStep = (p, v, edge, dt) => {
|
|
70
|
+
const nv = v + (-.015 * SPRING_OMEGA * (p - edge) - 2 * SPRING_OMEGA * v) * dt;
|
|
71
|
+
const np = p + nv * dt;
|
|
72
|
+
if (Math.abs(np - edge) < .5 && Math.abs(nv) < .01) return {
|
|
73
|
+
p: edge,
|
|
74
|
+
v: 0,
|
|
75
|
+
done: true
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
p: np,
|
|
79
|
+
v: nv,
|
|
80
|
+
done: false
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
|
|
84
|
+
/**
|
|
85
|
+
* Geometric zoom interpolation: `z0·(z1/z0)^k`. Zoom is multiplicative, so a
|
|
86
|
+
* constant-RATE zoom moves equal ratios per unit time; interpolating the
|
|
87
|
+
* level linearly instead makes the start rush and the end crawl (and, under
|
|
88
|
+
* a focal anchor, swings the anchored point along a curved path).
|
|
89
|
+
*/
|
|
90
|
+
const zoomLerp = (z0, z1, k) => z0 * Math.pow(z1 / z0, k);
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/responsive.ts
|
|
93
|
+
/**
|
|
94
|
+
* Container queries for the settings bag — the pure half. The capability owns
|
|
95
|
+
* the driver (when to re-resolve, how to react); the LAWS live here: what a
|
|
96
|
+
* box is, when a query matches, and what the effective settings are. No state,
|
|
97
|
+
* no dispatch — property-testable in isolation (the `wheel.ts` convention).
|
|
98
|
+
*
|
|
99
|
+
* The query vocabulary is deliberately just the box: a headless, DOM-free
|
|
100
|
+
* stage knows exactly one environmental fact — the viewport it was told.
|
|
101
|
+
* Anything else (pointer coarseness, platform, app state) is either per-event
|
|
102
|
+
* (`PointerSample.pointerType`) or the app's own business via `update()`.
|
|
103
|
+
*/
|
|
104
|
+
/** The reported viewport as a queryable box. A square box is 'portrait' (CSS). */
|
|
105
|
+
const boxOf = (vp) => ({
|
|
106
|
+
width: vp.width,
|
|
107
|
+
height: vp.height,
|
|
108
|
+
orientation: vp.height >= vp.width ? "portrait" : "landscape"
|
|
109
|
+
});
|
|
110
|
+
/** All bounds inclusive, all fields optional, conditions AND-ed (CSS ranges). */
|
|
111
|
+
const matchesQuery = (q, box) => (q.minWidth === void 0 || box.width >= q.minWidth) && (q.maxWidth === void 0 || box.width <= q.maxWidth) && (q.minHeight === void 0 || box.height >= q.minHeight) && (q.maxHeight === void 0 || box.height <= q.maxHeight) && (q.orientation === void 0 || box.orientation === q.orientation);
|
|
112
|
+
const ruleMatches = (rule, box) => typeof rule.when === "function" ? !!rule.when(box) : matchesQuery(rule.when, box);
|
|
113
|
+
/** Merge a patch over settings, skipping undefined values (Partial semantics). */
|
|
114
|
+
const mergeSettings = (into, patch) => {
|
|
115
|
+
const out = { ...into };
|
|
116
|
+
let key;
|
|
117
|
+
for (key in patch) {
|
|
118
|
+
const value = patch[key];
|
|
119
|
+
if (value !== void 0) Object.assign(out, { [key]: value });
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Setting-value equality: primitives by identity, the flat objects the
|
|
125
|
+
* settings vocabulary uses (`{ px }`, `{ x, y }`, page frames, zoom specs) by
|
|
126
|
+
* one level of own-key comparison. Settings values are never nested deeper.
|
|
127
|
+
*/
|
|
128
|
+
const eqSetting = (a, b) => {
|
|
129
|
+
if (a === b) return true;
|
|
130
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
131
|
+
const ka = Object.keys(a);
|
|
132
|
+
const kb = Object.keys(b);
|
|
133
|
+
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
134
|
+
};
|
|
135
|
+
const resolveResponsive = (base, rules, box) => {
|
|
136
|
+
let effective = base;
|
|
137
|
+
const active = [];
|
|
138
|
+
for (const rule of rules) {
|
|
139
|
+
if (!ruleMatches(rule, box)) continue;
|
|
140
|
+
if (rule.name !== void 0) active.push(rule.name);
|
|
141
|
+
if (rule.settings) effective = mergeSettings(effective, rule.settings);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
effective,
|
|
145
|
+
active
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
6
149
|
//#region src/settings.ts
|
|
7
150
|
/**
|
|
8
151
|
* Out-of-the-box defaults — a sensible document-reading feel. They are JUST
|
|
@@ -48,6 +191,18 @@ const DEFAULT_SETTINGS = {
|
|
|
48
191
|
scrollBehavior: "smooth",
|
|
49
192
|
viewUnitsPerPoint: 96 / 72
|
|
50
193
|
};
|
|
194
|
+
/**
|
|
195
|
+
* Out-of-the-box responsive rules — the platform feel with zero configuration:
|
|
196
|
+
* a compact container gets the thin gutter phones use (space, not device — an
|
|
197
|
+
* embedded 500px pane on a desktop is compact too). Override the whole list
|
|
198
|
+
* with `stagePlugin({ responsive: [...] })`; `responsive: []` opts out. The
|
|
199
|
+
* 'compact' name is queryable app-side via `stage.matches('compact')`.
|
|
200
|
+
*/
|
|
201
|
+
const DEFAULT_RESPONSIVE = [{
|
|
202
|
+
name: "compact",
|
|
203
|
+
when: { maxWidth: 600 },
|
|
204
|
+
settings: { padding: 4 }
|
|
205
|
+
}];
|
|
51
206
|
const SETTINGS_EFFECT = {
|
|
52
207
|
flow: "reflow",
|
|
53
208
|
layout: "scene",
|
|
@@ -86,8 +241,9 @@ const settingsEqual = (a, b) => SETTING_KEYS.every((k) => valueEq(a[k], b[k]));
|
|
|
86
241
|
* Layering, stated honestly:
|
|
87
242
|
* • stage-core — PURE spatial math (no DOM, no time). Ports to Rust later.
|
|
88
243
|
* • this capability — the IMPURE platform shell. It dispatches, caches, and owns
|
|
89
|
-
* the camera tween. It is NOT pure;
|
|
90
|
-
*
|
|
244
|
+
* the camera tween. It is NOT pure; its one host dependency (frame timing)
|
|
245
|
+
* enters through the Scheduler seam — injectable, defaulting to the host's
|
|
246
|
+
* own frame clock when one exists (see the seam below).
|
|
91
247
|
*
|
|
92
248
|
* The model: EVERY camera move is defined by what it holds fixed.
|
|
93
249
|
* • gestures (pan/pinch/wheel) — the content under the pointer. Physics, no setting.
|
|
@@ -373,12 +529,100 @@ function createStageCapability(ctx, config = {}) {
|
|
|
373
529
|
fitAlign: ctx.getState().fitAlign,
|
|
374
530
|
direction: ctx.getState().direction
|
|
375
531
|
});
|
|
532
|
+
let placementStarted = false;
|
|
533
|
+
let gestureDepth = 0;
|
|
534
|
+
let gestureZoomed = false;
|
|
535
|
+
let gestureElastic = false;
|
|
536
|
+
let gestureRaw = null;
|
|
537
|
+
const REST_MS = 150;
|
|
538
|
+
let restRaf = 0;
|
|
539
|
+
const armRest = () => {
|
|
540
|
+
if (!canAnimate || !placementStarted) return;
|
|
541
|
+
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
542
|
+
type: "CAMERA_REST",
|
|
543
|
+
resting: false
|
|
544
|
+
});
|
|
545
|
+
if (restRaf) scheduler.caf(restRaf);
|
|
546
|
+
let t0 = 0;
|
|
547
|
+
const tick = (ts) => {
|
|
548
|
+
restRaf = 0;
|
|
549
|
+
if (!t0) t0 = ts;
|
|
550
|
+
if (ts - t0 >= REST_MS) {
|
|
551
|
+
ctx.dispatch({
|
|
552
|
+
type: "CAMERA_REST",
|
|
553
|
+
resting: true
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
restRaf = scheduler.raf(tick);
|
|
558
|
+
};
|
|
559
|
+
restRaf = scheduler.raf(tick);
|
|
560
|
+
};
|
|
376
561
|
const setCam = (next, bounds = stayBounds()) => {
|
|
562
|
+
const clamped = S.clampCamera(next, bounds, vp(), constraint());
|
|
563
|
+
if (clamped.zoom !== cam().zoom) if (gestureDepth > 0) {
|
|
564
|
+
gestureZoomed = true;
|
|
565
|
+
if (ctx.getState().cameraResting) ctx.dispatch({
|
|
566
|
+
type: "CAMERA_REST",
|
|
567
|
+
resting: false
|
|
568
|
+
});
|
|
569
|
+
if (restRaf) {
|
|
570
|
+
scheduler.caf(restRaf);
|
|
571
|
+
restRaf = 0;
|
|
572
|
+
}
|
|
573
|
+
} else armRest();
|
|
377
574
|
ctx.dispatch({
|
|
378
575
|
type: "CAMERA",
|
|
379
|
-
camera:
|
|
576
|
+
camera: clamped
|
|
380
577
|
});
|
|
381
578
|
};
|
|
579
|
+
const axisTravels = (origin, content, view, zoom) => !S.travelRange(origin, content, view, zoom, constraint().padding).fits;
|
|
580
|
+
/** Unclamped finger-integrated camera → the DISPLAYED camera: clamp, then
|
|
581
|
+
* re-apply the overshoot through the resistance curve, per travelling axis.
|
|
582
|
+
* Inside the bounds this is exactly the clamp (rubber of zero is zero);
|
|
583
|
+
* on a fitting axis it is exactly the clamp ALWAYS. */
|
|
584
|
+
const rubberize = (raw) => {
|
|
585
|
+
const clamped = S.clampCamera(raw, stayBounds(), vp(), constraint());
|
|
586
|
+
const v = vp();
|
|
587
|
+
const b = stayBounds();
|
|
588
|
+
const axis = (rawA, clampedA, dim, travels) => {
|
|
589
|
+
if (!travels) return clampedA;
|
|
590
|
+
const dWorld = rawA - clampedA;
|
|
591
|
+
if (dWorld === 0) return clampedA;
|
|
592
|
+
const out = rubberOut(Math.abs(dWorld) * raw.zoom, Math.max(1, dim));
|
|
593
|
+
return clampedA + Math.sign(dWorld) * out / raw.zoom;
|
|
594
|
+
};
|
|
595
|
+
return {
|
|
596
|
+
zoom: raw.zoom,
|
|
597
|
+
x: axis(raw.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, raw.zoom)),
|
|
598
|
+
y: axis(raw.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, raw.zoom))
|
|
599
|
+
};
|
|
600
|
+
};
|
|
601
|
+
/** Displayed camera → the raw position `rubberize` would have produced it
|
|
602
|
+
* from. Overshoot is capped just under the asymptote so the inverse stays
|
|
603
|
+
* finite whatever state a catch finds the camera in. */
|
|
604
|
+
const unrubberize = (displayed) => {
|
|
605
|
+
const clamped = S.clampCamera(displayed, stayBounds(), vp(), constraint());
|
|
606
|
+
const v = vp();
|
|
607
|
+
const b = stayBounds();
|
|
608
|
+
const axis = (dispA, clampedA, dim, travels) => {
|
|
609
|
+
if (!travels) return clampedA;
|
|
610
|
+
const dWorld = dispA - clampedA;
|
|
611
|
+
if (dWorld === 0) return clampedA;
|
|
612
|
+
const d = Math.max(1, dim);
|
|
613
|
+
const out = Math.min(Math.abs(dWorld) * displayed.zoom, d - 1);
|
|
614
|
+
return clampedA + Math.sign(dWorld) * rubberIn(out, d) / displayed.zoom;
|
|
615
|
+
};
|
|
616
|
+
return {
|
|
617
|
+
zoom: displayed.zoom,
|
|
618
|
+
x: axis(displayed.x, clamped.x, v.width, axisTravels(b.x, b.width, v.width, displayed.zoom)),
|
|
619
|
+
y: axis(displayed.y, clamped.y, v.height, axisTravels(b.y, b.height, v.height, displayed.zoom))
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
const setCamRaw = (c) => ctx.dispatch({
|
|
623
|
+
type: "CAMERA",
|
|
624
|
+
camera: c
|
|
625
|
+
});
|
|
382
626
|
/**
|
|
383
627
|
* Cursor reconciliation, one direction per interaction:
|
|
384
628
|
* navigation → cursor is INTENT, set explicitly; the camera honors it as far
|
|
@@ -412,7 +656,6 @@ function createStageCapability(ctx, config = {}) {
|
|
|
412
656
|
}
|
|
413
657
|
cancelAnim();
|
|
414
658
|
const from = cam();
|
|
415
|
-
const ease = (t) => 1 - Math.pow(1 - t, 3);
|
|
416
659
|
let started = false;
|
|
417
660
|
let t0 = 0;
|
|
418
661
|
const tick = (now) => {
|
|
@@ -420,7 +663,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
420
663
|
started = true;
|
|
421
664
|
t0 = now;
|
|
422
665
|
}
|
|
423
|
-
const k =
|
|
666
|
+
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
424
667
|
setCam({
|
|
425
668
|
x: lerp(from.x, target.x, k),
|
|
426
669
|
y: lerp(from.y, target.y, k),
|
|
@@ -434,6 +677,116 @@ function createStageCapability(ctx, config = {}) {
|
|
|
434
677
|
};
|
|
435
678
|
raf = scheduler.raf(tick);
|
|
436
679
|
};
|
|
680
|
+
/**
|
|
681
|
+
* Zoom-anchored tween — animates THE INVARIANT, not the coordinates. A
|
|
682
|
+
* camera move is defined by what it holds fixed; for an anchored zoom that
|
|
683
|
+
* is "the focal page-point stays at `pt` while scale changes". Lerping
|
|
684
|
+
* x/y/zoom independently (the plain tween) breaks that mid-flight: holding
|
|
685
|
+
* a screen point requires `x(t) = focal.x − pt.x / zoom(t)` — hyperbolic in
|
|
686
|
+
* the zoom — so linear coordinates swing the tapped point along a curved
|
|
687
|
+
* path (the visible "dip"). Here the zoom interpolates GEOMETRICALLY
|
|
688
|
+
* (constant rate — zoom is multiplicative) and each frame's camera derives
|
|
689
|
+
* from the anchor at that zoom, then clamps: the focal point is stationary
|
|
690
|
+
* by construction, and a zoom-out rails into its fit progressively as the
|
|
691
|
+
* shrinking travel range lets the clamp take over.
|
|
692
|
+
*/
|
|
693
|
+
const animateZoomAnchored = (focal, pt, targetZoom, ms = 240, then) => {
|
|
694
|
+
if (!canAnimate) {
|
|
695
|
+
const sc = buildScene();
|
|
696
|
+
if (sc.itemCount) setCam(S.cameraForAnchorAtScreen(focal, sc, pt, targetZoom));
|
|
697
|
+
then?.();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
cancelAnim();
|
|
701
|
+
const z0 = cam().zoom;
|
|
702
|
+
let started = false;
|
|
703
|
+
let t0 = 0;
|
|
704
|
+
const tick = (now) => {
|
|
705
|
+
if (!started) {
|
|
706
|
+
started = true;
|
|
707
|
+
t0 = now;
|
|
708
|
+
}
|
|
709
|
+
const k = easeOutCubic(Math.min(1, (now - t0) / ms));
|
|
710
|
+
const z = zoomLerp(z0, targetZoom, k);
|
|
711
|
+
const sc = buildScene();
|
|
712
|
+
if (sc.itemCount) setCam(S.cameraForAnchorAtScreen(focal, sc, pt, z));
|
|
713
|
+
if (k < 1) raf = scheduler.raf(tick);
|
|
714
|
+
else {
|
|
715
|
+
raf = 0;
|
|
716
|
+
then?.();
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
raf = scheduler.raf(tick);
|
|
720
|
+
};
|
|
721
|
+
const startCameraPhysics = (vxFinger, vyFinger) => {
|
|
722
|
+
if (!canAnimate) {
|
|
723
|
+
setCam(cam());
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
cancelAnim();
|
|
727
|
+
const zoom = cam().zoom;
|
|
728
|
+
const c0 = cam();
|
|
729
|
+
const cl0 = S.clampCamera(c0, stayBounds(), vp(), constraint());
|
|
730
|
+
const b0 = stayBounds();
|
|
731
|
+
const v0 = vp();
|
|
732
|
+
const mk = (camA, clampedA, fingerV) => ({
|
|
733
|
+
p: camA * zoom,
|
|
734
|
+
v: -fingerV / 1e3,
|
|
735
|
+
springing: camA !== clampedA,
|
|
736
|
+
done: false
|
|
737
|
+
});
|
|
738
|
+
const ax = mk(c0.x, cl0.x, axisTravels(b0.x, b0.width, v0.width, zoom) ? vxFinger : 0);
|
|
739
|
+
const ay = mk(c0.y, cl0.y, axisTravels(b0.y, b0.height, v0.height, zoom) ? vyFinger : 0);
|
|
740
|
+
if (!ax.springing && !ay.springing && Math.hypot(ax.v, ay.v) < .02) return;
|
|
741
|
+
let started = false;
|
|
742
|
+
let last = 0;
|
|
743
|
+
const tick = (now) => {
|
|
744
|
+
raf = 0;
|
|
745
|
+
const dt = started ? Math.min(64, Math.max(1, now - last)) : 16;
|
|
746
|
+
started = true;
|
|
747
|
+
last = now;
|
|
748
|
+
const cl = S.clampCamera({
|
|
749
|
+
x: ax.p / zoom,
|
|
750
|
+
y: ay.p / zoom,
|
|
751
|
+
zoom
|
|
752
|
+
}, stayBounds(), vp(), constraint());
|
|
753
|
+
const step = (a, edgeWorld) => {
|
|
754
|
+
if (a.done) return;
|
|
755
|
+
const r = a.springing ? springStep(a.p, a.v, edgeWorld * zoom, dt) : glideStep(a.p, a.v, dt);
|
|
756
|
+
a.p = r.p;
|
|
757
|
+
a.v = r.v;
|
|
758
|
+
a.done = r.done;
|
|
759
|
+
};
|
|
760
|
+
step(ax, cl.x);
|
|
761
|
+
step(ay, cl.y);
|
|
762
|
+
const ncl = S.clampCamera({
|
|
763
|
+
x: ax.p / zoom,
|
|
764
|
+
y: ay.p / zoom,
|
|
765
|
+
zoom
|
|
766
|
+
}, stayBounds(), vp(), constraint());
|
|
767
|
+
const convert = (a, clampedA) => {
|
|
768
|
+
if (!a.springing && Math.abs(clampedA * zoom - a.p) > 1e-6) {
|
|
769
|
+
a.p = clampedA * zoom;
|
|
770
|
+
a.springing = true;
|
|
771
|
+
a.done = false;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
convert(ax, ncl.x);
|
|
775
|
+
convert(ay, ncl.y);
|
|
776
|
+
setCamRaw({
|
|
777
|
+
x: ax.p / zoom,
|
|
778
|
+
y: ay.p / zoom,
|
|
779
|
+
zoom
|
|
780
|
+
});
|
|
781
|
+
syncCursorFromCamera();
|
|
782
|
+
if (ax.done && ay.done) {
|
|
783
|
+
setCam(cam());
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
raf = scheduler.raf(tick);
|
|
787
|
+
};
|
|
788
|
+
raf = scheduler.raf(tick);
|
|
789
|
+
};
|
|
437
790
|
const anchorAt = (at) => {
|
|
438
791
|
const sc = buildScene();
|
|
439
792
|
return sc.itemCount ? S.anchorFromCamera(cam(), sc, vp(), at) : {
|
|
@@ -561,8 +914,11 @@ function createStageCapability(ctx, config = {}) {
|
|
|
561
914
|
} : unrotatedPoints(box);
|
|
562
915
|
const c = cam();
|
|
563
916
|
const ratio = dpr();
|
|
564
|
-
const
|
|
565
|
-
const
|
|
917
|
+
const rawX = (box.x - c.x) * c.zoom;
|
|
918
|
+
const rawY = (box.y - c.y) * c.zoom;
|
|
919
|
+
const resting = ctx.getState().cameraResting;
|
|
920
|
+
const screenX = resting ? snapToDevice(rawX, ratio) : rawX;
|
|
921
|
+
const screenY = resting ? snapToDevice(rawY, ratio) : rawY;
|
|
566
922
|
const transform = pageTransform({
|
|
567
923
|
pageSize,
|
|
568
924
|
rotation: box.rotation,
|
|
@@ -582,7 +938,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
582
938
|
width: transform.viewWidth,
|
|
583
939
|
height: transform.viewHeight
|
|
584
940
|
});
|
|
585
|
-
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.
|
|
941
|
+
const visibleRect = onScreen.width > 0 && onScreen.height > 0 ? transform.viewToContentRect(onScreen) : {
|
|
586
942
|
x: 0,
|
|
587
943
|
y: 0,
|
|
588
944
|
width: 0,
|
|
@@ -597,8 +953,19 @@ function createStageCapability(ctx, config = {}) {
|
|
|
597
953
|
visibleRect
|
|
598
954
|
};
|
|
599
955
|
};
|
|
956
|
+
const EMPTY_SCROLL_METRICS = {
|
|
957
|
+
scrollLeft: 0,
|
|
958
|
+
scrollTop: 0,
|
|
959
|
+
scrollWidth: 0,
|
|
960
|
+
scrollHeight: 0,
|
|
961
|
+
clientWidth: 0,
|
|
962
|
+
clientHeight: 0,
|
|
963
|
+
scrollableX: false,
|
|
964
|
+
scrollableY: false
|
|
965
|
+
};
|
|
600
966
|
let scrollMemo = null;
|
|
601
967
|
const scrollMetricsNow = () => {
|
|
968
|
+
if (!ctx.getState().placed) return EMPTY_SCROLL_METRICS;
|
|
602
969
|
const sc = buildScene();
|
|
603
970
|
const m = S.scrollMetrics(cam(), sc.itemCount ? stayBounds() : {
|
|
604
971
|
x: 0,
|
|
@@ -611,14 +978,16 @@ function createStageCapability(ctx, config = {}) {
|
|
|
611
978
|
scrollMemo = m;
|
|
612
979
|
return m;
|
|
613
980
|
};
|
|
981
|
+
const EMPTY_VISIBLE_PAGES = [];
|
|
614
982
|
let visSig = "";
|
|
615
983
|
let vis = [];
|
|
616
984
|
const visiblePages = () => {
|
|
985
|
+
if (!ctx.getState().placed) return EMPTY_VISIBLE_PAGES;
|
|
617
986
|
const c = cam();
|
|
618
987
|
const v = vp();
|
|
619
988
|
const sc = buildScene();
|
|
620
989
|
const items = paged() ? sc.items.slice(0, 1) : sc.query(S.cameraWorldRect(c, v));
|
|
621
|
-
const sig = `${sceneCache.key}|${ctx.getState().flow}|${c.x},${c.y},${c.zoom}|${v.width}x${v.height}|${dpr()}`;
|
|
990
|
+
const sig = `${sceneCache.key}|${ctx.getState().flow}|${c.x},${c.y},${c.zoom}|${v.width}x${v.height}|${dpr()}|r${ctx.getState().cameraResting ? 1 : 0}`;
|
|
622
991
|
if (sig === visSig) return vis;
|
|
623
992
|
visSig = sig;
|
|
624
993
|
vis = items.flatMap((it) => it.pages).map(withTransform);
|
|
@@ -630,8 +999,73 @@ function createStageCapability(ctx, config = {}) {
|
|
|
630
999
|
return out;
|
|
631
1000
|
};
|
|
632
1001
|
const snapshotSettings = () => pickSettings(ctx.getState());
|
|
1002
|
+
/**
|
|
1003
|
+
* Settings change, reacted per the registry — the strongest effect among the
|
|
1004
|
+
* touched settings wins: 'reflow' ⊃ 'scene'/'refit' ⊃ 'reclamp' ⊃ 'none'.
|
|
1005
|
+
* The ONE reactive applier: the public `update()` and the responsive driver
|
|
1006
|
+
* both land here, so a breakpoint crossing gets exactly the invariant a
|
|
1007
|
+
* hand-written update would.
|
|
1008
|
+
*/
|
|
1009
|
+
const applySettings = (patch) => {
|
|
1010
|
+
cancelAnim();
|
|
1011
|
+
const touched = (effect) => SETTING_KEYS.some((k) => patch[k] !== void 0 && SETTINGS_EFFECT[k] === effect);
|
|
1012
|
+
const align = touched("refit") && !touched("scene") && !touched("reflow") ? ctx.getState().zoomAlign : ctx.getState().anchorAlign;
|
|
1013
|
+
const anchor = anchorAt(alignPoint(align, vp()));
|
|
1014
|
+
ctx.dispatch({
|
|
1015
|
+
type: "PATCH",
|
|
1016
|
+
patch
|
|
1017
|
+
});
|
|
1018
|
+
if (touched("scene")) sceneCache = null;
|
|
1019
|
+
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1020
|
+
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
1021
|
+
else if (touched("reclamp")) setCam(cam());
|
|
1022
|
+
};
|
|
1023
|
+
let base = snapshotSettings();
|
|
1024
|
+
let rules = config.responsive ?? DEFAULT_RESPONSIVE;
|
|
1025
|
+
let lastEffective = base;
|
|
1026
|
+
const publishActive = (active) => {
|
|
1027
|
+
const prev = ctx.getState().activeRules;
|
|
1028
|
+
if (prev.length !== active.length || active.some((n, i) => n !== prev[i])) ctx.dispatch({
|
|
1029
|
+
type: "RESPONSIVE",
|
|
1030
|
+
active
|
|
1031
|
+
});
|
|
1032
|
+
};
|
|
1033
|
+
/**
|
|
1034
|
+
* Re-resolve and apply what CHANGED since the last resolution. `react: true`
|
|
1035
|
+
* routes the patch through `applySettings` (full anchor-preserving update);
|
|
1036
|
+
* `react: false` (inside `setViewport`, whose caller runs its own reframe)
|
|
1037
|
+
* just lands the patch + cache invalidation and reports the strongest
|
|
1038
|
+
* effect so the caller can escalate a rule-driven flow flip.
|
|
1039
|
+
*/
|
|
1040
|
+
const syncResponsive = (react) => {
|
|
1041
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1042
|
+
const patch = {};
|
|
1043
|
+
for (const k of SETTING_KEYS) if (!eqSetting(effective[k], lastEffective[k])) Object.assign(patch, { [k]: effective[k] });
|
|
1044
|
+
lastEffective = effective;
|
|
1045
|
+
publishActive(active);
|
|
1046
|
+
const keys = Object.keys(patch);
|
|
1047
|
+
if (keys.length === 0) return "none";
|
|
1048
|
+
if (react) {
|
|
1049
|
+
applySettings(patch);
|
|
1050
|
+
return "none";
|
|
1051
|
+
}
|
|
1052
|
+
ctx.dispatch({
|
|
1053
|
+
type: "PATCH",
|
|
1054
|
+
patch
|
|
1055
|
+
});
|
|
1056
|
+
const rank = {
|
|
1057
|
+
none: 0,
|
|
1058
|
+
reclamp: 1,
|
|
1059
|
+
refit: 2,
|
|
1060
|
+
scene: 3,
|
|
1061
|
+
reflow: 4
|
|
1062
|
+
};
|
|
1063
|
+
let strongest = "none";
|
|
1064
|
+
for (const k of keys) if (rank[SETTINGS_EFFECT[k]] > rank[strongest]) strongest = SETTINGS_EFFECT[k];
|
|
1065
|
+
if (strongest === "scene" || strongest === "reflow") sceneCache = null;
|
|
1066
|
+
return strongest;
|
|
1067
|
+
};
|
|
633
1068
|
const initialViewProviders = [];
|
|
634
|
-
let hasPlaced = false;
|
|
635
1069
|
const api = {
|
|
636
1070
|
camera: cam,
|
|
637
1071
|
viewport: vp,
|
|
@@ -649,6 +1083,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
649
1083
|
label: p.label ?? null
|
|
650
1084
|
})),
|
|
651
1085
|
pageRect: (pon) => {
|
|
1086
|
+
if (!ctx.getState().placed) return null;
|
|
652
1087
|
const meta = ctx.document();
|
|
653
1088
|
const index = meta ? meta.pages.findIndex((p) => p.pageObjectNumber === pon) : -1;
|
|
654
1089
|
if (index < 0) return null;
|
|
@@ -663,7 +1098,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
663
1098
|
const ly = screen.y - p.screenY;
|
|
664
1099
|
if (lx >= 0 && ly >= 0 && lx <= p.transform.viewWidth && ly <= p.transform.viewHeight) return {
|
|
665
1100
|
pon: p.pon,
|
|
666
|
-
point: p.transform.
|
|
1101
|
+
point: p.transform.viewToContent({
|
|
667
1102
|
x: lx,
|
|
668
1103
|
y: ly
|
|
669
1104
|
}),
|
|
@@ -677,7 +1112,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
677
1112
|
pointOnPage: (pon, screen) => {
|
|
678
1113
|
const p = visiblePages().find((v) => v.pon === pon);
|
|
679
1114
|
if (!p) return null;
|
|
680
|
-
return p.transform.
|
|
1115
|
+
return p.transform.viewToContent({
|
|
681
1116
|
x: screen.x - p.screenX,
|
|
682
1117
|
y: screen.y - p.screenY
|
|
683
1118
|
});
|
|
@@ -749,11 +1184,12 @@ function createStageCapability(ctx, config = {}) {
|
|
|
749
1184
|
anchor: currentAnchor()
|
|
750
1185
|
}),
|
|
751
1186
|
setViewport: (v) => {
|
|
752
|
-
if (!
|
|
1187
|
+
if (!placementStarted) {
|
|
753
1188
|
ctx.dispatch({
|
|
754
1189
|
type: "VP",
|
|
755
1190
|
vp: v
|
|
756
1191
|
});
|
|
1192
|
+
syncResponsive(false);
|
|
757
1193
|
if (v.width > 0 && v.height > 0 && (ctx.document()?.pageCount ?? 0) > 0) api.placeInitial();
|
|
758
1194
|
return;
|
|
759
1195
|
}
|
|
@@ -763,7 +1199,8 @@ function createStageCapability(ctx, config = {}) {
|
|
|
763
1199
|
type: "VP",
|
|
764
1200
|
vp: v
|
|
765
1201
|
});
|
|
766
|
-
|
|
1202
|
+
if (syncResponsive(false) === "reflow") goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
1203
|
+
else reapply(anchor);
|
|
767
1204
|
},
|
|
768
1205
|
setDevicePixelRatio: (ratio) => {
|
|
769
1206
|
if (ratio > 0 && ratio !== dpr()) ctx.dispatch({
|
|
@@ -778,7 +1215,10 @@ function createStageCapability(ctx, config = {}) {
|
|
|
778
1215
|
},
|
|
779
1216
|
panBy: (dx, dy) => {
|
|
780
1217
|
cancelAnim();
|
|
781
|
-
|
|
1218
|
+
if (gestureDepth > 0 && gestureElastic) {
|
|
1219
|
+
gestureRaw = S.panByScreen(gestureRaw ?? unrubberize(cam()), dx, dy);
|
|
1220
|
+
setCamRaw(rubberize(gestureRaw));
|
|
1221
|
+
} else setCam(S.panByScreen(cam(), dx, dy));
|
|
782
1222
|
syncCursorFromCamera();
|
|
783
1223
|
},
|
|
784
1224
|
scrollTo: (opts) => {
|
|
@@ -804,14 +1244,72 @@ function createStageCapability(ctx, config = {}) {
|
|
|
804
1244
|
const before = buildScene();
|
|
805
1245
|
const focal = before.itemCount ? S.anchorAtPoint(before, S.toWorld(cam(), pt)) : null;
|
|
806
1246
|
setCam(S.zoomAround(cam(), pt, factor));
|
|
807
|
-
ctx.dispatch({
|
|
1247
|
+
if (gestureDepth === 0) ctx.dispatch({
|
|
808
1248
|
type: "PATCH",
|
|
809
1249
|
patch: { zoom: { level: cam().zoom } }
|
|
810
1250
|
});
|
|
811
1251
|
const after = buildScene();
|
|
812
1252
|
if (after !== before && focal && after.itemCount) setCam(S.cameraForAnchorAtScreen(focal, after, pt, cam().zoom));
|
|
1253
|
+
if (gestureDepth > 0 && gestureElastic) gestureRaw = unrubberize(cam());
|
|
813
1254
|
syncCursorFromCamera();
|
|
814
1255
|
},
|
|
1256
|
+
beginGesture: (options) => {
|
|
1257
|
+
gestureDepth++;
|
|
1258
|
+
if (gestureDepth === 1) {
|
|
1259
|
+
cancelAnim();
|
|
1260
|
+
gestureZoomed = false;
|
|
1261
|
+
gestureElastic = options?.elastic === true;
|
|
1262
|
+
gestureRaw = null;
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
endGesture: () => {
|
|
1266
|
+
if (gestureDepth === 0) return;
|
|
1267
|
+
gestureDepth--;
|
|
1268
|
+
if (gestureDepth > 0) return;
|
|
1269
|
+
gestureRaw = null;
|
|
1270
|
+
const wasElastic = gestureElastic;
|
|
1271
|
+
gestureElastic = false;
|
|
1272
|
+
if (gestureZoomed) {
|
|
1273
|
+
gestureZoomed = false;
|
|
1274
|
+
ctx.dispatch({
|
|
1275
|
+
type: "PATCH",
|
|
1276
|
+
patch: { zoom: { level: cam().zoom } }
|
|
1277
|
+
});
|
|
1278
|
+
armRest();
|
|
1279
|
+
}
|
|
1280
|
+
syncCursorFromCamera();
|
|
1281
|
+
if (wasElastic) {
|
|
1282
|
+
const c = cam();
|
|
1283
|
+
const cl = S.clampCamera(c, stayBounds(), vp(), constraint());
|
|
1284
|
+
if (cl.x !== c.x || cl.y !== c.y) startCameraPhysics(0, 0);
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
fling: (vx, vy) => startCameraPhysics(vx, vy),
|
|
1288
|
+
cameraInMotion: () => raf !== 0,
|
|
1289
|
+
doubleTapZoom: (pt) => {
|
|
1290
|
+
cancelAnim();
|
|
1291
|
+
const sc0 = buildScene();
|
|
1292
|
+
if (!sc0.itemCount) return;
|
|
1293
|
+
const item = paged() ? sc0.items[0] : sc0.items[itemIndexOfPage(ctx.getState().cursor)];
|
|
1294
|
+
const fit = fitBox(item);
|
|
1295
|
+
const auto = S.resolveZoom({ mode: S.ZoomMode.Automatic }, fit, vp(), pad());
|
|
1296
|
+
const stops = [
|
|
1297
|
+
auto,
|
|
1298
|
+
S.resolveZoom({ mode: S.ZoomMode.FitWidth }, fit, vp(), pad()),
|
|
1299
|
+
Math.min(auto * 2.5, S.ZOOM_MAX)
|
|
1300
|
+
].sort((a, b) => a - b).filter((z, i, all) => i === 0 || z > all[i - 1] * 1.1);
|
|
1301
|
+
const near = (z, stop) => z > stop / 1.1 && z < stop * 1.1;
|
|
1302
|
+
const onRung = stops.findIndex((s) => near(cam().zoom, s));
|
|
1303
|
+
const target = onRung >= 0 ? stops[(onRung + 1) % stops.length] : stops[0];
|
|
1304
|
+
ctx.dispatch({
|
|
1305
|
+
type: "PATCH",
|
|
1306
|
+
patch: { zoom: { level: target } }
|
|
1307
|
+
});
|
|
1308
|
+
const sc = buildScene();
|
|
1309
|
+
if (!sc.itemCount) return;
|
|
1310
|
+
const focal = S.anchorAtPoint(sc, S.toWorld(cam(), pt));
|
|
1311
|
+
animateZoomAnchored(focal, pt, target, 240, syncCursorFromCamera);
|
|
1312
|
+
},
|
|
815
1313
|
zoomIn: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1.2),
|
|
816
1314
|
zoomOut: () => api.zoomAround(alignPoint(ctx.getState().zoomAlign, vp()), 1 / 1.2),
|
|
817
1315
|
zoomTo: (spec) => api.update({ zoom: spec }),
|
|
@@ -820,7 +1318,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
820
1318
|
fitAll: () => api.update({ zoom: { mode: S.ZoomMode.FitAll } }),
|
|
821
1319
|
automatic: () => api.update({ zoom: { mode: S.ZoomMode.Automatic } }),
|
|
822
1320
|
refit: () => {
|
|
823
|
-
if (!
|
|
1321
|
+
if (!placementStarted) return;
|
|
824
1322
|
cancelAnim();
|
|
825
1323
|
reapply(currentAnchor());
|
|
826
1324
|
},
|
|
@@ -829,7 +1327,7 @@ function createStageCapability(ctx, config = {}) {
|
|
|
829
1327
|
const doc = ctx.document();
|
|
830
1328
|
if (!doc || doc.pageCount === 0) return;
|
|
831
1329
|
const target = Math.max(0, Math.min(pageIndex, doc.pageCount - 1));
|
|
832
|
-
if (!(!!opts && (opts.rect
|
|
1330
|
+
if (!(!!opts && (opts.rect != null || opts.anchor !== void 0 || opts.zoom !== void 0 && opts.zoom !== "keep"))) {
|
|
833
1331
|
if (paged()) {
|
|
834
1332
|
goToTarget(target, opts);
|
|
835
1333
|
return;
|
|
@@ -895,20 +1393,17 @@ function createStageCapability(ctx, config = {}) {
|
|
|
895
1393
|
},
|
|
896
1394
|
next: (opts) => step(1, opts),
|
|
897
1395
|
prev: (opts) => step(-1, opts),
|
|
1396
|
+
lensId: () => ctx.id,
|
|
898
1397
|
update: (patch) => {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
patch
|
|
906
|
-
});
|
|
907
|
-
if (touched("scene")) sceneCache = null;
|
|
908
|
-
if (touched("reflow")) goToTarget(ctx.getState().cursor, { behavior: "instant" });
|
|
909
|
-
else if (touched("scene") || touched("refit")) reapply(anchor, align);
|
|
910
|
-
else if (touched("reclamp")) setCam(cam());
|
|
1398
|
+
base = mergeSettings(base, patch);
|
|
1399
|
+
syncResponsive(true);
|
|
1400
|
+
},
|
|
1401
|
+
setResponsive: (next) => {
|
|
1402
|
+
rules = next;
|
|
1403
|
+
syncResponsive(true);
|
|
911
1404
|
},
|
|
1405
|
+
matches: (name) => ctx.getState().activeRules.includes(name),
|
|
1406
|
+
activeRules: () => ctx.getState().activeRules,
|
|
912
1407
|
setFlow: (flow) => api.update({ flow }),
|
|
913
1408
|
setLayout: (layout) => api.update({ layout }),
|
|
914
1409
|
setSpread: (spread) => api.update({ spread }),
|
|
@@ -928,9 +1423,13 @@ function createStageCapability(ctx, config = {}) {
|
|
|
928
1423
|
setScrollBehavior: (behavior) => api.update({ scrollBehavior: behavior }),
|
|
929
1424
|
applyViewState: (view) => {
|
|
930
1425
|
cancelAnim();
|
|
1426
|
+
base = mergeSettings(base, pickSettings(view));
|
|
1427
|
+
const { effective, active } = resolveResponsive(base, rules, boxOf(vp()));
|
|
1428
|
+
lastEffective = effective;
|
|
1429
|
+
publishActive(active);
|
|
931
1430
|
ctx.dispatch({
|
|
932
1431
|
type: "PATCH",
|
|
933
|
-
patch:
|
|
1432
|
+
patch: effective
|
|
934
1433
|
});
|
|
935
1434
|
ctx.dispatch({
|
|
936
1435
|
type: "CURSOR",
|
|
@@ -946,62 +1445,24 @@ function createStageCapability(ctx, config = {}) {
|
|
|
946
1445
|
});
|
|
947
1446
|
},
|
|
948
1447
|
placeInitial: () => {
|
|
949
|
-
|
|
1448
|
+
placementStarted = true;
|
|
950
1449
|
const sorted = [...initialViewProviders].sort((a, b) => b.priority - a.priority);
|
|
951
1450
|
for (const p of sorted) {
|
|
952
1451
|
const view = p.fn();
|
|
953
1452
|
if (view) {
|
|
954
1453
|
api.applyViewState(view);
|
|
1454
|
+
ctx.dispatch({ type: "PLACED" });
|
|
955
1455
|
return;
|
|
956
1456
|
}
|
|
957
1457
|
}
|
|
958
1458
|
api.resetView();
|
|
1459
|
+
ctx.dispatch({ type: "PLACED" });
|
|
959
1460
|
},
|
|
960
1461
|
resetView: () => goToTarget(0, { behavior: "instant" })
|
|
961
1462
|
};
|
|
962
1463
|
return api;
|
|
963
1464
|
}
|
|
964
1465
|
//#endregion
|
|
965
|
-
//#region src/scroll-handler.ts
|
|
966
|
-
/**
|
|
967
|
-
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
968
|
-
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
969
|
-
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
970
|
-
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
971
|
-
* it pans over the whole viewport (pages and gaps).
|
|
972
|
-
*
|
|
973
|
-
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
974
|
-
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
975
|
-
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
976
|
-
*/
|
|
977
|
-
function createScrollHandler(stage, interaction, options = {}) {
|
|
978
|
-
const panFallback = options.panFallback ?? true;
|
|
979
|
-
let last = {
|
|
980
|
-
x: 0,
|
|
981
|
-
y: 0
|
|
982
|
-
};
|
|
983
|
-
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
984
|
-
return {
|
|
985
|
-
id: "stage-scroll",
|
|
986
|
-
priority: 10,
|
|
987
|
-
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
988
|
-
onDown: (s) => {
|
|
989
|
-
if (!isPanTool() && s.page) return false;
|
|
990
|
-
last = s.viewport;
|
|
991
|
-
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
992
|
-
return true;
|
|
993
|
-
},
|
|
994
|
-
onMove: (s) => {
|
|
995
|
-
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
996
|
-
last = s.viewport;
|
|
997
|
-
},
|
|
998
|
-
onUp: () => interaction.setCursor("stage-grab", null),
|
|
999
|
-
onHover: (s) => {
|
|
1000
|
-
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
//#endregion
|
|
1005
1466
|
//#region src/effects.ts
|
|
1006
1467
|
/**
|
|
1007
1468
|
* The one effect a stage lens needs: when the document's page registry changes
|
|
@@ -1024,19 +1485,22 @@ function registerStageEffects(ctx, token) {
|
|
|
1024
1485
|
//#endregion
|
|
1025
1486
|
//#region src/reducer.ts
|
|
1026
1487
|
const initialStageState = (config) => {
|
|
1027
|
-
const { scheduler: _scheduler, ...overrides } = config;
|
|
1488
|
+
const { scheduler: _scheduler, responsive: _responsive, ...overrides } = config;
|
|
1028
1489
|
return {
|
|
1029
1490
|
camera: {
|
|
1030
1491
|
x: 0,
|
|
1031
1492
|
y: 0,
|
|
1032
1493
|
zoom: 1
|
|
1033
1494
|
},
|
|
1495
|
+
placed: false,
|
|
1496
|
+
cameraResting: true,
|
|
1034
1497
|
vp: {
|
|
1035
1498
|
width: 0,
|
|
1036
1499
|
height: 0
|
|
1037
1500
|
},
|
|
1038
1501
|
dpr: 1,
|
|
1039
1502
|
cursor: 0,
|
|
1503
|
+
activeRules: [],
|
|
1040
1504
|
...DEFAULT_SETTINGS,
|
|
1041
1505
|
...overrides
|
|
1042
1506
|
};
|
|
@@ -1062,6 +1526,14 @@ const stageReducer = (state, a) => {
|
|
|
1062
1526
|
...state,
|
|
1063
1527
|
camera: a.camera
|
|
1064
1528
|
};
|
|
1529
|
+
case "CAMERA_REST": return state.cameraResting === a.resting ? state : {
|
|
1530
|
+
...state,
|
|
1531
|
+
cameraResting: a.resting
|
|
1532
|
+
};
|
|
1533
|
+
case "PLACED": return state.placed ? state : {
|
|
1534
|
+
...state,
|
|
1535
|
+
placed: true
|
|
1536
|
+
};
|
|
1065
1537
|
case "VP": return {
|
|
1066
1538
|
...state,
|
|
1067
1539
|
vp: a.vp
|
|
@@ -1075,6 +1547,10 @@ const stageReducer = (state, a) => {
|
|
|
1075
1547
|
cursor: a.cursor
|
|
1076
1548
|
};
|
|
1077
1549
|
case "PATCH": return applyPatch(state, a.patch);
|
|
1550
|
+
case "RESPONSIVE": return {
|
|
1551
|
+
...state,
|
|
1552
|
+
activeRules: a.active
|
|
1553
|
+
};
|
|
1078
1554
|
default: return state;
|
|
1079
1555
|
}
|
|
1080
1556
|
};
|
|
@@ -1089,19 +1565,14 @@ const StageToken = createCapabilityToken("stage");
|
|
|
1089
1565
|
* sibling files.
|
|
1090
1566
|
*/
|
|
1091
1567
|
const stagePlugin = (options = {}) => {
|
|
1092
|
-
const { id = "stage", token = StageToken,
|
|
1568
|
+
const { id = "stage", token = StageToken, ...config } = options;
|
|
1093
1569
|
return definePlugin({
|
|
1094
1570
|
id,
|
|
1095
1571
|
token,
|
|
1096
1572
|
scope: "document",
|
|
1097
|
-
optional: interaction ? [InteractionToken] : void 0,
|
|
1098
1573
|
initialState: () => initialStageState(config),
|
|
1099
1574
|
reduce: stageReducer,
|
|
1100
1575
|
capability: (ctx) => createStageCapability(ctx, config),
|
|
1101
|
-
init: interaction ? (ctx) => {
|
|
1102
|
-
const ix = ctx.tryGet(InteractionToken);
|
|
1103
|
-
if (ix) ix.registerHandler(createScrollHandler(ctx.get(token), ix, { panFallback }));
|
|
1104
|
-
} : void 0,
|
|
1105
1576
|
effects: (ctx) => registerStageEffects(ctx, token)
|
|
1106
1577
|
});
|
|
1107
1578
|
};
|
|
@@ -1248,19 +1719,46 @@ function destinationToReveal(dest, layout, bbox) {
|
|
|
1248
1719
|
};
|
|
1249
1720
|
}
|
|
1250
1721
|
//#endregion
|
|
1251
|
-
//#region src/
|
|
1252
|
-
/**
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1722
|
+
//#region src/scroll-handler.ts
|
|
1723
|
+
/**
|
|
1724
|
+
* Pan-the-camera as an interaction handler. Live under the built-in `pan` tool
|
|
1725
|
+
* (the `scroll` tag) and — with `panFallback` — under every tool, where it then
|
|
1726
|
+
* captures ONLY page-gap drags (on-page downs belong to the active tool, which
|
|
1727
|
+
* claims them first at higher priority). It uses the sample's viewport delta, so
|
|
1728
|
+
* it pans over the whole viewport (pages and gaps).
|
|
1729
|
+
*
|
|
1730
|
+
* Cursor feel: a closed hand (`grabbing`) while dragging; an open hand (`grab`)
|
|
1731
|
+
* hovering a gap under a non-pan tool. The pan tool's own resting `grab` comes
|
|
1732
|
+
* from its `Tool.cursor`, so hovering it needs no claim here.
|
|
1733
|
+
*/
|
|
1734
|
+
function createScrollHandler(stage, interaction, options = {}) {
|
|
1735
|
+
const panFallback = options.panFallback ?? true;
|
|
1736
|
+
let last = {
|
|
1737
|
+
x: 0,
|
|
1738
|
+
y: 0
|
|
1739
|
+
};
|
|
1740
|
+
const isPanTool = () => interaction.activeTool().enables.has("scroll");
|
|
1741
|
+
return {
|
|
1742
|
+
id: "stage-scroll",
|
|
1743
|
+
priority: 10,
|
|
1744
|
+
enabledFor: (tool) => tool.enables.has("scroll") || panFallback,
|
|
1745
|
+
onDown: (s) => {
|
|
1746
|
+
if (!isPanTool() && s.page) return false;
|
|
1747
|
+
last = s.viewport;
|
|
1748
|
+
interaction.setCursor("stage-grab", "grabbing", 40);
|
|
1749
|
+
return true;
|
|
1750
|
+
},
|
|
1751
|
+
onMove: (s) => {
|
|
1752
|
+
stage.panBy(s.viewport.x - last.x, s.viewport.y - last.y);
|
|
1753
|
+
last = s.viewport;
|
|
1754
|
+
},
|
|
1755
|
+
onUp: () => interaction.setCursor("stage-grab", null),
|
|
1756
|
+
onHover: (s) => {
|
|
1757
|
+
if (panFallback && !isPanTool()) interaction.setCursor("stage-pan-fallback", s.page ? null : "grab", 5);
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1262
1760
|
}
|
|
1263
1761
|
//#endregion
|
|
1264
|
-
export { DEFAULT_SETTINGS, StageToken, ZoomMode, destinationToReveal, settingsEqual, stagePlugin
|
|
1762
|
+
export { DEFAULT_RESPONSIVE, DEFAULT_SETTINGS, StageToken, ZoomMode, boxOf, createScrollHandler, destinationToReveal, matchesQuery, resolveResponsive, settingsEqual, stagePlugin };
|
|
1265
1763
|
|
|
1266
1764
|
//# sourceMappingURL=index.js.map
|