@egjs/axes
Version:
A module used to change the information of user action entered by various input devices such as touch screen or mouse into the logical virtual coordinates. You can easily create a UI that responds to user actions.
114 lines (102 loc) • 4.23 kB
text/typescript
import { AxesOption } from "../Axes";
import { Axis } from "../AxisManager";
import { getCirculatedPos } from "../Coordinate";
import { AnimationParam, UpdateAnimationOption } from "../types";
import { map } from "../utils";
import { AnimationManager, AnimationState } from "./AnimationManager";
export class EasingManager extends AnimationManager {
protected _useDuration = true;
protected _options: AxesOption;
private _durationOffset: number;
private _initialEasingPer: number;
private _prevEasingPer: number;
public interpolate(displacement: number, threshold: number): number {
const initSlope = this._easing(0.00001) / 0.00001;
return this._easing(displacement / (threshold * initSlope)) * threshold;
}
public updateAnimation(options: UpdateAnimationOption): void {
const animateParam = this._animateParam;
if (!animateParam) {
return;
}
const diffTime = new Date().getTime() - animateParam.startTime;
const pos = options?.destPos || animateParam.destPos;
const duration = options?.duration ?? animateParam.duration;
if (options?.restart || duration <= diffTime) {
this.setTo(pos, duration - diffTime);
return;
}
if (options?.destPos) {
const currentPos = this.axisManager.get();
// When destination is changed, new delta should be calculated as remaining percent.
// For example, moving x:0, y:0 to x:200, y:200 and it has current easing percent of 92%. coordinate is x:184 and y:184
// If destination changes to x:300, y:300. xdelta:200, ydelta:200 changes to xdelta:116, ydelta:116 and use remaining easingPer as 100%, not 8% as previous.
// Therefore, original easingPer by time is kept. And divided by (1 - self._initialEasingPer) which means new total easing percent. Like calculating 8% as 100%.
this._initialEasingPer = this._prevEasingPer;
animateParam.delta = this.axisManager.getDelta(currentPos, pos);
animateParam.destPos = pos;
}
if (options?.duration) {
const ratio = (diffTime + this._durationOffset) / animateParam.duration;
// Use durationOffset for keeping animation ratio after duration is changed.
// newRatio = (diffTime + newDurationOffset) / newDuration = oldRatio
// newDurationOffset = oldRatio * newDuration - diffTime
this._durationOffset = ratio * duration - diffTime;
animateParam.duration = duration;
}
}
protected _initState(info: AnimationParam): AnimationState {
this._initialEasingPer = 0;
this._prevEasingPer = 0;
this._durationOffset = 0;
return {
pos: info.depaPos,
easingPer: 0,
finished: false,
};
}
protected _getNextState(prevState: AnimationState): AnimationState {
const animateParam = this._animateParam;
const prevPos = prevState.pos;
const destPos = animateParam.destPos;
const directions = map(prevPos, (value, key) => {
return value <= destPos[key] ? 1 : -1;
});
const diffTime = new Date().getTime() - animateParam.startTime;
const ratio = (diffTime + this._durationOffset) / animateParam.duration;
const easingPer = this._easing(ratio);
const toPos: Axis = this.axisManager.map(prevPos, (pos, options, key) => {
const nextPos =
ratio >= 1
? destPos[key]
: pos +
(animateParam.delta[key] * (easingPer - this._prevEasingPer)) /
(1 - this._initialEasingPer);
// Subtract distance from distance already moved.
// Recalculate the remaining distance.
// Fix the bouncing phenomenon by changing the range.
const circulatedPos = getCirculatedPos(
nextPos,
options.range,
options.circular as boolean[]
);
if (nextPos !== circulatedPos) {
// circular
const rangeOffset =
directions[key] * (options.range[1] - options.range[0]);
destPos[key] -= rangeOffset;
prevPos[key] -= rangeOffset;
}
return circulatedPos;
});
this._prevEasingPer = easingPer;
return {
pos: toPos,
easingPer,
finished: easingPer >= 1,
};
}
private _easing(p: number): number {
return p > 1 ? 1 : this._options.easing(p);
}
}