/** * 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; }