All files / src/animation EasingManager.ts

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114    9x   9x   9x   292x 292x           9x 484x 484x     9x 5x 5x       5x 5x 5x 5x 1x 1x   4x 3x         3x 3x 3x   4x 3x       3x 3x       9x 97x 97x 97x 97x             2535x 2535x 2535x 2535x 2535x 5072x   2535x 2535x 2535x   2535x   5072x                 5072x         5072x     13x   13x 13x   5072x   2535x 2535x             9x 3503x   9x  
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;
    Iif (!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);
  }
}