feat: Make tolerance a pixel value
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -4432,7 +4432,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
/** clamps scroll/zoom back into `appState.scrollConstraints` (no-op when
|
||||
* unconstrained). Runs as a queued update, so it sees the preceding change.
|
||||
* `tolerance` (0–1) relaxes the bounds for rubberbanding. */
|
||||
* `tolerance` (screen px) relaxes the bounds for rubberbanding. */
|
||||
private constrainViewportToScrollConstraints = (tolerance = 0) => {
|
||||
this.setState((prevState) =>
|
||||
prevState.scrollConstraints
|
||||
|
||||
@@ -90,9 +90,10 @@ const constrainScrollAxis = (
|
||||
* inside the box, any zoom anchor (which lives within the viewport) is also
|
||||
* guaranteed to stay inside the box.
|
||||
*
|
||||
* `tolerance` (0–1, a fraction of the viewport) relaxes the bounds for
|
||||
* rubberbanding: the viewport may pan past the edges / zoom out below the fit
|
||||
* zoom by that fraction. Pass `0` (default) for a hard clamp.
|
||||
* `tolerance` is the rubberband overscroll allowance, in screen pixels: the
|
||||
* viewport may pan past the box edges (and zoom out below the fit zoom) until
|
||||
* each edge has crossed the box by up to `tolerance` pixels on screen,
|
||||
* independent of the current zoom. Pass `0` (default) for a hard clamp.
|
||||
*/
|
||||
export const constrainScrollState = (
|
||||
state: Pick<
|
||||
@@ -107,31 +108,35 @@ export const constrainScrollState = (
|
||||
return { scrollX: state.scrollX, scrollY: state.scrollY, zoom: state.zoom };
|
||||
}
|
||||
|
||||
tolerance = clamp(tolerance, 0, 1);
|
||||
tolerance = Math.max(tolerance, 0);
|
||||
|
||||
const minZoom = getMinZoomForConstraints(scrollConstraints, {
|
||||
width,
|
||||
height,
|
||||
// relax the min zoom so the user can briefly zoom out past the fit zoom,
|
||||
// letting each viewport edge cross the box by up to `tolerance` screen px
|
||||
const relaxedMinZoom = getMinZoomForConstraints(scrollConstraints, {
|
||||
width: width - 2 * tolerance,
|
||||
height: height - 2 * tolerance,
|
||||
});
|
||||
// relax the min zoom so the user can briefly zoom out past the fit zoom
|
||||
const zoomValue = getNormalizedZoom(
|
||||
clamp(state.zoom.value, minZoom * (1 - tolerance), MAX_ZOOM),
|
||||
clamp(state.zoom.value, relaxedMinZoom, MAX_ZOOM),
|
||||
);
|
||||
|
||||
// `tolerance` screen px expressed in scene coords at the current zoom
|
||||
const overscroll = tolerance / zoomValue;
|
||||
|
||||
return {
|
||||
scrollX: constrainScrollAxis(
|
||||
state.scrollX,
|
||||
scrollConstraints.x,
|
||||
scrollConstraints.width,
|
||||
width / zoomValue,
|
||||
(tolerance * width) / zoomValue,
|
||||
overscroll,
|
||||
),
|
||||
scrollY: constrainScrollAxis(
|
||||
state.scrollY,
|
||||
scrollConstraints.y,
|
||||
scrollConstraints.height,
|
||||
height / zoomValue,
|
||||
(tolerance * height) / zoomValue,
|
||||
overscroll,
|
||||
),
|
||||
zoom: { value: zoomValue },
|
||||
};
|
||||
|
||||
@@ -221,19 +221,38 @@ describe("setScrollConstraints (integration)", () => {
|
||||
describe("rubberband tolerance (pure)", () => {
|
||||
const box: ScrollConstraints = { x: 0, y: 0, width: 1000, height: 1000 };
|
||||
|
||||
it("allows scroll overscroll up to `tolerance` fraction of the viewport", () => {
|
||||
const tolerance = 0.25;
|
||||
// hard max for scrollX is -box.x = 0; soft adds tolerance * width / zoom
|
||||
it("allows scroll overscroll up to `tolerance` screen pixels", () => {
|
||||
const tolerance = 30; // screen px
|
||||
// hard max for scrollX is -box.x = 0; soft adds tolerance / zoom scene px
|
||||
const result = constrainScrollState(
|
||||
makeState({ scrollX: 999, scrollConstraints: { ...box, tolerance } }),
|
||||
tolerance,
|
||||
);
|
||||
expect(result.scrollX).toBeCloseTo((tolerance * VIEWPORT.width) / 1); // 50
|
||||
expect(result.scrollX).toBeCloseTo(tolerance / 1); // 30 (zoom 1)
|
||||
});
|
||||
|
||||
it("relaxes the minimum zoom by the `tolerance` fraction", () => {
|
||||
const tolerance = 0.25;
|
||||
const minZoom = getMinZoomForConstraints(box, VIEWPORT); // 0.2
|
||||
it("keeps the overscroll a fixed screen distance regardless of zoom", () => {
|
||||
const tolerance = 30; // screen px
|
||||
const result = constrainScrollState(
|
||||
makeState({
|
||||
scrollX: 999,
|
||||
zoom: { value: getNormalizedZoom(2) },
|
||||
scrollConstraints: { ...box, tolerance },
|
||||
}),
|
||||
tolerance,
|
||||
);
|
||||
// 30 screen px at zoom 2 -> 15 scene px of overscroll
|
||||
expect(result.scrollX).toBeCloseTo(tolerance / 2); // 15
|
||||
});
|
||||
|
||||
it("relaxes the minimum zoom by the `tolerance` screen pixels", () => {
|
||||
const tolerance = 25; // screen px
|
||||
// relaxed min zoom lets the box shrink within the viewport by `tolerance`
|
||||
// px on each side: max((200-50)/1000, (100-50)/1000) = 0.15
|
||||
const expected = getMinZoomForConstraints(box, {
|
||||
width: VIEWPORT.width - 2 * tolerance,
|
||||
height: VIEWPORT.height - 2 * tolerance,
|
||||
});
|
||||
const result = constrainScrollState(
|
||||
makeState({
|
||||
zoom: { value: getNormalizedZoom(0.01) },
|
||||
@@ -241,7 +260,7 @@ describe("rubberband tolerance (pure)", () => {
|
||||
}),
|
||||
tolerance,
|
||||
);
|
||||
expect(result.zoom.value).toBeCloseTo(minZoom * (1 - tolerance)); // 0.15
|
||||
expect(result.zoom.value).toBeCloseTo(expected); // 0.15
|
||||
});
|
||||
|
||||
it("hard-clamps when tolerance is 0 (default)", () => {
|
||||
@@ -305,7 +324,7 @@ describe("rubberband tolerance (integration)", () => {
|
||||
y: 0,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
tolerance: 0.25,
|
||||
tolerance: 25,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -315,8 +334,10 @@ describe("rubberband tolerance (integration)", () => {
|
||||
// page-up pans up, pushing scrollY past the hard edge into the overscroll
|
||||
Keyboard.keyPress(KEYS.PAGE_UP);
|
||||
|
||||
// overscrolled past 0, but bounded by tolerance * height = 25
|
||||
// overscrolled past 0, but bounded by tolerance (25 screen px / zoom 1)
|
||||
expect(h.state.scrollY).toBeGreaterThan(0);
|
||||
expect(h.state.scrollY).toBeLessThanOrEqual(0.25 * h.state.height + 0.001);
|
||||
expect(h.state.scrollY).toBeLessThanOrEqual(
|
||||
25 / h.state.zoom.value + 0.001,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user