/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {clamp} from './clamp';
/**
* Single-axis offset and length state.
*
* ```
* contentStart containerStart containerEnd contentEnd
* |<----------offset| | |
* |<-------------------length------------------->|
* ```
*/
export type ScrollState = {
offset: number,
length: number,
};
function clampOffset(state: ScrollState, containerLength: number): ScrollState {
return {
offset: clamp(-(state.length - containerLength), 0, state.offset),
length: state.length,
};
}
function clampLength({
state,
minContentLength,
maxContentLength,
containerLength,
}: {
state: ScrollState,
minContentLength: number,
maxContentLength: number,
containerLength: number,
}): ScrollState {
return {
offset: state.offset,
length: clamp(
Math.max(minContentLength, containerLength),
Math.max(containerLength, maxContentLength),
state.length,
),
};
}
/**
* Returns `state` clamped such that:
* - `length`: you won't be able to zoom in/out such that the content is
* shorter than the `containerLength`.
* - `offset`: content remains in `containerLength`.
*/
export function clampState({
state,
minContentLength,
maxContentLength,
containerLength,
}: {
state: ScrollState,
minContentLength: number,
maxContentLength: number,
containerLength: number,
}): ScrollState {
return clampOffset(
clampLength({
state,
minContentLength,
maxContentLength,
containerLength,
}),
containerLength,
);
}
export function translateState({
state,
delta,
containerLength,
}: {
state: ScrollState,
delta: number,
containerLength: number,
}): ScrollState {
return clampOffset(
{
offset: state.offset + delta,
length: state.length,
},
containerLength,
);
}
/**
* Returns a new clamped `state` zoomed by `multiplier`.
*
* The provided fixed point will also remain stationary relative to
* `containerStart`.
*
* ```
* contentStart containerStart fixedPoint containerEnd
* |<---------offset-| x |
* |-fixedPoint-------------------------------->x |
* |-fixedPointFromContainer->x |
* |<----------containerLength----------->|
* ```
*/
export function zoomState({
state,
multiplier,
fixedPoint,
minContentLength,
maxContentLength,
containerLength,
}: {
state: ScrollState,
multiplier: number,
fixedPoint: number,
minContentLength: number,
maxContentLength: number,
containerLength: number,
}): ScrollState {
// Length and offset must be computed separately, so that if the length is
// clamped the offset will still be correct (unless it gets clamped too).
const zoomedState = clampLength({
state: {
offset: state.offset,
length: state.length * multiplier,
},
minContentLength,
maxContentLength,
containerLength,
});
// Adjust offset so that distance between containerStart<->fixedPoint is fixed
const fixedPointFromContainer = fixedPoint + state.offset;
const scaledFixedPoint = fixedPoint * (zoomedState.length / state.length);
const offsetAdjustedState = clampOffset(
{
offset: fixedPointFromContainer - scaledFixedPoint,
length: zoomedState.length,
},
containerLength,
);
return offsetAdjustedState;
}
export function moveStateToRange({
state,
rangeStart,
rangeEnd,
contentLength,
minContentLength,
maxContentLength,
containerLength,
}: {
state: ScrollState,
rangeStart: number,
rangeEnd: number,
contentLength: number,
minContentLength: number,
maxContentLength: number,
containerLength: number,
}): ScrollState {
// Length and offset must be computed separately, so that if the length is
// clamped the offset will still be correct (unless it gets clamped too).
const lengthClampedState = clampLength({
state: {
offset: state.offset,
length: contentLength * (containerLength / (rangeEnd - rangeStart)),
},
minContentLength,
maxContentLength,
containerLength,
});
const offsetAdjustedState = clampOffset(
{
offset: -rangeStart * (lengthClampedState.length / contentLength),
length: lengthClampedState.length,
},
containerLength,
);
return offsetAdjustedState;
}
export function areScrollStatesEqual(
state1: ScrollState,
state2: ScrollState,
): boolean {
return state1.offset === state2.offset && state1.length === state2.length;
}