All files / src/inputType PinchInput.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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249        3x   3x   3x                                                                                   3x   15x 15x   15x 15x   15x   15x           15x 15x             15x 15x 15x     3x 12x     3x 14x     14x 14x         14x     3x 28x 28x 28x   28x             3x 15x 15x               3x 1x 1x               3x 3x 3x               3x 6x     3x 12x 12x 12x 2x     10x 10x 10x 10x 10x     3x 576x 576x 576x 576x           96x     480x 480x         480x 334x 334x   480x     3x 12x 12x 12x         7x     5x 5x 5x 5x     14x 14x 14x 14x     14x     14x 14x 14x 14x 14x   14x 14x   14x 28x       28x 28x 28x 28x 28x 27x   28x 27x   28x 54x     28x 28x     480x 480x     3x 480x   3x  
/*
 * Copyright (c) 2015 NAVER Corp.
 * egjs projects are licensed under the MIT license
 */
import { $, isCssPropsFromAxes, setCssProps, revertCssProps } from "../utils";
import { ActiveEvent, ElementType, InputEventType } from "../types";
import { DIRECTION_ALL } from "../const";
 
import {
  toAxis,
  convertInputType,
  InputType,
  InputTypeObserver,
} from "./InputType";
 
export interface PinchInputOption {
  scale?: number;
  threshold?: number;
  inputType?: string[];
  touchAction?: string;
}
 
/**
 * @typedef {Object} PinchInputOption The option object of the eg.Axes.PinchInput module
 * @ko eg.Axes.PinchInput 모듈의 옵션 객체
 * @param {Number} [scale=1] Coordinate scale that a user can move<ko>사용자의 동작으로 이동하는 좌표의 배율</ko>
 * @param {Number} [threshold=0] Minimal scale before recognizing <ko>사용자의 Pinch 동작을 인식하기 위해산 최소한의 배율</ko>
 * @param {String[]} [inputType=["touch", "pointer"]] Types of input devices
 * - touch: Touch screen
 * - pointer: Mouse and touch <ko>입력 장치 종류
 * - touch: 터치 입력 장치
 * - pointer: 마우스 및 터치</ko>
 * @param {String} [touchAction="none"] Value that overrides the element's "touch-action" css property. It is set to "none" to prevent scrolling during touch. <ko>엘리먼트의 "touch-action" CSS 속성을 덮어쓰는 값. 터치 도중 스크롤을 방지하기 위해 "none" 으로 설정되어 있다.</ko>
 **/
 
/**
 * A module that passes the amount of change to eg.Axes when two pointers are moving toward (zoom-in) or away from each other (zoom-out). use one axis.
 * @ko 2개의 pointer를 이용하여 zoom-in하거나 zoom-out 하는 동작의 변화량을 eg.Axes에 전달하는 모듈. 한 개 의 축을 사용한다.
 * @example
 * ```js
 * const pinch = new eg.Axes.PinchInput("#area", {
 *   scale: 1
 * });
 *
 * // Connect 'something' axis when two pointers are moving toward (zoom-in) or away from each other (zoom-out).
 * axes.connect("something", pinch);
 * ```
 * @param {HTMLElement|String|jQuery} element An element to use the eg.Axes.PinchInput module <ko>eg.Axes.PinchInput 모듈을 사용할 엘리먼트</ko>
 * @param {PinchInputOption} [options] The option object of the eg.Axes.PinchInput module<ko>eg.Axes.PinchInput 모듈의 옵션 객체</ko>
 */
export class PinchInput implements InputType {
  public options: PinchInputOption;
  public axes: string[] = [];
  public element: HTMLElement = null;
  private _observer: InputTypeObserver;
  private _pinchFlag = false;
  private _enabled = false;
  private _originalCssProps: { [key: string]: string };
  private _activeEvent: ActiveEvent = null;
  private _baseValue: number;
  private _isOverThreshold = false;
 
  /**
   *
   */
  public constructor(el: ElementType, options?: PinchInputOption) {
    this.element = $(el);
    this.options = {
      scale: 1,
      threshold: 0,
      inputType: ["touch", "pointer"],
      touchAction: "none",
      ...options,
    };
    this._onPinchStart = this._onPinchStart.bind(this);
    this._onPinchMove = this._onPinchMove.bind(this);
    this._onPinchEnd = this._onPinchEnd.bind(this);
  }
 
  public mapAxes(axes: string[]) {
    this.axes = axes;
  }
 
  public connect(observer: InputTypeObserver): InputType {
    Iif (this._activeEvent) {
      this._detachEvent();
    }
    this._attachEvent(observer);
    this._originalCssProps = setCssProps(
      this.element,
      this.options,
      DIRECTION_ALL
    );
    return this;
  }
 
  public disconnect() {
    this._detachEvent();
    Eif (!isCssPropsFromAxes(this._originalCssProps)) {
      revertCssProps(this.element, this._originalCssProps);
    }
    return this;
  }
 
  /**
   * Destroys elements, properties, and events used in a module.
   * @ko 모듈에 사용한 엘리먼트와 속성, 이벤트를 해제한다.
   */
  public destroy() {
    this.disconnect();
    this.element = null;
  }
 
  /**
   * Enables input devices
   * @ko 입력 장치를 사용할 수 있게 한다
   * @return {PinchInput} An instance of a module itself <ko>모듈 자신의 인스턴스</ko>
   */
  public enable() {
    this._enabled = true;
    return this;
  }
 
  /**
   * Disables input devices
   * @ko 입력 장치를 사용할 수 없게 한다.
   * @return {PinchInput} An instance of a module itself <ko>모듈 자신의 인스턴스</ko>
   */
  public disable() {
    this._enabled = false;
    return this;
  }
 
  /**
   * Returns whether to use an input device
   * @ko 입력 장치를 사용 여부를 반환한다.
   * @return {Boolean} Whether to use an input device <ko>입력장치 사용여부</ko>
   */
  public isEnabled() {
    return this._enabled;
  }
 
  private _onPinchStart(event: InputEventType) {
    const activeEvent = this._activeEvent;
    const pinchEvent = activeEvent.onEventStart(event);
    if (!pinchEvent || !this._enabled || activeEvent.getTouches(event) !== 2) {
      return;
    }
 
    this._baseValue = this._observer.get(this)[this.axes[0]];
    this._observer.hold(this, event);
    this._pinchFlag = true;
    this._isOverThreshold = false;
    activeEvent.prevEvent = pinchEvent;
  }
 
  private _onPinchMove(event: InputEventType) {
    const threshold = this.options.threshold;
    const activeEvent = this._activeEvent;
    const pinchEvent = activeEvent.onEventMove(event);
    if (
      !pinchEvent ||
      !this._pinchFlag ||
      !this._enabled ||
      activeEvent.getTouches(event) !== 2
    ) {
      return;
    }
 
    const distance = this._getDistance(pinchEvent.scale);
    const offset = this._getOffset(
      pinchEvent.scale,
      activeEvent.prevEvent.scale
    );
 
    if (this._isOverThreshold || distance >= threshold) {
      this._isOverThreshold = true;
      this._observer.change(this, event, toAxis(this.axes, [offset]));
    }
    activeEvent.prevEvent = pinchEvent;
  }
 
  private _onPinchEnd(event: InputEventType) {
    const activeEvent = this._activeEvent;
    activeEvent.onEventEnd(event);
    if (
      !this._pinchFlag ||
      !this._enabled ||
      activeEvent.getTouches(event) >= 2
    ) {
      return;
    }
 
    activeEvent.onRelease();
    this._observer.release(this, event, [0], 0);
    this._baseValue = null;
    this._pinchFlag = false;
  }
 
  private _attachEvent(observer: InputTypeObserver) {
    const activeEvent = convertInputType(this.options.inputType);
    const element = this.element;
    Iif (!activeEvent) {
      return;
    }
    Iif (!element) {
      throw new Error("Element to connect input does not exist.");
    }
    this._observer = observer;
    this._enabled = true;
    this._activeEvent = activeEvent;
    activeEvent.start.forEach((event) => {
      element.addEventListener(event, this._onPinchStart, false);
    });
    activeEvent.move.forEach((event) => {
      element.addEventListener(event, this._onPinchMove, false);
    });
    activeEvent.end.forEach((event) => {
      element.addEventListener(event, this._onPinchEnd, false);
    });
  }
 
  private _detachEvent() {
    const activeEvent = this._activeEvent;
    const element = this.element;
    Eif (element) {
      activeEvent?.start.forEach((event) => {
        element.removeEventListener(event, this._onPinchStart, false);
      });
      activeEvent?.move.forEach((event) => {
        element.removeEventListener(event, this._onPinchMove, false);
      });
      activeEvent?.end.forEach((event) => {
        element.removeEventListener(event, this._onPinchEnd, false);
      });
    }
    this._enabled = false;
    this._observer = null;
  }
 
  private _getOffset(pinchScale: number, Iprev: number = 1): number {
    return this._baseValue * (pinchScale - prev) * this.options.scale;
  }
 
  private _getDistance(pinchScale: number): number {
    return Math.abs(pinchScale - 1);
  }
}