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
|
/** clamps scroll/zoom back into `appState.scrollConstraints` (no-op when
|
||||||
* unconstrained). Runs as a queued update, so it sees the preceding change.
|
* 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) => {
|
private constrainViewportToScrollConstraints = (tolerance = 0) => {
|
||||||
this.setState((prevState) =>
|
this.setState((prevState) =>
|
||||||
prevState.scrollConstraints
|
prevState.scrollConstraints
|
||||||
|
|||||||
@@ -90,9 +90,10 @@ const constrainScrollAxis = (
|
|||||||
* inside the box, any zoom anchor (which lives within the viewport) is also
|
* inside the box, any zoom anchor (which lives within the viewport) is also
|
||||||
* guaranteed to stay inside the box.
|
* guaranteed to stay inside the box.
|
||||||
*
|
*
|
||||||
* `tolerance` (0–1, a fraction of the viewport) relaxes the bounds for
|
* `tolerance` is the rubberband overscroll allowance, in screen pixels: the
|
||||||
* rubberbanding: the viewport may pan past the edges / zoom out below the fit
|
* viewport may pan past the box edges (and zoom out below the fit zoom) until
|
||||||
* zoom by that fraction. Pass `0` (default) for a hard clamp.
|
* 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 = (
|
export const constrainScrollState = (
|
||||||
state: Pick<
|
state: Pick<
|
||||||
@@ -107,31 +108,35 @@ export const constrainScrollState = (
|
|||||||
return { scrollX: state.scrollX, scrollY: state.scrollY, zoom: state.zoom };
|
return { scrollX: state.scrollX, scrollY: state.scrollY, zoom: state.zoom };
|
||||||
}
|
}
|
||||||
|
|
||||||
tolerance = clamp(tolerance, 0, 1);
|
tolerance = Math.max(tolerance, 0);
|
||||||
|
|
||||||
const minZoom = getMinZoomForConstraints(scrollConstraints, {
|
// relax the min zoom so the user can briefly zoom out past the fit zoom,
|
||||||
width,
|
// letting each viewport edge cross the box by up to `tolerance` screen px
|
||||||
height,
|
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(
|
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 {
|
return {
|
||||||
scrollX: constrainScrollAxis(
|
scrollX: constrainScrollAxis(
|
||||||
state.scrollX,
|
state.scrollX,
|
||||||
scrollConstraints.x,
|
scrollConstraints.x,
|
||||||
scrollConstraints.width,
|
scrollConstraints.width,
|
||||||
width / zoomValue,
|
width / zoomValue,
|
||||||
(tolerance * width) / zoomValue,
|
overscroll,
|
||||||
),
|
),
|
||||||
scrollY: constrainScrollAxis(
|
scrollY: constrainScrollAxis(
|
||||||
state.scrollY,
|
state.scrollY,
|
||||||
scrollConstraints.y,
|
scrollConstraints.y,
|
||||||
scrollConstraints.height,
|
scrollConstraints.height,
|
||||||
height / zoomValue,
|
height / zoomValue,
|
||||||
(tolerance * height) / zoomValue,
|
overscroll,
|
||||||
),
|
),
|
||||||
zoom: { value: zoomValue },
|
zoom: { value: zoomValue },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -221,19 +221,38 @@ describe("setScrollConstraints (integration)", () => {
|
|||||||
describe("rubberband tolerance (pure)", () => {
|
describe("rubberband tolerance (pure)", () => {
|
||||||
const box: ScrollConstraints = { x: 0, y: 0, width: 1000, height: 1000 };
|
const box: ScrollConstraints = { x: 0, y: 0, width: 1000, height: 1000 };
|
||||||
|
|
||||||
it("allows scroll overscroll up to `tolerance` fraction of the viewport", () => {
|
it("allows scroll overscroll up to `tolerance` screen pixels", () => {
|
||||||
const tolerance = 0.25;
|
const tolerance = 30; // screen px
|
||||||
// hard max for scrollX is -box.x = 0; soft adds tolerance * width / zoom
|
// hard max for scrollX is -box.x = 0; soft adds tolerance / zoom scene px
|
||||||
const result = constrainScrollState(
|
const result = constrainScrollState(
|
||||||
makeState({ scrollX: 999, scrollConstraints: { ...box, tolerance } }),
|
makeState({ scrollX: 999, scrollConstraints: { ...box, tolerance } }),
|
||||||
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", () => {
|
it("keeps the overscroll a fixed screen distance regardless of zoom", () => {
|
||||||
const tolerance = 0.25;
|
const tolerance = 30; // screen px
|
||||||
const minZoom = getMinZoomForConstraints(box, VIEWPORT); // 0.2
|
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(
|
const result = constrainScrollState(
|
||||||
makeState({
|
makeState({
|
||||||
zoom: { value: getNormalizedZoom(0.01) },
|
zoom: { value: getNormalizedZoom(0.01) },
|
||||||
@@ -241,7 +260,7 @@ describe("rubberband tolerance (pure)", () => {
|
|||||||
}),
|
}),
|
||||||
tolerance,
|
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)", () => {
|
it("hard-clamps when tolerance is 0 (default)", () => {
|
||||||
@@ -305,7 +324,7 @@ describe("rubberband tolerance (integration)", () => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 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
|
// page-up pans up, pushing scrollY past the hard edge into the overscroll
|
||||||
Keyboard.keyPress(KEYS.PAGE_UP);
|
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).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