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