/* * Copyright (c) 2015 NAVER Corp. * egjs projects are licensed under the MIT license */ import { OnRelease } from "@egjs/axes"; import FlickingError from "../core/FlickingError"; import AnchorPoint from "../core/AnchorPoint"; import { circulateIndex, clamp, getFlickingAttached } from "../utils"; import * as AXES from "../const/axes"; import * as ERROR from "../const/error"; import Control from "./Control"; /** * An options for the {@link SnapControl} * @ko {@link SnapControl} 생성시 사용되는 옵션 * @interface * @property {number} count Maximum number of panels can go after release입력 중단 이후 통과하여 이동할 수 있는 패널의 최대 갯수 */ export interface SnapControlOptions { count: number; } /** * A {@link Control} that uses a release momentum to choose destination panel * @ko 입력을 중단한 시점의 가속도에 영향받아 도달할 패널을 계산하는 이동 방식을 사용하는 {@link Control} */ class SnapControl extends Control { private _count: SnapControlOptions["count"]; /** * Maximum number of panels can go after release * @ko 입력 중단 이후 통과하여 이동할 수 있는 패널의 최대 갯수 * @type {number} * @default Infinity */ public get count() { return this._count; } public set count(val: SnapControlOptions["count"]) { this._count = val; } /** */ public constructor({ count = Infinity }: Partial = {}) { super(); this._count = count; } /** * Move {@link Camera} to the given position * @ko {@link Camera}를 주어진 좌표로 이동합니다 * @param {number} position The target position to move이동할 좌표 * @param {number} duration Duration of the panel movement animation (unit: ms).패널 이동 애니메이션 진행 시간 (단위: ms) * @param {object} [axesEvent] {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html#event:release release} event of {@link https://naver.github.io/egjs-axes/ Axes} * {@link https://naver.github.io/egjs-axes/ Axes}의 {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html#event:release release} 이벤트 * @fires Flicking#moveStart * @fires Flicking#move * @fires Flicking#moveEnd * @fires Flicking#willChange * @fires Flicking#changed * @fires Flicking#willRestore * @fires Flicking#restored * @fires Flicking#needPanel * @fires Flicking#visibleChange * @fires Flicking#reachEdge * @throws {FlickingError} * |code|condition| * |---|---| * |{@link ERROR_CODE POSITION_NOT_REACHABLE}|When the given panel is already removed or not in the Camera's {@link Camera#range range}| * |{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING}|When {@link Control#init init} is not called before| * |{@link ERROR_CODE ANIMATION_INTERRUPTED}|When the animation is interrupted by user input| * |{@link ERROR_CODE STOP_CALLED_BY_USER}|When the animation is interrupted by user input| * * * |code|condition| * |---|---| * |{@link ERROR_CODE POSITION_NOT_REACHABLE}|주어진 패널이 제거되었거나, Camera의 {@link Camera#range range} 밖에 있을 경우| * |{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING}|{@link Control#init init}이 이전에 호출되지 않은 경우| * |{@link ERROR_CODE ANIMATION_INTERRUPTED}|사용자 입력에 의해 애니메이션이 중단된 경우| * |{@link ERROR_CODE STOP_CALLED_BY_USER}|발생된 이벤트들 중 하나라도 `stop()`이 호출된 경우| * * * @return {Promise} A Promise which will be resolved after reaching the target position해당 좌표 도달시에 resolve되는 Promise */ public moveToPosition(position: number, duration: number, axesEvent?: OnRelease) { const flicking = getFlickingAttached(this._flicking); const camera = flicking.camera; const activeAnchor = camera.findActiveAnchor(); const anchorAtCamera = camera.findNearestAnchor(camera.position); const state = this._controller.state; if (!activeAnchor || !anchorAtCamera) { return Promise.reject(new FlickingError(ERROR.MESSAGE.POSITION_NOT_REACHABLE(position), ERROR.CODE.POSITION_NOT_REACHABLE)); } const snapThreshold = this._calcSnapThreshold(position, activeAnchor); const posDelta = flicking.animating ? state.delta : position - camera.position; const absPosDelta = Math.abs(posDelta); const snapDelta = axesEvent && axesEvent.delta[AXES.POSITION_KEY] !== 0 ? Math.abs(axesEvent.delta[AXES.POSITION_KEY]) : absPosDelta; let targetAnchor: AnchorPoint; if (snapDelta >= snapThreshold && snapDelta > 0) { // Move to anchor at position targetAnchor = this._findSnappedAnchor(position, anchorAtCamera); } else if (absPosDelta >= flicking.threshold && absPosDelta > 0) { // Move to the adjacent panel targetAnchor = this._findAdjacentAnchor(position, posDelta, anchorAtCamera); } else { // Restore to active panel return this.moveToPanel(activeAnchor.panel, { duration, axesEvent }); } this._triggerIndexChangeEvent(targetAnchor.panel, position, axesEvent); return this._animateToPosition({ position: camera.clampToReachablePosition(targetAnchor.position), duration, newActivePanel: targetAnchor.panel, axesEvent }); } private _findSnappedAnchor(position: number, anchorAtCamera: AnchorPoint): AnchorPoint { const flicking = getFlickingAttached(this._flicking); const camera = flicking.camera; const count = this._count; const currentPos = camera.position; const clampedPosition = camera.clampToReachablePosition(position); const anchorAtPosition = camera.findAnchorIncludePosition(clampedPosition); if (!anchorAtCamera || !anchorAtPosition) { throw new FlickingError(ERROR.MESSAGE.POSITION_NOT_REACHABLE(position), ERROR.CODE.POSITION_NOT_REACHABLE); } if (!isFinite(count)) { return anchorAtPosition; } const panelCount = flicking.panelCount; const anchors = camera.anchorPoints; let loopCount = Math.sign(position - currentPos) * Math.floor(Math.abs(position - currentPos) / camera.rangeDiff); if ((position > currentPos && anchorAtPosition.index < anchorAtCamera.index) || (anchorAtPosition.position > anchorAtCamera.position && anchorAtPosition.index === anchorAtCamera.index)) { loopCount += 1; } else if ((position < currentPos && anchorAtPosition.index > anchorAtCamera.index) || (anchorAtPosition.position < anchorAtCamera.position && anchorAtPosition.index === anchorAtCamera.index)) { loopCount -= 1; } const circularIndexOffset = loopCount * panelCount; const anchorAtPositionIndex = anchorAtPosition.index + circularIndexOffset; if (Math.abs(anchorAtPositionIndex - anchorAtCamera.index) <= count) { const anchor = anchors[anchorAtPosition.index]; return new AnchorPoint({ index: anchor.index, position: anchor.position + loopCount * camera.rangeDiff, panel: anchor.panel }); } if (flicking.circularEnabled) { const targetAnchor = anchors[circulateIndex(anchorAtCamera.index + Math.sign(position - currentPos) * count, panelCount)]; let loop = Math.floor(count / panelCount); if (position > currentPos && targetAnchor.index < anchorAtCamera.index) { loop += 1; } else if (position < currentPos && targetAnchor.index > anchorAtCamera.index) { loop -= 1; } return new AnchorPoint({ index: targetAnchor.index, position: targetAnchor.position + loop * camera.rangeDiff, panel: targetAnchor.panel }); } else { return anchors[clamp(anchorAtCamera.index + Math.sign(position - currentPos) * count, 0, anchors.length - 1)]; } } private _findAdjacentAnchor(position: number, posDelta: number, anchorAtCamera: AnchorPoint): AnchorPoint { const flicking = getFlickingAttached(this._flicking); const camera = flicking.camera; if (camera.circularEnabled) { const anchorIncludePosition = camera.findAnchorIncludePosition(position); if (anchorIncludePosition && anchorIncludePosition.position !== anchorAtCamera.position) { return anchorIncludePosition; } } const adjacentAnchor = (posDelta > 0 ? camera.getNextAnchor(anchorAtCamera) : camera.getPrevAnchor(anchorAtCamera)) ?? anchorAtCamera; return adjacentAnchor; } private _calcSnapThreshold(position: number, activeAnchor: AnchorPoint): number { const isNextDirection = position > activeAnchor.position; const panel = activeAnchor.panel; const panelSize = panel.size; const alignPos = panel.alignPosition; // Minimum distance needed to decide prev/next panel as nearest /* * | Prev | Next | * |<------>|<------------>| * [ |<-Anchor ] */ return isNextDirection ? panelSize - alignPos + panel.margin.next : alignPos + panel.margin.prev; } } export default SnapControl;